Я — frontend разработчик и в последнее время мне все чаще приходится пользоваться нодой, будь то использование webpack-а для сборки проекта, либо настройка различных gulp тасков. Хоть у меня и нету большого опыта в использование ноды, со временем у меня накопилось три вещи, которые мне хотелось бы улучшить при работе с модулями:

  • Избавиться от кучи require-ов в начале каждого файла
  • Подгружать модули только тогда, когда они нужны(особенно это актуально для gulp тасков)
  • Иметь возможность работать с локальными модулями проекта, как с внешними модулями, то есть вместо, например,
    вызова var core = require('../../deep/deep/deep/core/core'), вызывать этот же модуль вот так var core = require('core')

Для решения всех трех задач по отдельности есть уже различные модули и решения, но во-первых, все они имеют, как мне кажется, недостатки, а во-вторых, ни одно из решений не решает все три проблемы вместе.

Например, для подгрузки модулей по требованию(он же lazy load или load on demand), есть модуль gulp-load-plugins. Он решает 1-ую и 2-ую проблему, но не решает 3-юю и имеет еще один недостаток — чтобы подключить модули, нужно в каждом файле, где эти модули нужны, производить инициализацию gulp-load-plugins модуля. Можно, конечно, делать инициализацию в отдельном файле и экспортировать из файла значение, но в таком случае придется подключать этот файл с использованием относительных или абсолютного путей.

Для решения 3-ей проблемы около года назад в npm добавили поддержку так званых локальных модулей. Суть сводится к тому, что
в package.json в dependencies нужно указать в качестве ключей имена модулей, а в значениях — относительные пути к папкам локальных модулей с префиксом «file:», например, вот часть package.json файла:

  "dependencies": {
    "lodash": "^2.0.0",
    "core": "file:deep/deep/deep/core",
    "my-other-module": "file:/my-other-module"
  }

При этом папки локальных модулей должны быть оформлены, как обычные модули, то есть должны содержать свой package.json и readme.md файлы. После запуска npm i ваши локальные модули будут установлены в папку node_modules, как обычные модули. По мне так это крайне неудобно класть каждый файл проекта в отдельную папку, да еще и заводить на него package.json и readme.md файлы.

Одним словом я не нашел хорошего решения для описанных мною проблем, может плохо искал, но облазив различные форумы и прочитав различные свежие статьи пришел к выводу, что люди до сих пор ищут хорошее решение всех этих проблем.

В итоге я решил написать свое решение, возможно, свой велосипед, о котором и хочу поведать вам. На сколько он хорош или плох судить вам. Итак, позвольте представить вам sp-load. Сразу оговорюсь, префикс sp- не несет в себе никакого сакрального смысла, это всего лишь первые буквы моей фамилии и имени и добавлен не с целью прославить меня, а по причине того, что имена «load», «loader» и прочие были уже заняты.

Итак, вы сделали npm i sp-load -S в своем проекте. Предположим, что вы имеете следующее содержимое package.json файла:

{
    "name": "your-project",
    "version": "1.0.0",
    "main": "index.js",
    "dependencies": {
        "lodash": "^3.10.1",
        "sp-load": "^1.0.0"
    },
    "devDependencies": {
        "gulp": "^3.9.0",
        "webpack": "^1.12.9"
    },
    "_localDependencies": {
        "core": "./core/core",
        "some-module": "./deep/deep/deep/deep/deep/deep/deep/some-module"
    }
}

И имеете следующую структуру файлов:

your-project/
    node_modules
        sp-load/
            ...
        gulp/
            ...
        lodash/
            ...
        webpack/
            ...
    package.json
    core/
        core.js
    deep/
        deep/
            deep/
                deep/
                    deep/
                        deep/
                            deep/
                                some-module.js
    gulpfile.js
    index.js

Что вам нужно сделать, чтобы использовать sp-load в простейшем виде? Всего одну вещь, сделать var $ = require('sp-load'); внутри какого-либо файла, например, вот содержимое gulpfile.js:

'use strict';

var $ = require('sp-load'),
    webpackConfig = {};

$.gulp.task("webpack", function (callback) {
  $.webpack(webpackConfig, function (err, stats) {
    callback();
  });
});

Содержимое some-module.js:

'use strict';

function someModuleFuction() {
    console.log('I\'m some module function call!');
}

module.exports = someModuleFuction;

Содержимое core.js:

'use strict';

function coreModuleFuction() {
    console.log('I\'m core module function call!');
}

