require()
встречается в коде node-модулей почти так же часто, как Array#forEach()
. Самое обидное, что чаще всего мы подключаем модули "util"
, "fs"
и "path"
, чуть реже "url"
. Наличие других подключенных модулей зависит уже от задачи модуля. Причем, говоря о модуле "util"
, загружается в память node-процесса даже если вы ни разу его не подключали.В прошлой статье Node.JS Загрузка модулей по требованию я поведал о возможности автоматической загрузкой модуля при первом обращении к его именованной ссылке. Если честно, на момент написания той статьи, я не был уверен в том, что такой подход не станет причиной странного поведения node-процесса. Но, уже сегодня с гордостью могу ручаться, что
demandLoad()
работает уже пол года в продакшене. Как мы его только не гоняли… Это и нагрузочное тестирование конкретного процесса, и работа demandLoad()
в worker-процессах кластеров, и работа процесса под небольшой нагрузкой в течении долгого времени. Результаты сравнивались с использованием demandLoad()
и с использованием require()
. Никаких существенных отклонений в сравнении не было замечено.Сегодня речь пойдет уже не о стабильности
demandLoad()
. Если кому интересно, задавайте вопросы в комментариях, сделаю скриншоты, могу рассказать о методах и инструментах тестирования, других возможностях использования подхода. Сегодня, как следует из заголовка статьи, мы будем избавляться от успевших уже надоесть require()
в шапках каждого node-модуля.Заранее отмечу, ни в коем случае не агитирую использовать предложенный метод в продакшене. Практика изложена для ознакомления и не претендует на статус «true-практики». Громкий заголовок только для привлечения внимания.
Предположим, мы работаем над проектом с названием
"mytestsite.com"
, и находится он у нас здесь:~/projects/mytestsite.com
Создадим исполняемый файл нашего проекта, например, по адресу:
~/projects/mytestsite.com/lib/bin/server.js
Внутри попробуем вызвать подключенные модули без предварительных
require()
:console.log(util.inspect(url.parse('https://habrahabr.ru/')));
Теперь создадим файл
"require-all.js"
где-нибудь, вне всех проектов.Например, здесь:
~/projects/general/require-all.js
Согласно документации, все предопределенные переменные и константы каждого node-модуля являются свойствами
global
. Соответственно, мы можем определять и свои глобальные объекты. Так мы должны поступить со всеми используемыми нами модулями.Наполним
require-all.js
списком всех используемых модулей во всех проектах:// нет смысла оставлять неподгруженным модуль "util",
// т.к. его все равно до загрузки подгружает модуль "console".
// А console.log(), если ему передать объект единственным параметром,
// в свою очередь вызывает util.inspect()
global.util = require('util');
// так выглядит подключение других стандартных модулей, например:
demandLoad(global, 'fs', 'fs');
demandLoad(global, 'path', 'path');
demandLoad(global, 'url', 'url');
// абсолютно так же выглядит подключение npm-модулей, например:
demandLoad(global, 'express', 'express');
// а, вот, например, так можно подключить локальный модуль:
demandLoad(global, 'routes', './../mytestsite.com/lib/routes');
// определение demandLoad
function demandLoad(obj, name, modPath){
// тело вырезано для простоты схемы
// необходимо взять из статьи по ссылке выше.
}
Можно представить список модулей в виде массива или карты (
Map
), и, например, пройтись по нему/ней циклом, чтобы не повторять строчку кода с вызовом demandLoad()
. Можно, например, прочитать список используемых npm-модулей из package.json
. Если, например, количество используемых модулей очень высокое, и не хочется засорять глобальный скоуп, можно определить, например, пустой объект m
(let m = {}
), определить m
в global
(global['m'] = m
), и уже к m
применять demandLoad()
. Как говорится, кому как удобнее.Теперь, осталось лишь запустить это хозяйство. Добавим ключ
--require
к запуску node (версии >= 4.x):node --require ~/projects/general/require-all.js ~/projects/mytestsite.com/lib/bin/server.js
Ошибок нет. Скрипт отработал как надо:
Url {
protocol: 'https:',
slashes: true,
auth: null,
host: 'habrahabr.ru',
port: null,
hostname: 'habrahabr.ru',
hash: null,
search: null,
query: null,
pathname: '/',
path: '/',
href: 'https://habrahabr.ru/' }
Если у вас много проектов, для удобства разворачивания проектов, можно создать по своему
require-all.js
внутри каждого проекта по отдельности.node --require ~/projects/mytestsite.com/lib/require-all.js ~/projects/mytestsite.com/lib/bin/server.js
Расширяя последний случай, отмечу, можно даже использовать несколько таких
require-all.js
одновременно:node --require ~/projects/general/require-all.js --require ~/projects/mytestsite.com/lib/require-all.js ~/projects/mytestsite.com/lib/bin/server.js
Как отмечено в комментарии ниже, связка
--require
+global
также может быть использована для расширения/перегрузки стандартных возможностей node.Напоследок, повторюсь из прошлой статьи: Если
demandLoad()
определена не в нашем файле(1) (откуда вызываем demandLoad()
), а в каком-нибудь файле(2), причем файл(1) и файл(2) находятся в разных директориях, последним параметром необходимо передавать полный путь до модуля, например:demandLoad(global, 'routes', path.join(__dirname, './../mytestsite.com/lib/routes'));
Иначе, тот
require()
, что вызывается из demandLoad()
будет искать модуль относительно папки, где расположили тот самый файл(2) с описанием demandLoad()
, вместо того, чтобы искать модуль относительно файла(1), откуда мы вызываем demandLoad()
.Спасибо за внимание. Всем удачного рефакторинга!
Комментарии (16)
k12th
18.12.2015 14:14+5Явное все-таки лучше неявного.
ckr
18.12.2015 14:27-2Вы правы. Но идеология распределения имен в node сама по себе не идеальна.
То есть, мы не против глобальныхconsole
иprocess
.
Мы же не добавляем в шапку что-то вроде:
var console = require('console'); var process = require('process');
Почему разработчики node не сделали то же самое с модулемutil
?!
Ведь самutil
всегда загружен, и его методами активно пользуется та жеconsole
.k12th
18.12.2015 14:46+2Честно говоря, за несколько лет пользовался модулем util всего пару раз:) Я согласен, что как-то неконсистентно получается, но, с другой стороны, util — это, имхо, барахолка, сборник функций разного назначения, часть из которых устарела, часть deprecated, а часть нужна не каждый раз (готов аргументировать по каждому методу). Согласитесь, насильно пробрасывать эту барахолку в глобальный скоуп всем — как-то некомильфо.
Я думаю, если бы это было не так, этот вопрос давно бы уже подняли в сообществе и вынесли все нужное в глобальный скоуп.
Засорение глобального скоупа всегда потенцильно опасно. Через полгода выйдет ES2016, нет никакой гарантии, что в нем не появится глобальное имя util.ckr
18.12.2015 15:17Ну, если в ES2016 появится ключевое слово
util
, то, так и так придется переписывать исходники, будь то:
var util = require('util');
или
global.util = require('util');
А иногда даже неявное необходимо больше явного.
Например, если мы хотим заменить стандартныйconsole
собственным логгером.
Например, мы хотим, чтобыconsole.log()
выдавал в stdout не только строку, переданную параметром, а еще путь к файлу и номер строки, откуда этотconsole.log()
был вызван.
С помощьюglobal
и ключа node--require
это можно сделать прозрачно для самой программы, т.е. не исправляя исходный код самой программы.
И, вообще связкаglobal
+--require
, собственно, существует для расширения стандартных возможностей node.k12th
18.12.2015 15:23+1Ваш пример (да и вообще практически весь столь любимый вами модуль util) нужен исключительно для дебага. Для которого существуют и другие методы. Не говоря уж о том, что проще написать юнит-тесты, чем расставлять сотни console.log в надежде найти ошибку.
ckr
18.12.2015 16:11Вы преувеличиваете. Unit-тестами обычно покрывают свой исходный код, когда есть 100% понимание работы этого кода.
console.log()
не все используют только лишь в поиске ошибок.
Например, я его часто использую, когда разбираюсь в чужих исходных кодах.
При этом, сами чужие коды работают без ошибок.
Можно, конечно, подключить отладчик к IDE и выполнять скрипт step-by-step.
Но такой режим не очень подходит для изучения чужих исходных кодов и очень утомителен.k12th
18.12.2015 16:27Ну положим, вам так удобнее, не буду спорить. Ну а модуль util в процессе изучения чужого кода вам зачем?
ckr
18.12.2015 16:50Я, вроде бы, нигде не утверждал, что util нужен не в процессе изучения чужого кода.
В util использую чаще всего методы isArray(), isError() и проч.
На основе стандартного util, я его немного расширил, добавил методы isInt(), isGenerator() и проч., вот тот demandLoad() у меня util.demandLoad().
Еще пользуюсь util.inspect(), но это, опять же для дебага, в продакшене бесполезен.
Раньше пользовался util.inherits(), но он уже отходит с появлением классов в синтаксисе.k12th
18.12.2015 17:17Stability: 0 — Deprecated
Вместо util.isArray с незапамятных времен есть стандартный Array.isArray, если что.
я его немного расширил
Вообще непонятно, зачем менять стандартное, если можно свое завернуть в модуль, положить на какой-нить гитхаб и счастливо юзать. npm — лучшее изобретение со времен нарезанного хлеба. Тем более там наверняка такого счастья уже навалом.
Как я уже говорил — util состоит из deprecated, ненужного и util.inspect/util.format, которые нужны, в основном, для дебага.ckr
18.12.2015 18:15Про Array.isArray я в курсе. Как раз именно его util.isArray() использует в последних версиях.
Так, стандартное как раз я никогда не пытался заменить.
Самим node пользуюсь с версии 0.4.x. Могу, конечно, пропустить обновления в документации о deprecated-статусах.
Само использование util.isError() не выводит в stdout, что использую deprecated-функцию, в отличии от, например, util.exec(), который deprecated уже очень давно.
Ну а по поводу того, что расширил. Сами то исходники всего расширения в отдельном модуле. Так исторически получилось, что я назвал его util (можно было выбрать любое название). В работе просто подключаю его локально вместо стандартного util. В него стекались все простые функции абсолютно разного назначения, которых очень много. Чтобы не подключать букет всевозможных npm-модулей, все было собрано в один файл. Разумеется, ведь не в глобальный скоуп их подключать.
Выложил на pastebin
К сожалению, мы за деньги пишем пока только проприетарный код. Сами его используем. Сами разворачиваем. Сами поддерживаем. Ну, по разным понятным соображениям, публичным гитхабом пользоваться не стали. Недавно приняли решение об использовании приватного npm. Правда, дальше принятия решения дело не двинулось :) Ну, не до этого было.k12th
18.12.2015 18:20Понятненько.
Про--require
я как-то не знал, спасибо. Хотя я все-таки остаюсь за явное:)
mwizard
Но ведь это замусоривание глобального неймспейса заранее неизвестными идентификаторами :(
ckr
Работать другим модулям это не мешает. Переопределение любой переменной через
let
,const
илиvar
остается.Другой вопрос, если в модуле используются заранее не определенные переменные. И алгоритм рассчитывает, что их значения заранее undefined. Но это не очень хорошая практика, и при использовании
'use strict'
-режима может вызвать runtime-ошибку.ckr
Кроме того, например, у меня в самом большом проекте их подключено таким образом максимум штук 40. Это количество включает в себя некоторые стандартные модули, используемые npm-модули и локальные модули.
И никто не заставляет прописывать их все. Профит получается уже с часто используемых «util», «fs» и «path».
В целом, вы правы. Практика немного сыровата. Пользоваться или не пользоваться — дело индивидуальное. Но статья имеет место быть. Надеюсь, и вы научились для себя чему-то новому. Или вам жалко времени, убитого на прочтение статьи?