YM Modular System. Why?

Published on 2/25/2014

The recently published step-by-step tutorial on i-bem.js mentioned YM modular system as a base for component JavaScript solution behind BEM. Why do we need another modular system? Let us see...

The author of YM modules, Dmitry Filatov recently came up with an article about YM modules in Russian. And below you can find the translation.


So, one more modular system? Besides CommonJS and AMD? Why should we care?

I will not write why modules and modular systems are needed, there are plenty of articles about it. Let us rather proceed to the main question: why do we need another modular system?
For sure, there are CommonJS and AMD, but working on large projects with them I faced large drawbacks. One is that they are synchronous. This is not fatal, but in my project we often had to provie different hacks for it.

Let us say, we have 3 modules: moduleA, moduleB and moduleC. moduleC depends on both moduleA and moduleB. Initially I will describe this in code for all the three solutions:

####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';

Linking and usage:

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';
});

Linking and usage:

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');
});

Linking and usage:

modules.require(['С'], function(moduleC) {
    console.log(moduleC); // prints "ABC"
});

Nothing interesting yet. All three examples are similar, although you may notice the provide callback in the YM example. What is it for?

Let us imagine a case that moduleA and moduleB cannot be resolved immediately (synchronously, as it is required by CommonJS and AMD). Sometimes you need to do an asynchronous action first. The simpliest example can be setTimeout. There is no way to implement it elegantly with CommonJS and AMD. But with YM it can be coded as follows:

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');
});

Interestingly moduleC does not know anything about asynchronous actions in its dependant modules. Win!

Real life example

As for real file example, I often use the YandexMaps API (http://api.yandex.com/maps/, API of Yandex.Maps public service). Yandex.Maps API has a complex loading scheme and this cannot be done synchronously. This means that I cannot simply link it to a page <script type="text/javascript" src="url-of-ymaps.js"></script> and be sure that all the following scripts will get the API code ready. First I need to wait for the event ymaps.ready to fire.

The project I am working for is quite complex; we have many classes inherited from the basic API. For example, we have a ComplexLayer class based on ymaps.Layer. With YM modules it is simple to implement. We define a ymaps module which loads the API code, waits for the ymaps.ready event and then provides itself. All the modules which have the ymaps module as a dependency only start to resolve after this. As you can see, other modules know nothing about the asynchronicity of the Yandex.Map API. No hacks in code!

ymaps.js:

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

    var url = config.hosts.ymaps + '/2.1.4/?lang=ru-RU' +
              '&load=package.full&coordorder=longlat';

    loader(url, function() {
        ymaps.ready(function() {
            provide(ymaps);
        });
    });
});

There are 2 other modules in use here: loader and config. I do not show their code, but the first one loads scripts and the second one is a hash with constant values.

ComplexLayer.js:

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

    provide(ComplexLayer);
});

We can do the same if jQuery is needed. There is a module to load jQuery:

modules.define(
    'jquery',
    ['loader',
    function(provide, loader) {

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

Then we make other modules dependent on jquery module.

Thus, the whole project code is represented with modules. There is no global, no need for agreement on the order of linking the scripts (including third-party ones), no dirty hacks for asynchronicity.

And to wrap up, let me demonstrate you the YM modular system API (indeed, it has more methods, and these are only the basic ones).

Defining a module:

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

Requiring a module:

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

The project is open source and hosted at GitHub: github.com/ymaps/modules.