- PVSM.RU - https://www.pvsm.ru -
В последнее время я стал много писать на JS, сейчас работаю над сложным приложением и довольно крупной библиотекой (~5K SLoC). Конечно же, я столкнулся с проблемой модульности.
Для приложения идеально подошел AMD [1] — указываешь в зависимостях библиотеки, добавляешь связующий код, логику… и приложение готово. Но при разработке библиотеки я столкнулся с проблемой управления внутренними зависимостями при помощи AMD или CommonJS [2] — получается слишком много обвязок (boilerplate), особенно когда части библиотеки взаимозависимы. Поэтому я выделил еще один подход к определению модулей в JS — YAMD [3].
Внимание! Это не замена AMD или CommonJS, для сборки приложения я по прежнему использую AMD, просто одна из библиотек, которую я подключаю, собрана с помощью YAMD. Таким образом, YAMD является подходом к декомпозиции сложной библиотеки без внешних зависимостей на части и отдельные файлы, и инструментом для сборки этих файлов воедино.
В статье я опишу подход. От вас хочется узнать в комментариях, что вы используете для тех же задач.
Еще один подход для определения модулей для JavaScript. В отличие от CommonJS и AMD:
YAMD — подход к созданию библиотеки через декомпозицию её функциональности на отдельные файлы и последующую сборку этих файлов воедино. Файл на выходе может быть оформлен как IIFE, вводящий в глобальную область видимости одно имя (название библиотеки), так в виде CommonJS или AMD обертки.
Я придерживался принципа, что использование YAMD при разработке библиотеки не должно накладывать ограничений или обязательств на пользователя библиотеки.
Применять YAMD имеет смысл когда:
Мне понять что-то проще, когда проводится аналогия с тем, что я уже знаю, поэтому описание 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 стиле нужно запустить 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 [4]. Но тем не менее, есть требование к тому, чтобы 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 это тоже возможно, разница только в кол-ве кода. Для примера напишем функцию, вычисляющую кол-во шагов в процессе Коллатца [5]. Как и в первом примере структура каталога не будет меняться в случае 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 описывались согласно этому документу [6] — нам пришлось добавить зависимость от require и использовать её для явного импорта зависимостей внутри функций.
С этим примером справились все три подхода, но он относительно простой — рекурсивная природа вылезает только при пользовательском вызове функций библиотеки, а к этому времени библиотека уже загружена. Проблема с взаимной рекурсией возникает, если при инициализации библиотеки нужно использовать функции самой библиотеки. Эта проблема хорошо разобрана в сообщении [7] Тома.
Для борьбы с ней в 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 у нас есть два способа реализовать тоже самое:
Получается плохо — в 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 библиотеки.
Автор: shai_xylyd
Источник [8]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/javascript/41338
Ссылки в тексте:
[1] AMD: http://requirejs.org/
[2] CommonJS: http://en.wikipedia.org/wiki/CommonJS
[3] YAMD: https://github.com/rystsov/yamd
[4] hoisting: http://www.adequatelygood.com/JavaScript-Scoping-and-Hoisting.html
[5] процессе Коллатца: http://ru.wikipedia.org/wiki/%D0%93%D0%B8%D0%BF%D0%BE%D1%82%D0%B5%D0%B7%D0%B0_%D0%9A%D0%BE%D0%BB%D0%BB%D0%B0%D1%82%D1%86%D0%B0
[6] этому документу: http://requirejs.org/docs/api.html#circular
[7] сообщении: https://groups.google.com/forum/#!topic/commonjs/b9hbk0QgVGQ
[8] Источник: http://habrahabr.ru/post/190700/
Нажмите здесь для печати.