module.exports = coreModuleFuction;

Содержимое index.js:

'use strict';

var $ = require('sp-load');

$.someModule();

$.core();

Как вы видите, всё что нужно сделать — подключить sp-load модуль. Он возвращает объект, содержащий список модулей, которые будут подгружены по требованию, то есть модуль будет загружен node-ой при первом обращение по имени модуля, например, $.core().

Также вы, наверное, заметили нестандартное свойство "_localDependencies" в package.json. В этом свойстве вы можете определить список локальных модулей вашего проекта. Ключи объекта — названиям модулей, значения — относительный путь к файлу модуля(путь относительный package.json файла).

Если же вы хотите обращаться к модулям не как к свойствам объекта, а как к переменным, то можете сделать это следующим образом(в примере используется es6 деструктуризация. как включить возможности es6 в nodejs вы можете прочесть в документацие nodejs):

'use strict';

var {someModule, core} = require('sp-load');

someModule();

core();

Или с использованием es5:

'use strict';

var $ = require('sp-load'),
    someModule = $.someModule,
    core = $.core;

someModule();

core();

В обоих этих примерах, модули someModule и core буду подгружены при присвоение, если же вы хотите, чтобы они были подгружены в момент первого их использования(то есть on demand), то обращайтесь к модулям, как к свойствам объекта $.

Это было простейшее использование sp-load, без каких-либо конфигураций, за исключением использования свойства "_localDependencies" в package.json. Теперь же хочу показать какие настройки поддерживает sp-load. Для того, чтобы конфигурировать sp-load, необходимо добавить свойство "_sp-load" в package.json. Ниже приведен пример package.json файла, в котором указаны все возможные настройки с комментариями о назначение каждой из них:

{
    "name": "your-project",
    "version": "1.0.0",
    "main": "index.js",
    "dependencies": {
        "lodash": "^3.10.1",
        "sp-load": "^1.0.0"
    },
    "devDependencies": {
        "gulp": "^3.9.0",
        "webpack": "^1.12.9"
    },
    "_localDependencies": {
        "core": "./core/core",
        "some-module": "./deep/deep/deep/deep/deep/deep/deep/some-module"
    },
    "_sp-load": {
        /*
			если значение true, имена модулей будут в виде camel case.
			например, вместо $['some-module'] будет $.someModule.
			дефолтное значение - true.
        */
        "camelizing": false,
        /*
			эта настройка отвечает за переименование имен модулей. например, вместо $.lodash модуль будет доступен
			как $._
        */
        "renaming": {
            "lodash": "_",
            "gulp": "supergulp"
        },
        /*
			если вы хотите заменить часть названия модулей, используйте эту настройку. ключи - регулярные выражения,
			значения - строки, на которые будет произведена замена. наиболее частый случай использования - gulp 
			плагины, большинство из которых начинаются с префикса gulp-, например, gulp-concat, а вы хотите обращаться
			к нему как $.concat вместо $.gulpConcat.
        */
        "replacing": {
            "/^gulp-/": ""
        }
    }
}

Если же вы не хотите засорять package.json файл, то поместите настройки sp-load и список локальных модулей в файл _sp-load.json, который должен находиться в той же папке, где и package.json, то есть:

yourProject/
    package.json
    _sp-load.json

Вот пример содержимого _sp-load.json файла:

{
    "_localDependencies": {
        "core": "./core/core",
        "some-module": "./deep/deep/deep/deep/deep/deep/deep/some-module"
    },
    "_sp-load": {
        "camelizing": false,
        "renaming": {
            "lodash": "_",
            "gulp": "supergulp"
        },
        "replacing": {
            "/^gulp-/": ""
        }
    }
}

И последнее, о чем еще не упомянул. Когда вы делаете $ = require('sp-load');, объект $ содержит свойство "_spModulesList" в своем прототипе. Это свойство содержит объект, где ключи — имена модулей, а значения — абсолютный путь к файлу модуля. Вот пример содержимого этого объекта:

{
    "lodash": "lodash",
    "sp-load": "sp-load",
    "gulp": "gulp",
    "webpack": "webpack",
    "core": "D://your-project//core//core.js",
    "some-module": "D://your-project//deep//deep//deep//deep//deep//deep//deep//some-module.js"
}

Для чего это может пригодиться? Например, при использование System.js загрузчика.

Пожалуй, это всё. Перед тем, как опубликовать модуль на npmjs.com, протестировал его, но в реальном проекте ещё его не использовал, поэтому, если будут какие-либо ошибки — буду рад, если сообщите о них.

