Node.JS Избавься от require() навсегда

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

      +8
      Но ведь это замусоривание глобального неймспейса заранее неизвестными идентификаторами :(
        0
        Работать другим модулям это не мешает. Переопределение любой переменной через let, const или var остается.
        Другой вопрос, если в модуле используются заранее не определенные переменные. И алгоритм рассчитывает, что их значения заранее undefined. Но это не очень хорошая практика, и при использовании 'use strict'-режима может вызвать runtime-ошибку.
          0
          Кроме того, например, у меня в самом большом проекте их подключено таким образом максимум штук 40. Это количество включает в себя некоторые стандартные модули, используемые npm-модули и локальные модули.
          И никто не заставляет прописывать их все. Профит получается уже с часто используемых «util», «fs» и «path».

          В целом, вы правы. Практика немного сыровата. Пользоваться или не пользоваться — дело индивидуальное. Но статья имеет место быть. Надеюсь, и вы научились для себя чему-то новому. Или вам жалко времени, убитого на прочтение статьи?
          +5
          Явное все-таки лучше неявного.
            –2
            Вы правы. Но идеология распределения имен в node сама по себе не идеальна.
            То есть, мы не против глобальных console и process.
            Мы же не добавляем в шапку что-то вроде:
            var console = require('console');
            var process = require('process');
            

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

              Засорение глобального скоупа всегда потенцильно опасно. Через полгода выйдет ES2016, нет никакой гарантии, что в нем не появится глобальное имя util.
                0
                Ну, если в ES2016 появится ключевое слово util, то, так и так придется переписывать исходники, будь то:
                var util = require('util');

                или
                global.util = require('util');


                А иногда даже неявное необходимо больше явного.
                Например, если мы хотим заменить стандартный console собственным логгером.
                Например, мы хотим, чтобы console.log() выдавал в stdout не только строку, переданную параметром, а еще путь к файлу и номер строки, откуда этот console.log() был вызван.
                С помощью global и ключа node --require это можно сделать прозрачно для самой программы, т.е. не исправляя исходный код самой программы.

                И, вообще связка global + --require, собственно, существует для расширения стандартных возможностей node.
                  +1
                  Ваш пример (да и вообще практически весь столь любимый вами модуль util) нужен исключительно для дебага. Для которого существуют и другие методы. Не говоря уж о том, что проще написать юнит-тесты, чем расставлять сотни console.log в надежде найти ошибку.
                    0
                    Вы преувеличиваете. Unit-тестами обычно покрывают свой исходный код, когда есть 100% понимание работы этого кода.
                    console.log() не все используют только лишь в поиске ошибок.
                    Например, я его часто использую, когда разбираюсь в чужих исходных кодах.
                    При этом, сами чужие коды работают без ошибок.
                    Можно, конечно, подключить отладчик к IDE и выполнять скрипт step-by-step.
                    Но такой режим не очень подходит для изучения чужих исходных кодов и очень утомителен.
                      0
                      Ну положим, вам так удобнее, не буду спорить. Ну а модуль util в процессе изучения чужого кода вам зачем?
                        0
                        Я, вроде бы, нигде не утверждал, что util нужен не в процессе изучения чужого кода.
                        В util использую чаще всего методы isArray(), isError() и проч.
                        На основе стандартного util, я его немного расширил, добавил методы isInt(), isGenerator() и проч., вот тот demandLoad() у меня util.demandLoad().
                        Еще пользуюсь util.inspect(), но это, опять же для дебага, в продакшене бесполезен.
                        Раньше пользовался util.inherits(), но он уже отходит с появлением классов в синтаксисе.
                          0
                          util.isError
                          Stability: 0 — Deprecated


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

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

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

                          Как я уже говорил — util состоит из deprecated, ненужного и util.inspect/util.format, которые нужны, в основном, для дебага.
                            0
                            Про Array.isArray я в курсе. Как раз именно его util.isArray() использует в последних версиях.
                            Так, стандартное как раз я никогда не пытался заменить.
                            Самим node пользуюсь с версии 0.4.x. Могу, конечно, пропустить обновления в документации о deprecated-статусах.
                            Само использование util.isError() не выводит в stdout, что использую deprecated-функцию, в отличии от, например, util.exec(), который deprecated уже очень давно.

                            Ну а по поводу того, что расширил. Сами то исходники всего расширения в отдельном модуле. Так исторически получилось, что я назвал его util (можно было выбрать любое название). В работе просто подключаю его локально вместо стандартного util. В него стекались все простые функции абсолютно разного назначения, которых очень много. Чтобы не подключать букет всевозможных npm-модулей, все было собрано в один файл. Разумеется, ведь не в глобальный скоуп их подключать.

                            Выложил на pastebin

                            К сожалению, мы за деньги пишем пока только проприетарный код. Сами его используем. Сами разворачиваем. Сами поддерживаем. Ну, по разным понятным соображениям, публичным гитхабом пользоваться не стали. Недавно приняли решение об использовании приватного npm. Правда, дальше принятия решения дело не двинулось :) Ну, не до этого было.
                              0
                              Понятненько.

                              Про --require я как-то не знал, спасибо. Хотя я все-таки остаюсь за явное:)
            0
            если хотите, то можете исправить четыре опечатки denamdLoad в первых абзацах :)
              0
              Спасибо, исправил.

            Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

            Самое читаемое