In last post Implements a deferred object in Javascript, we implemented a deferred object, this tool can be used in loading dependencies for a Javascript file.

A Javascript file must wait for all its dependencies loaded and then initialize itself.

In modern Javascript application design, most Javascript files will be loaded dynamically in an on demand way. This not only can speedup the page loading and also lower the page size, since a page only load the Javacript that its need.

Usually, there will be a bootstrap or stub Javascript file. This file will define necessary tools to load other js files. For example, create script element and set its src attribute then append to head element.

Modules storage

We call each Javascript file a module, we need a storage to record information about these modules, like the uri path of the file, whether the file is loaded and ready, and also the deferred object associate with the module.

 
var APP = window.APP || {};
APP.modules = {};
 
APP.register = window.register = function( mod ) {
        if( APP.modules[mod] ) return APP.modules[mod];
 
        APP.modules[mod] = {
            dependencies : [],
            include : modInclude(mod),
            deferred : new APP.Deferred(),
            status : statusConstants.REGISTERED,
            done : modDone(mod),
            uri: moduleUris[mod],
            imports: modImports(mod),
            exports: modExports(mod),
            exports_: {},
            depReady: modDepReady(mod),
            setExport: modSetExport(mod)
        };
 
        return APP.modules[mod];
    }
 

The register method is used to add new module to modules storage and return the object if it is already registered.

Lets first take a look at what a module will look like.

 
(function(){
  var mod = register('testmodule');
  mod.include('jquery');
 
  mod.depReady(function(){
    var jquery = mod.imports('jquery');
    console.log('jquery version is ' + jquery.fn.jquery );
 
    mod.setExport('hello', function(msg) {
       console.log('hello world');
    });
  });
})();
 

Define dependencies

A module can declare its dependencies with include method of the module object.

 
function modInclude( mod ) {
 
        return function () {
            var modObj = register( mod );
            var moduleList = Array.prototype.slice.call(arguments);
            APP.each(moduleList, function(value, index){
                modObj.dependencies.push(value);
 
            });
 
            loadDependencies(mod);
        }
    }
 

It simply push the module name to the dependencies property of the module object.

And then start to load its dependencies, the loading process is asynchronous , so there is a depReady method which accept a call back function when the dependencies are all ready.

 
    function loadDependencies( mod ) {
        var modObj = register( mod ) ;
        var todo = [];
        APP.each(modObj.dependencies, function( module ) {
            if( register( module ).status < statusConstants.FETCHING ) {
 
                todo.push(module);
                register( module ).status = statusConstants.FETCHING;
            }
        } );
 
        APP.each(todo, function(module) {
 
            loadFile(module);
        } );
 
 
    }
 
    function modDepReady( mod ) {
        return function (factory) {
            var modObj = register( mod );
            loadDependencies( mod );
 
            var waitLoad = APP.map(modObj.dependencies, function ( module ) {
                return register(module).deferred;
            } ) ;
 
            APP.when.apply({}, waitLoad).done(function(){
                factory();
                modObj.done();
            });
        }
    }
 

The modDepReady using deferred object to wait for all the dependencies load complete.

The loadFile just create script element and set the src attribute and change the status to FETCHED in the callback of onreadystatechange.

The done method just resolve the deferred object.

 
    function modDone ( mod ) {
        return function () {
            var modObj = register( mod );
            modObj.deferred.done();
        }
    }
 
 

Export and import resources between modules

If a module needs to expose some resources like data or function, it can add them to global name space, or add it to its module object.

The export_ property will hold exported resources.

 
    function modImports(mod) {
        return function() {
            var modObj = register( mod );
            if ( arguments.length == 1 ) {
                var exportMod = register(arguments[0])
                return exportMod.exports();
            }
            var  importsList = [];
            if ( arguments.length == 0 ) {
                APP.each(modObj.dependencies, function(value,index) {
                    importsList.push(value);
                });
            }
            if ( arguments.length > 1 ) {
                APP.each(arguments, function(value) {
                    importsList.push(value);
                });
            }
 
            var exports = {};
            APP.each(importsList, function(value) {
                var exportMod = register(value);
                APP.mix(exports, exportMod.exports());
            });
            return exports;
        }
    }
 
    function modSetExport(mod) {
        return function(name,value) {
            var modObj = register( mod );
            modObj.exports_[name] = value;
        }
    }
 
    function modExports(mod) {
        return function() {
            var modObj = register( mod );
            return modObj.exports_;
        }
    }