JavaScript Asynchronous Dependency Loader
Back in 2013, AMD was the standard approach for non-blocking script loading. It works well, but requires every dependency to be wrapped as an AMD module. That’s fine for your own code. For third-party libraries, it means patching external code just to satisfy a module format, which isn’t a trade-off worth making.
So I built a small loader that works around this. Non-AMD libraries expose their API via globals, and all we really care about is knowing when they’ve finished loading. The loader fires a custom event when each script loads, and your code declares which events it needs before running. No module wrapping required.
For your own code, a CommonJS-style pattern keeps the namespace clean:
(function( exports ){
exports.moduleA = { /*..*/ };
exports.moduleN = { /*..*/ };
}( module ));
console.log( typeof module.moduleA );
The API:
// Load /js/form.js asynchronously and register it as a dependency named "form"
rjs.define("/js/form.js", "form");
// Invoke the handler once both DOMContentLoaded and the "form" dependency have fired
rjs.require(["DOMContentLoaded", "form-loaded"], function( module ){
});
Unlike AMD, rjs.require takes event names rather than module identifiers. This makes the intent explicit: you’re declaring what needs to happen before your code runs, not what it imports. Any DOM event can appear in the list alongside dependency-loaded events.
The vanilla JS implementation (ES5):
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 && 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 && 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 ) && fn( module );
});
});
}
};
}());
}( this, module ));
A jQuery port is also available. The library’s event helpers reduce the boilerplate and add legacy browser support:
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 && 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 && 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 ) && fn( module );
});
});
}
};
}());
}( window, module ));
The library is released under MIT license at https://github.com/dsheiko/micro-requirejs