Ссылка на сам модуль: sp-load.

P.S.: Может, кто-нибудь подскажет, как удалить опубликованный модуль из npmjs.com? Нигде не нашел, как это сделать, а npm unpublish удаляет модуль, но при последующем npm publish приходится увеличивать версию модуля т.к. npm ругается, что текущая версия уже зарегистрирована.

Комментарии (17)


  1. k12th
    13.01.2016 13:08
    +4

    Нельзя выкатить новую версию модуля под видом старой — мало ли что вы там поломали в новой. Поэтому npm publish требует увеличивать версию.


    1. pavel06081991
      13.01.2016 13:54
      +1

      Благодарю за ответ.


  1. chelaxe
    13.01.2016 14:30

    Наверное Вы это читали, но все же оставлю ссылку на эту статью.


    1. pavel06081991
      13.01.2016 14:36
      +1

      Да, читал. Основа того, что описано в этой статье взята из gulp-load-plugins, тонее сам алгоритм. Тот же алгоритм. В своем модуле я также заюзал этот подход, но есть несколько дополнительных особенностей в моем модуле:
      1) Добавлена поддержка локальных модулей
      2) Не нужно подключать sp-load и производить какие-либо инициализации, вызов require('sp-load') возвращает уже сформированный объект, содержащий модули по требованию
      3) Вся информация о модулях, будь то локальных или внешних хранится в package.json-е проекта.
      и т.д.


    1. pavel06081991
      13.01.2016 14:41
      +1

      И что самое важное — я не нашел такого решения на npmjs.com, посему оформил все это в свой модуль. Не претендую на авторство различных алгоритмов, просто сделал и опубликовал.


      1. vintage
        13.01.2016 17:36
        +1

        Его сложно найти :-) github.com/nin-jin/node-jin#autoloader


  1. zxcabs
    13.01.2016 17:15
    +2

    Я три раза перечитал статью, но так и не понял какую проблему решает автор.

    1) Много require в начале файлов -> создайте отдельный файл который будет возвращать объект с уже подключенными модулями и используйте его везде.
    2) В чем смысл подключения модулей в nodejs только тогда когда они нужны? Экономии по памяти не выйдет, а лишняя логика добавляется, да и в некоторых ситуациях это может вызвать тормоза для первых обращений к таким методам.
    3) Чем не устраивает стандартный метод «резолва» модулей через NODE_PATH?


    1. AndyGrom
      13.01.2016 17:48
      +2

      Здесь ещё вот какой момент. Вызов require — синхронный. Так как он отложен и будет вызван неизвестно когда, скорее всего его вызов попадётся в асинхронном коде. Что доставляет следующие неприятности:
      1. Ожидание файлового ввода-вывода, что порочит идею асинхронного кода.
      2. exception в этом коде уронит ноду (приятной отладки).

      Вызов синхронного кода во время работы сервиса (не консольной утилиты) — это всегда зло. Поэтому загружаем все модули на старте и если что-то не то, получаем просто не загрузившееся приложение, что быстро диагностируется и лечится.

      В общем не дай бог я увижу проект с таким вот подходом…


      1. vintage
        13.01.2016 18:11

        1. При использовании fibers можно и асинхронно грузить файлы. С другой стороны, синхронная загрузка файлов быстрее асинхронной.
        2. Exception в любом коде уронит ноду, если не завернуть его в try-catch или domain.
        3. А что там может быть не то? Любой нормальный модуль при загрузке ничего не делает — просто предоставляет некоторый объект, так что там и падать-то особо не чему. Разве что модуля может не оказаться, но это лишь малая доля ошибок. Куда большая часть ошибок в том, что модуль неправильно функционирует, а чтобы это понять нужно прогнать тесты, а не просто загрузить его.


        1. AndyGrom
          13.01.2016 19:27

          Загружаем мы не просто файлы, а программный код, которые нужно скомпилировать и выполнить, что может потянуть ещё зависимости. Многие модули зависят от других модулей и решают этот вопрос с помощью синхронного require.

          Зачем это?


          1. vintage
            13.01.2016 20:14

            Согласен, профита тут мало получится. Это затем, чтобы не грузить всё подряд, а только то, что реально используется. И чтобы не следить вручную за портянкой require. И чтобы вообще эту копипасту не писать.


      1. pavel06081991
        13.01.2016 21:47

        Никто не запрещает вам подключать модули, как и раньше подгружая их в шапке файла, например, вот так:

        var {
        		lodash,
        		gulp,
        		webpack
        	} = require('sp-load');
        


        И не будет никаких проблем. Полагаю, вы согласитесь со мной, что пример выше, как минимум выглядит посимпотичнее, чем:

        var lodash = require('lodash'),
        	gulp = require('gulp');
        	webpack = require('webpack');
        


        С ростом кол-ва зависимостей в файле это становится заметнее. Согласен, что это мелочь, но все же. И да, es6 деструктуризации
        еще нету в ноде, если запускать ее без каких-либо флагов, но будущее уже совсем рядом.

        Смысл подключения модулей по требованию(lazy load) в том, что, например, если вы работали с gulp-ом, то
        вы знаете, что при запуске gulp-а происходит инициализация различных тасков, например, таска запуска юнит тестов,
        таска сборки проекта, таска минификации js-а, css-а и т.п. и т.д. Так вот, например, вы хотите запустить
        таск минификации css. Для минификации нужен всего один галп плагин — gulp-cssmin(с названием могу ошибаться, но
        не суть), так вот скажите мне зачем для минификации css-а тянуть модули karma для юнит тестов, модули для минификации
        js-а, модули для сборки проекта, модули для чего угодно, но не для css минификации? Не знаете? Вот и я не знаю, зачем.

        Если вы говорите, что подгрузка всех этих модулей не влияет ни на экономию памяти, ни на производительность, то
        скажите, пожалуйста, почему у модуля gulp-load-plugins 15 000 скачиваний за сегодняшний день и сотни модулей,
        которые зависят от модуля gulp-load-plugins? Я не пытаюсь как-то оскорбить вас, но, думаю, что среди тех людей, который
        обеспечили 15 тысяч скачиваний этого модуля за сегодняшний день, есть не глупые ребята и просто так бы использовать
        его не стали?

        Ну и последнее — в моем модуле есть поддержка локальный модулей, это основная «фича», ради которой и задумывался этот модуль.
        Не нужно писать относительные или абсолютные пути к файлам проекта. Определив их в "_localDependencies", затем работаешь с ними,
        как с обычными сторонними модулями из node_modules, то есть обращаешься по имени модуля, например,

        $.myDeepLocalModule().
        

        Опять же, если вам не подходит по какой-либо причине загрузка локальных модулей по требованию, присвойстве их в шапке
        файла:

        
        var {myDeepLocalModule} = require('sp-load');
        


        Если вы прочтете вот эту статью https://gist.github.com/branneman/8048520, то увидите хронологию этой проблемы с локальными модулями,
        она обсуждалась много лет назад и до сих пор обсуждается, в этой статье собраны все текущие «хаки», решающие в той или иной мере
        эту проблему. Мне кажется, что мое решение этой проблемы гораздо удобнее, нежели предложенные в этой статье.


        1. zxcabs
          14.01.2016 00:26

          1) Если у вас один конфиг на все случаи жизни, который работает по разному в зависимости от условий, то вы сами себе создали проблему. Я не знаю какие там проблемы у gulp и зачем он вообще нужен вместе с webpack'ом.
          2) За все время работы я не сталкивался с проблемой написать полный путь к нужному мне файлу, а вот с проблемой таких хитрых загрузчиков сталкивался. С ростом приложения, все эти хитрые загрузчики только осложняют жизнь.
          3) Объясните мне для чего нужны локальные модули?


          1. pavel06081991
            14.01.2016 00:32

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


            1. zxcabs
              14.01.2016 00:40

              Я видел, для меня это не проблема. Во первых это очень редкие случаи, а во вторых имеется нормальная поддержка IDE. В третьих если вы не пишите модуль, то проблему решает NODE_PATH.


              1. vintage
                14.01.2016 09:29

                Это очень частые случаи, когда вы пытаетесь хорошо структурировать своё приложение. Перенесли скрипт — извольте поправить все относительные ссылки.

                Собственно по ссылке:

                4. The Environment
                Setting application-specific settings as environment variables globally or in your current shell is an anti-pattern if you ask me. E.g. it's not very handy for development machines which need to run multiple applications.

                If you're adding it only for the currently executing program, you're going to have to specify it each time you run your app. Your start-app command is not easy anymore, which also sucks.


  1. SDSWanderer
    13.01.2016 17:36

    Похоже что автор придумал сам себе проблему, и героически ее решил)