Анализируя исходные коды прошлых проектов, рейтинг популярности прямых вызовов функций показал, что прямой вызов 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)


  1. mwizard
    18.12.2015 10:31
    +8

    Но ведь это замусоривание глобального неймспейса заранее неизвестными идентификаторами :(


    1. ckr
      18.12.2015 11:07

      Работать другим модулям это не мешает. Переопределение любой переменной через let, const или var остается.
      Другой вопрос, если в модуле используются заранее не определенные переменные. И алгоритм рассчитывает, что их значения заранее undefined. Но это не очень хорошая практика, и при использовании 'use strict'-режима может вызвать runtime-ошибку.


    1. ckr
      18.12.2015 12:13

      Кроме того, например, у меня в самом большом проекте их подключено таким образом максимум штук 40. Это количество включает в себя некоторые стандартные модули, используемые npm-модули и локальные модули.
      И никто не заставляет прописывать их все. Профит получается уже с часто используемых «util», «fs» и «path».

      В целом, вы правы. Практика немного сыровата. Пользоваться или не пользоваться — дело индивидуальное. Но статья имеет место быть. Надеюсь, и вы научились для себя чему-то новому. Или вам жалко времени, убитого на прочтение статьи?


  1. k12th
    18.12.2015 14:14
    +5

    Явное все-таки лучше неявного.


    1. ckr
      18.12.2015 14:27
      -2

      Вы правы. Но идеология распределения имен в node сама по себе не идеальна.
      То есть, мы не против глобальных console и process.
      Мы же не добавляем в шапку что-то вроде:

      var console = require('console');
      var process = require('process');
      

      Почему разработчики node не сделали то же самое с модулем util?!
      Ведь сам util всегда загружен, и его методами активно пользуется та же console.


      1. k12th
        18.12.2015 14:46
        +2

        Честно говоря, за несколько лет пользовался модулем util всего пару раз:) Я согласен, что как-то неконсистентно получается, но, с другой стороны, util — это, имхо, барахолка, сборник функций разного назначения, часть из которых устарела, часть deprecated, а часть нужна не каждый раз (готов аргументировать по каждому методу). Согласитесь, насильно пробрасывать эту барахолку в глобальный скоуп всем — как-то некомильфо.
        Я думаю, если бы это было не так, этот вопрос давно бы уже подняли в сообществе и вынесли все нужное в глобальный скоуп.

        Засорение глобального скоупа всегда потенцильно опасно. Через полгода выйдет ES2016, нет никакой гарантии, что в нем не появится глобальное имя util.


        1. 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.


          1. k12th
            18.12.2015 15:23
            +1

            Ваш пример (да и вообще практически весь столь любимый вами модуль util) нужен исключительно для дебага. Для которого существуют и другие методы. Не говоря уж о том, что проще написать юнит-тесты, чем расставлять сотни console.log в надежде найти ошибку.


            1. ckr
              18.12.2015 16:11

              Вы преувеличиваете. Unit-тестами обычно покрывают свой исходный код, когда есть 100% понимание работы этого кода.
              console.log() не все используют только лишь в поиске ошибок.
              Например, я его часто использую, когда разбираюсь в чужих исходных кодах.
              При этом, сами чужие коды работают без ошибок.
              Можно, конечно, подключить отладчик к IDE и выполнять скрипт step-by-step.
              Но такой режим не очень подходит для изучения чужих исходных кодов и очень утомителен.


              1. k12th
                18.12.2015 16:27

                Ну положим, вам так удобнее, не буду спорить. Ну а модуль util в процессе изучения чужого кода вам зачем?


                1. ckr
                  18.12.2015 16:50

                  Я, вроде бы, нигде не утверждал, что util нужен не в процессе изучения чужого кода.
                  В util использую чаще всего методы isArray(), isError() и проч.
                  На основе стандартного util, я его немного расширил, добавил методы isInt(), isGenerator() и проч., вот тот demandLoad() у меня util.demandLoad().
                  Еще пользуюсь util.inspect(), но это, опять же для дебага, в продакшене бесполезен.
                  Раньше пользовался util.inherits(), но он уже отходит с появлением классов в синтаксисе.


                  1. k12th
                    18.12.2015 17:17

                    util.isError

                    Stability: 0 — Deprecated


                    Вместо util.isArray с незапамятных времен есть стандартный Array.isArray, если что.

                    я его немного расширил

                    Вообще непонятно, зачем менять стандартное, если можно свое завернуть в модуль, положить на какой-нить гитхаб и счастливо юзать. npm — лучшее изобретение со времен нарезанного хлеба. Тем более там наверняка такого счастья уже навалом.

                    Как я уже говорил — util состоит из deprecated, ненужного и util.inspect/util.format, которые нужны, в основном, для дебага.


                    1. 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. Правда, дальше принятия решения дело не двинулось :) Ну, не до этого было.


                      1. k12th
                        18.12.2015 18:20

                        Понятненько.

                        Про --require я как-то не знал, спасибо. Хотя я все-таки остаюсь за явное:)


  1. yojique
    21.12.2015 22:26

    если хотите, то можете исправить четыре опечатки denamdLoad в первых абзацах :)


    1. ckr
      22.12.2015 11:23

      Спасибо, исправил.