JavaScript Asynchronous Dependency Loader
A substantial web application doesn’t need to wait until all the required JavaScript libraries loaded. Usually most of them can load asynchronously and start acting whenever they are ready. Most commonly used approach here would be AMD. That’s a sophisticated and time-proved solution. However to use it with libraries, you must have them converted to modules. I don’t appreciate the idea to interfere with 3-rd party library code. At the same time I still need non-blocking loading and dependency being resolved in my code. Thus, I worked out a very simple, but yet working solution.
Non-AMD libraries usually expose public API by providing accessor(s) in global scope. So, to resolve this kind of dependency we can find the exposed API in global scope as soon as the dependency script is loaded. Therefore we just need a handler subscribed for the event, which is being fired when the dependency script is loaded. It’ll serve, though polluting the global scope isn’t a good practice to adopt. On the contrary, while writing our own code, we can use CommonJS-like module pattern:
(function( exports ){
exports.moduleA = { /*..*/ };
exports.moduleN = { /*..*/ };
}( module ));
console.log ( typeof module.moduleA );
Keeping this in mind, I came up with the following API:
// Load /js/form.js asynchronously and make it a dependency named as form
rjs.define("/js/form.js", "form");
// Call the handler when both events DOMContentLoaded and form dependency loaded are fired
rjs.require(["DOMContentLoaded", "form-loaded"], function( module ){
});
Here, unlike AMD, rjs.require refers not to dependencies, but to the involved events. Thus we imply that we want the handler to invoke when all the supplied events fired, provided that dependency onLoad events named as -loaded. It makes more sense since, in fact, we are interested in the events. Besides, we can refer not only to dependency loaded events, but to others such as DOMContentLoaded.
Next goes the implementation in VanillaJs (JavaScript 1.6):
var module = module || {},
rjs = (function( global, exports ){
"use strict";
var document = global.document,
queue = {},
/**
* Firing event helper
*
* @param {string} eventName
* @param {array} payload
*/
triggerEvent = function( eventName, payload ) {
var e = document.createEvent("Event");
e.initEvent( eventName, true, true );
e.payload = payload;
document.dispatchEvent( e );
},
/**
* Subscribe helper to an event helper
*
* @param {string} eventName
* @param {function} fn
*/
onEvent = function( eventName, fn ) {
document.addEventListener( eventName, function( e ){
fn.apply( null, e.payload || [] );
}, false );
},
eventQueue = (function(){
return new function() {
var queue = [];
return {
/**
* Register a supplied event to the queue
*
* @param {string} eventName
*/
register: function( eventName ) {
queue.indexOf( eventName ) === -1 %26%26 queue.push( eventName );
},
/**
* Checks if all the events of a supplied array already registered
* in the queue
*
* @param {array} events
*/
isResolved: function( events ) {
var i = 0, len = events.length;
if (!len) {
return false;
}
for ( ; i < len; i++ ) {
// If at least one event of the supplied list is not
// yet fired, the queue is not resolved
if ( queue.indexOf( events[ i ] ) === -1 ) {
return false;
}
}
return true;
}
}
};
}());
return (function(){
// Register DOMContentLoaded event
onEvent( "DOMContentLoaded", function(){
eventQueue.register("DOMContentLoaded");
});
return {
/**
* Load a given script asynchronously and fires event <dependency>-load
* when script is loaded and both DOM are ready and script is loaded
*
* @param {string} file - dependency script path
* @param {string} dependency - dependency name
* @param {function) completeFn OPTIONAL - A callback function
* that is executed when the request completes.
*/
define: function( file, dependency, completeFn ) {
var script = document.createElement("script");
script.type = "text/javascript";
script.src = file;
document.body.appendChild( script );
script.addEventListener( "load", function(){
var eventName = dependency + "-loaded";
// Registers event in the queue
eventQueue.register( eventName );
// Fires event when the script is loaded
triggerEvent( eventName, [ exports ] );
completeFn %26%26 completeFn();
}, false );
},
/**
* Call the function fn when all supplied events fired
*
* @param {array} dependencies
* @param {function} fn - callback function that is executed
* when all the supplied dependencies resolved
*/
require: function( events, fn ) {
// Event fired before a handler subscribed
if ( eventQueue.isResolved( events ) ) {
return fn( module );
}
// Event fired after a handler subscribed
events.forEach(function( eventName ){
onEvent( eventName, function( module ){
eventQueue.isResolved( events ) %26%26 fn( module );
});
});
}
};
}());
}( this, module ));
When using JQuery the code looks a bit more succinct as the library provides event-handling helpers. Moreover, it is supposed to be supported by legacy browsers. Here the port:
var module = module || {},
rjs = (function( global, exports ){
"use strict";
var document = global.document,
$ = global.jQuery,
queue = {},
eventQueue = (function(){
return new function() {
var queue = [];
return {
/**
* Register a supplied event to the queue
*
* @param {string} eventName
*/
register: function( eventName ) {
$.inArray( eventName, queue ) === -1 %26%26 queue.push( eventName );
},
/**
* Checks if all the events of a supplied array already registered
* in the queue
*
* @param {array} events
*/
isResolved: function( events ) {
var i = 0, len = events.length;
if (!len) {
return false;
}
for ( ; i < len; i++ ) {
// If at least one event of the supplied list is not
// yet fired, the queue is not resolved
if ( $.inArray( events[ i ], queue ) === -1 ) {
return false;
}
}
return true;
}
}
};
}());
return (function(){
// Register DOMContentLoaded event
$( document ).on( "DOMContentLoaded", function( ){
eventQueue.register("DOMContentLoaded");
});
return {
/**
* Load a given script asynchronously and fires event <dependency>-load
* when script is loaded and both DOM are ready and script is loaded
*
* @param {string} file - dependency script path
* @param {string} dependency - dependency name
* @param {function) completeFn OPTIONAL - A callback function
* that is executed when the request completes.
*/
define: function( file, dependency, completeFn ) {
var script = document.createElement("script");
script.type = "text/javascript";
script.src = file;
document.body.appendChild( script );
$( script ).on( "load", function(){
var eventName = dependency + "-loaded";
eventQueue.register( eventName );
// Fires event when the script is loaded
$( document ).trigger( eventName, [ exports ] );
completeFn %26%26 completeFn();
});
},
/**
* Call the function fn when all supplied events fired
*
* @param {array} dependencies
* @param {function} fn - callback function that is executed
* when all the supplied dependencies resolved
*/
require: function( events, fn ) {
// Event fired before a handler subscribed
if ( eventQueue.isResolved( events ) ) {
return fn( module );
}
// Event fired after a handler subscribed
events.forEach(function( eventName ){
$( document ).on( eventName, function( e, module ){
eventQueue.isResolved( events ) %26%26 fn( module );
});
});
}
};
}());
}( window, module ));
The library is released under MIT license at https://github.com/dsheiko/micro-requirejs