Для приложения идеально подошел AMD — указываешь в зависимостях библиотеки, добавляешь связующий код, логику… и приложение готово. Но при разработке библиотеки я столкнулся с проблемой управления внутренними зависимостями при помощи AMD или CommonJS — получается слишком много обвязок (boilerplate), особенно когда части библиотеки взаимозависимы. Поэтому я выделил еще один подход к определению модулей в JS — YAMD.
Внимание! Это не замена AMD или CommonJS, для сборки приложения я по прежнему использую AMD, просто одна из библиотек, которую я подключаю, собрана с помощью YAMD. Таким образом, YAMD является подходом к декомпозиции сложной библиотеки без внешних зависимостей на части и отдельные файлы, и инструментом для сборки этих файлов воедино.
В статье я опишу подход. От вас хочется узнать в комментариях, что вы используете для тех же задач.
YAMD
Еще один подход для определения модулей для JavaScript. В отличие от CommonJS и AMD:
- позволяет писать меньше обвязок (boilerplate code)
- хорошо поддерживает взаимно рекурсивные модули
- повторяет (насколько это возможно) модульность Java/C#
- создан для создания библиотек, а не компоновки приложений
YAMD — подход к созданию библиотеки через декомпозицию её функциональности на отдельные файлы и последующую сборку этих файлов воедино. Файл на выходе может быть оформлен как IIFE, вводящий в глобальную область видимости одно имя (название библиотеки), так в виде CommonJS или AMD обертки.
Я придерживался принципа, что использование YAMD при разработке библиотеки не должно накладывать ограничений или обязательств на пользователя библиотеки.
Применять YAMD имеет смысл когда:
- вы создаете относительно сложную библиотеку
- у библиотеки нет внешних зависимостей
Мне понять что-то проще, когда проводится аналогия с тем, что я уже знаю, поэтому описание YAMD будет дано через кросс сравнение с AMD и CommonJS.
Сравнение YAMD, AMD и CommonJS
Сравним на простом примере YAMD, AMD и CommonJS. Представим, что мы пишем математическую библиотеку.
Выделим функции библиотеки в отдельные файлы, таким образом, каталог с исходниками для всех подходов будет одинаков:
> find . ./math ./math/multiply.js ./math/add.js
Посмотрим на исходники:
AMD | CommonJS | YAMD |
./math/add.js | ||
|
|
|
./math/multiply.js | ||
|
|
|
USAGE (assuming all dependencies are included) | ||
|
|
|
AMD подход получился довольно многословным, CommonJS уменьшает кол-во обвязок за счет неявного оборачивания каждого файла в функцию, а YAMD делает еще один шаг вперед — вводит корень библиотеки root
, через который можно обращаться к любой её части без явного импорта.
Работа с YAMD
Для сборки библиотеки оформленной в YAMD стиле нужно запустить python yamd.py path/to/library
— в результате, в текущем каталоге появится файл nameOfTheLibrary.js
. Имя библиотеки задается именем каталога с исходниками, кроме того, это имя используется для добавления библиотеки в глобальную область видимости (если, конечно, не указана сборка в CommonJS или AMD модуль).
Имя каталога, а так же имена всех подкаталогов и js-файлов (до ".js") должны быть валидны с точки зрения ограничений для имен переменных в JS.
Иерархия каталогов задает иерархию модулей, а js-файлы наполняют эти модули функциями (конструкторами) — получается что-то типа пакетов и классов в Java или пространств имен и классов в C#.
Для того, чтобы добавить функцию в модуль нужно создать в каталоге соответствующем этому модулю js-файл (имя файла до .js задает имя функции), определить в нем функцию с любым именем, например add
, и в начале файла вызвать `expose` передав ей функцию, например, `expose(add);`. Весь остальной контент файла будет приватным и виден только экспортируемой функции.
Может показаться странным, что функция используется до объявления — expose(add);
, но это не магия YAMD, а легальное поведение для JS — hoisting. Но тем не менее, есть требование к тому, чтобы expose
шла в начале файла, встречалась только один раз и до её вызова не было ни одного обращения к root
.
Предыдущий пример (математическая библиотека) после сборки будет примерно эквивалентен следующему коду:
var math = (function(){ var root = { add: function(a, b) { return a + b; }, multiply: function(a,b) { var result = 0; for (var i=0;i<a;i++) { result = root.add(result, b); } return result; } }; return root; })();
Допустим, мы решили усложнить нашу библиотеку, и добавить в неё распределения из теорвера. Логично их поместить в отдельный модуль (каталог), после изменений каталог с исходниками выглядит следующем образом:
> find . ./math ./math/multiply.js ./math/add.js ./math/distributions ./math/distributions/normal.js ./math/distributions/bernoulli.js
Тогда после сборки мы получим примерно следующий код
var math = (function(){ var root = { add: function(a, b) { return a + b; }, multiply: function(a,b) { var result = 0; for (var i=0;i<a;i++) { result = root.add(result, b); } return result; }, distributions: { normal: function() { throw new Error("TODO"); }, bernoulli: function() { throw new Error("TODO"); } } }; return root; })();
Вернемся к `expose`, помимо функции, она конечно же может экспортировать в модуль строки, числа или объекты. Получается, что мы можем переписать предыдущий пример, поместив все распределения в один файл в корне библиотеки, а не создавая отдельный каталог:
// FILE ./math/distributions.js expose({normal: normal, bernoulli: bernoulli}); function normal() { throw new Error("TODO"); } function bernoulli() { throw new Error("TODO"); }
После сборки библиотеки будут полностью эквивалентны.
Взаимно рекурсивные модули
В YAMD возможно добавить в модуль функцию, которая использует функцию другого модуля, а та первую. Впрочем в случае CommonJS и AMD это тоже возможно, разница только в кол-ве кода. Для примера напишем функцию, вычисляющую кол-во шагов в процессе Коллатца. Как и в первом примере структура каталога не будет меняться в случае AMD, CommonJS и YAMD:
> find math/collatz math/collatz math/collatz/steps.js math/collatz/inc.js math/collatz/dec.js
А теперь код:
AMD | CommonJS | YAMD |
./math/collatz/steps.js | ||
|
|
|
./math/collatz/inc.js | ||
|
|
|
./math/collatz/dec.js | ||
|
|
|
Взаимные зависимости в случае AMD описывались согласно этому документу — нам пришлось добавить зависимость от require и использовать её для явного импорта зависимостей внутри функций.
С этим примером справились все три подхода, но он относительно простой — рекурсивная природа вылезает только при пользовательском вызове функций библиотеки, а к этому времени библиотека уже загружена. Проблема с взаимной рекурсией возникает, если при инициализации библиотеки нужно использовать функции самой библиотеки. Эта проблема хорошо разобрана в сообщении Тома.
Для борьбы с ней в YAMD была добавлена отложенная инициализация: в expose
вторым аргументом можно передать функцию (конструктор модуля), для которой гарантируется, что она будет вызвана после того, как все модули загрузятся.
Вернемся к нашему примеру, допустим мы решили ускорить работу steps
и для некоторых n
вычислить число шагов при загрузке библиотеки. Пусть у нас определен декоратор tableLookup
.
function tableLookup(table, f) { return function(n) { if (n in table) return table[n]; return f(n); } }
Тогда нам достаточно изменить файл steps.js в YAMD подходе следующем образом:
// FILE ./math/collatz/steps.js var table = {}; expose(tableLookup(table, steps), ctor); function ctor() { table[3] = steps(3); } function steps(n) { if (n==1) return 0; if (n%2==0) return root.collatz.dec(n); if (n%2==1) return root.collatz.inc(n); }
При использовании CommonJS/AMD у нас есть два способа реализовать тоже самое:
- добавить в библиотеку явной метод инициализации
- отложить инициализацию table до первого вызова
Получается плохо — в CommonJS для решения этой задачи мы должны либо поменять API, либо нарушить single responsibility principle и добавить в steps контроль ленивости:
// FILE ./math/collatz/steps.js var table = {}; var inited = false; function ctor() { table[3] = steps(3); } function steps(n) { if (!inited) { ctor(); inited = true } if (n==1) return 0; if (n%2==0) return require('./dec')(n); if (n%2==1) return require('./inc')(n); } module.exports = tableLookup(table, steps)
Если закрыть глаза на нарушение SRP в CommonJS и рассматривать тяжелые процессы инициализации, то оба варианта плохи тормозами, в случае YAMD, тормозами при подключении библиотеки, а в случае CommonJS, тормозами при первом вызове steps
. Но инициализация не всегда тяжелая, а если она все таки такая, то используя YAMD можно попытаться её вынести в процессы WebWorker’ов запускаемых из ctor и надеяться, что к первому запуску `steps` она уже закончится. Использовать CommonJS так же мы не можем, так как шанс запустить инициализацию у нас получится только при первом запросе, следовательно этот запрос не успеет предсчитаться и гарантированно будет подтормаживать.
Спасибо за внимание. И не забудте написать в комментариях, как вы разрабатываете сложные JS библиотеки.
ссылка на оригинал статьи http://habrahabr.ru/post/190700/
Добавить комментарий