In January 2009 Kevin Dangoor started the ServerJS project, later that year it was renamed to CommonJS. The goal of the project was to create an ecosystem to enable developers to write javascript code to run outside of the browser and, at the time of writing, Wikipedia lists 15 implementations including node.js, CouchDB and XULJet and 4 proposals have become specifications: modules, packages, system and promises (although the latter does not seem to be reflected on the CommonJS site). Today we’ll look at the module specification and how we can get it to load modules asynchronously.
The specification
The specification is at version 1.1.1 right now and the general theory of operation is pretty straight forward. It defines a single free function called require which is responsible for loading and managing any dependencies outside of your current .js file.
var add = require('math').add
var increment = function(val) {
return add(val, 1);
};
In the above example we require the math library and then use the add method in our increment function. The math library itself defined the add function by “exporting” it. This is done by adding it as a property of an exports object that is created for it by the module loader:
exports.add = function(a, b) {
return a+b;
};
It’s the job of require to load the math.js file and provide it with the exports object which it [math.js] can then populate. This all happens synchronously when require is called which means that implementations without the ability to directly load files are unable to use this version of the specification.
There’s been a lot of talk in the CommonJS community about how to tackle just this issue and they’ve come up with the transport addition to the spec. It’s still in the proposal phase right now and has 4 sub proposals each of which has various levels of support. Transport proposal D seems to include many of the features of the other proposals and also seems [in my view] to be garnering the most acceptance, or at least the most discussion.
The Transport Proposal
The basic theory of the transport proposal is that, instead of a file that just starts populating the exports object, we defer calling the body of the module until later. We do this by calling a require method which takes care of resolving dependencies and initialising the module.
Lets look at an example, the code below defines the math module and the add method:
require.define({
"math": function (require, exports, module) {
exports.add = function(a, b) {
return a+b;
}
}
}, []);
So now in our module instead of starting straight away with our exports we make a call to require.define which sets up the definition of our module. The definition contains a set of the modules defined in the file and the factory methods needed to instantiate them. When we then call require(‘math’) later in our code the factory function is call and the exports object is returned.
The last argument of require.define is a list of dependencies for the modules defined in the preceding definition, before any call to require can be made the dependencies listed in the final argument must have been loaded. If not then require will throw an exception.
The main advantage of the above method for defining our modules is the ability to know all the dependencies of a module upfront. Additionally we have the ability to delay initialising any module until it’s at which point we should have loaded it’s dependencies. If you put these two together then you have the ability to asynchronously load your modules.
Asynchronous Loading
The proposal leaves the nuts and bolts of module loading up to the implementation so from here on we’re going to go “off-road” and define an additional set of methods that we’ll use to load our modules. To make things neat we’ll put all these new methods into an object called require.resolve so the chance of colliding with changes to the spec is minimal.
First up is require.resolve.next, this will check the dependency list and return the first unloaded item in the dependency list. Until the module becomes defined, calling the method again will return the same value. This will be useful for checking to see if a load was successful as we can check to see if the next module has changed after the load.
Next we’ll add a method called require.resolve.find(id) which, given a module id, will return the URI we need to use in our script tag. Additionally we’ll need a require.resolve.path(prefix, path) which will allow us to match top level module names to specific paths, this will become useful if a module is located somewhere other that the current host or we want to change the base path to load from.
Finally we need some way to tell require what modules to start with (bootstrapping). For this we’ll use require.resolve.load(id) which will push a module onto the dependency stack.
Using the new methods we can poll the dependency stack and load modules. Loading new modules should update the dependency stack and we can keep going till all modules have been loaded. Below is some sample code for how you might use these new methods to implement a loader/resolver:
function loadScript(uri, callbacks) {
// Put your loader here
}
function resolve(loader) {
var next = require.resolve.next());
var uri = require.resolve.find(next);
loader(uri, {
onSuccess: function() {
if (next === require.resolve.next()) {
// The module did not load properly
// handle the error
} else {
// The module loaded successfully
resolve(loader);
}
},
onError: function() {
// handle error
}
});
}
// Setup the paths
require.resolve.path('', '/js');
require.resolve.path('thirdPartyModule', 'http://path.to.third.party/module');
// Initialise the dependency stack
require.resolve.load('myModule');
// load the module(s)
resolve(loadScript);
Look out for my next post which will show you how to implement the above code in VXML.
Tags: CommonJS, ECMAScript, javascript