Pull to refresh

Модульная система YM

Reading time 4 min
Views 11K
Еще одна? Зачем? Есть же CommonJS и AMD? Страждующие могут пройти под кат.


Я не буду вдаваться в то, зачем вообще нужны модули и модульные системы, про это есть статья Путь JavaScript модуля от azproduction, перейдем сразу к главному: зачем еще одна модульная система? Ведь есть уже и CommonJS, и AMD. Но у них обеих есть один недостаток, который для большинства проектов, в которых я участвую, является если ни фатальным, то очень уж неудобным — они, так или иначе, в большей или меньшей степени, являются синхронными. И у нас часто возникали ситуации, когда нужно было придумывать и писать костыли, позволяющие обходить этот недостаток.

Рассмотрим простой пример: у нас есть модули moduleA, moduleB, moduleC, причем последний зависит от двух предыдущих.
Для начала, напишем код, описывающий эти модули, для всех трех модульных систем: CommonJS, AMD, YM.

CommonJS

moduleA.js:
module.exports = 'A';

moduleB.js:
module.exports = 'B';

moduleC.js:
var moduleA = require('A'), 
    moduleB = require('B');

module.exports = moduleA + moduleB + 'C';

Подключение и использование:
var moduleC = require('C');
console.log(moduleC); // prints "ABC"


AMD

moduleA.js:
define('A', function() {
    return 'A';
});

moduleB.js:
define('B', function() {
    return 'B';
});

moduleC.js:
define('С', ['A', 'B'], function(moduleA, moduleB) {
    return moduleA + moduleB + 'C';
});

Подключение и использование:
require(['С'], function(moduleC) {
    console.log(moduleC); // prints "ABC"
});


YM

moduleA.js:
modules.define('A', function(provide) {
    provide('A');
});

moduleB.js:
modules.define('B', function(provide) {
    provide('B');
});

moduleC.js:
modules.define('C', ['A', 'B'], function(provide, moduleA, moduleB) {
    provide(moduleA + moduleB + 'C');
});

Подключение и использование:
modules.require(['С'], function(moduleC) {
    console.log(moduleC); // prints "ABC"
});


Пока ничего интересного. Все три примера эквивалентны. Теперь, обратите внимание на одну деталь в YM-модулях — в callback-функцию декларации модуля передается некая функция provide. Особо непонятно зачем она там, но теперь представим себе ситуацию, когда модули moduleA и moduleB не могут разрезолвиться сразу же, синхронно (как того требуют и CommonJS и AMD), что для этого им нужно выполнить какое-то асинхронное действие. Для упрощения, пусть это будет setTimeout. С помощью YM, предыдущий пример может быть легко переписан следующим образом (что невозможно выразить ни средствами CommonJS, ни AMD, хотя в последней даже в названии присутствует слово Asynchronous, но оно влияет только на способ декларации и способ реквайринга модуля):

moduleA.js:
modules.define('A', function(provide) {
    setTimeout(function() {
        provide('A');
   });
});

moduleB.js:
modules.define('B', function(provide) {
    setTimeout(function() {
        provide('B');
    });
});

moduleC.js:
modules.define('C', ['A', 'B'], function(provide, moduleA, moduleB) {
    provide(moduleA + moduleB + 'C');
});

При этом, заметим, что сам moduleC вообще ничего не знает об асинхронной природе moduleA и moduleB. Profit.

Пример из жизни

От синтетического примера перейдем к реальному. В проектах мы используем API Яндекс.Карт, которое в принципе не умеет грузиться синхронно (внутри оно использует многостадийную загрузку). Это значит примерно то, что невозможно просто написать и надеяться на то, что все последующие мои скрипты уже смогут работать с готовым апи. Для начала работы необходимо дождаться события ymaps.ready. Наш проект достаточно сложный, и мы используем достаточно много наследований от базовых классов из апи. Рассмотрим на примере одного из них. У нас есть наш собственный класс слоя ComplexLayer, который мы хотим отнаследовать от базового слоя из ymaps: ymaps.Layer. С помощью YM это делается просто: мы определяем модуль ymaps, который загружает апи, затем дожидается нужного события (ymaps.ready) и после этого провайдит себя. Все модули, которые зависели от модуля апи (ymaps), начинают свой резолвинг только после этого. Таким образом, наши модули, опять же, ничего не знают об асинхронной природе API Яндекс.Карт. Никаких костылей в коде!

ymaps.js:
modules.define(
    'ymaps',
    ['loader', 'config'],
    function(provide, loader, config) {

    loader(config.hosts.ymaps + '/2.1.4/?lang=ru-RU&load=package.full&coordorder=longlat', function() {
        ymaps.ready(function() {
            provide(ymaps);
        });
    });
});

Код модулей loader и config здесь не приводится: первый умеет загружать скрипты по урлу, второй — просто хэш констант.

ComplexLayer.js:
modules.define('ComplexLayer', ['inherit', 'ymaps'], function(provide, inherit, ymaps) {
    var ComplexLayer = inherit(ymaps.Layer, ...);
    
    provide(ComplexLayer);
});

Точно так же мы поступаем, если нам, например, нужна зависимость от jQuery.
Определяем модуль jquery:
modules.define(
    'jquery',
    ['loader',
    function(provide, loader) {

    loader('//yandex.st/jquery/2.1.0/jquery.min.js', function() {
        provide(jQuery.noConflict(true));
    });
});

И используем зависимость от модуля jquery во всех остальных модулях.

Таким образом, весь код нашего проекта представляет собой только модули, никаких глобальностей, никаких договоренностей о порядке подключения других скриптов (в том числе сторонних) или других модулей, никаких костылей про асинхронность.

В окончание, привожу апи нашей модульной системы YM (на самом деле там несколько больше методов, здесь приводятся только основные):

Объявление модуля

void modules.define(
    String moduleName,
    [String[] dependencies],
    Function(
        Function(Object objectToProvide) provide,
        [Object resolvedDependency, ...],
        [Object previousDeclaration]
    ) declarationFunction
)

Подключение модуля

void modules.require(
    String[] dependencies,
    Function(
        [Object resolvedDependency, ...]
    ) callbackFunction
)


Репозиторий проекта: github.com/ymaps/modules
Tags:
Hubs:
+26
Comments 62
Comments Comments 62

Articles