The Three Little Modules and the slick Universal Loader
Modules are normally divided into two parts: i) Authoring which loosely refers to how one can export and import modules and ii) Loading which defines how a module is loaded under each environment (e.g. Node vs Browser). Initially ES6 Harmony included both aspects within the specification but in 2014 the loader specification was removed from the spec. WHATWG, has filled in this gap by creating a loader spec API. But why should we care about this today? - because of SystemJS. SystemJS is a universal module loader which is based on the WHATWG loader spec (through the ES6 Loader polyfill). Using SystemJS in our application relieves us from having to choose between today’s popular module formats (AMD, CommonJS and globals) and allows us to load multiple module formats within the same application. In this post we will create global modules from scratch and basic CommonJS and AMD loaders. In a future post we will see how these module formats can be universally loaded through SystemJS.
#Global Module Format ##Lesson: Functions Create Scope Let us consider the following Javascript code
var a = 1;
{
var a = 3;
console.log(a);
}
console.log(a)
What will the two console.log
functions output? Surprisingly both console.log
output 3. This is due to the fact that Javascript does not have block level scopes. In Javascript only functions can create scope hence to get the desired behaviour we must rewrite the above code as follows:
var a = 1;
(function(){
var a = 3;
console.log(a); // 3
})();
console.log(a) // 1
The take-home message from this interlude is that for modules to have their own scope we must use functions.
##Limiting Global Exposure
Consider creating a module which associates the month-of-the-year
.
var names = ["January", "February", "March", "April",
"May", "June", "July", "August", "September",
"October", "November", "December"];
function monthName(number){
return names[number];
}
console.log(monthName(1)); // February
Defining the function in the following manner would result in the names
variable to leak to the global scope. Can we do any better? Using functions we can define the monthName
function as follows:
var monthName = function(){
var names = ["January", "February", "March", "April",
"May", "June", "July", "August", "September",
"October", "November", "December"];
return function(number){
return names[number];
}
}();
console.log(monthName(2)); // -> March
By defining monthName
in this way, we have create a local variable names
which is only available from within the monthName
function. Can we avoid leaking monthName
to the global scope? We can achieve this by wrapping our code within an anonymous function as follows:
(function(){
var monthName = function(){
var names = ["January", "February", "March", "April",
"May", "June", "July", "August", "September",
"October", "November", "December"];
return function(number){
return names[number];
}
}();
console.log(monthName(3)); // April
})();
##Multiple Exports Given that we want to create an additional function which gives us the reverse mapping i.e. from month name to number, how would we implement this? We cannot return a single function anymore and thus we have to wrap the two functions within an object.
var monthModule = function(){
var names = ["January", "February", "March", "April",
"May", "June", "July", "August", "September",
"October", "November", "December"];
return {
number: function(number){ return names[number]; },
name: function(name){ return names.indexOf(name); }
}
}();
Alternative we can write the same code (semantically) as follows:
function(exports){
var names = ["January", "February", "March", "April",
"May", "June", "July", "August", "September",
"October", "November", "December"];
exports.number = function(number){ return names[number]; }
exports.name = function(name){ return names.indexOf(name); }
}(this.monthModule = {});
What we have just created is the global module format. The main disadvantage of this format is that it claims a single variable from the global scope for each module defined. The advantage is that it is a really simple module format. If you are working on a simple project and you do not import a lot of libraries this could be a great format to use. The limitations of such a module format pop-up when two modules try to claim the same variable, or when we want to load different versions of the same library (say jQuery) side-by-side.
#CommonJS Modules
In the previous module format the main problem was that modules were created by making use of the global scope. Can we create modules without claiming variables from within the global scope? What we require is a require
function () which given a module name will load the module’s file and return the interface value. The require
function can be implemented by creating these two functions:
- A
readFile
function which returns the content of a given file as a string and - A JavaScript function which allows us to evaluate the retrieved code.
The former is usually provided by the underlying environment for example on Node we can make use of the readFileSync function to read the contents of a file.
Evaluating the returned code can be naively implemented using the eval
function. However eval
evaluates the function in the current scope which can cause problems. As an example consider the following:
// File sample.js
var x = 2;
// -------------------
eval(readFileSync("sample.js"));
console.log(x); // 2
What we need is the code within the function to have its own scope. So how do we create a local scope? You guessed it! By creating a function!
function require(name){
var code = new Function("exports", readFile(name + ".js"));
var exports = {};
code(exports);
return exports;
}
Since the new Function
wraps the code, we do not need the code itself to be wrapped up in a function. Hence we can write our previous example as follows:
// monthModule.js
var names = ["January", "February", "March", "April",
"May", "June", "July", "August", "September",
"October", "November", "December"];
exports.number = function(number){ return names[number]; }
exports.name = function(name){ return names.indexOf(name); }
When we use this module pattern we will typically require
the dependencies at the start of our library and use these libraries to provide additional behaviour.
var months = require("monthModule");
var month = months.number("January");
console.log(month); // -> 0
This in essence is how one would implement CommonJS modules and a basic loader to load this module format.
#Asynchronous Module Definition (AMD) There are a couple of issues when CommonJS modules are applied within a browser environment. All these issues stem from the fact that all files are loaded synchronously. In a server environment loading a file from disk will not be the major bottleneck but in a web environment this is surely something we need to reconsider. If such an approach was used, the browser would be made unusable (frozen) while the source files are being retrieved from the remote source degrading the user experience. A better solution would be to load the dependencies in the background (asynchronously) and then make use of a function callback to initialise the module when all the dependencies have loaded. This is in essence what Asynchronous Module Definition (AMD) tries to achieve. Before diving into the details of how we would write an AMD loader let us first address how we would define our modules. To do this lets look into how we would use the previously defined month module.
define(["monthModule"], function(monthModule){
console.log(monthModule.name(0)); // January
});
The define
function takes in an array of module dependencies and an initialisation callback function which will be called when all of the dependencies are resolved. Note that the callback function takes an argument for each dependency. All dependencies are loaded in the background (usually through Promises) allowing the web page to continue processing. When all the dependency promises are resolved the initialisation callback is called with the resolved dependencies as arguments. The modules will also make use of the define
function to define modules. Using this module pattern we can define the monthModule
as follows:
define([], function(){
var names = ["January", "February", "March", "April",
"May", "June", "July", "August", "September",
"October", "November", "December"];
return {
number: function(number){ return names[number]; },
name: function(name){ return names.indexOf(name); }
}
});
Having worked out how we author modules let us now concentrate on writing a simple AMD module loader. To simplify our implementation we assume that we have access to the fetch
function that takes a filename and a function as parameters and calls the function with the response as soon as it has finished loading the file.
In order to keep track of the state of a module e.g. loading vs loaded, we will create a function called getModule
. Such a function will schedule the module to be loaded - if not yet scheduled - and additionally will cache the module definition so that different entities refer to the same object.
var defineCache = Object.create(null);
// We do not need want any prototypes
var currentMod = null;
function getModule(name){
if (name in defineCache)
return defineCache[name];
var module = {
exports: null,
loaded: false,
onLoad: []
}
defineCache[name] = module;
fetch(name).then(function(response) {
var code = response.text();
currentMod = module;
new Function("", code)();
});
return module;
}
Note that the currentModule
variable is used by the define
function to track which module is currently being loaded. Once a module is loaded the define
function will set the exports
variable and also mark the module as loaded
. Additionally the define
function will trigger the onLoad
callback. The define
function can be implemented as follows:
function define(depNames, moduleFunction){
var myMod = currentMod;
var deps = depNames.map(getModule);
deps.forEach(function(mod){
if (!mod.loaded) mod.onLoad.push(whenDepsLoaded);
});
function whenDepsLoaded(){
if (!deps.every(function(m){return m.loaded; })) return;
var args = deps.map(function(m) { return m.exports; });
var exports = moduleFunction.apply(null, args);
if (myMod){
myMod.exports = exports;
myMod.loaded = true;
myMod.onLoad.forEach(function(f){ f(); });
}
}
whenDepsLoaded();
}
The define
function itself makes use of the getModule
function to retrieve the state of all dependent modules. If a dependent module is not yet loaded, we add the current loaded module to the onLoad
callback list of the dependent module. By doing this the current module is notified when the dependent module loads. Note that when we have multiple dependencies we will add whenDepsLoaded
a number of times to the onLoad
callback however only one call will execute the whole function.
This is ensured by the following line of code:
if (!deps.every(function(m){return m.loaded; })) return;
The other whenDepsLoaded
callback functions will return immediately if there are still unloaded dependencies. When all the dependencies have loaded the module will call the moduleFunction
callback with all the dependencies and call the onLoad
function to signal that the current module has now fully initialised
. This will in turn trigger the modules which depend on the loaded module and so on until the whole dependency tree has loaded.
As you can appreciate, asynchronous callbacks create a much more complex loader. One of the popular implementations of the AMD module pattern is RequireJS. This implementation is much more intricate than the one we just created due to the fact that it handles more corner cases and supports browser globals through the shim config.
Summary
In this post we have worked through the three main module definitions. Through SystemJS we are not required to choose between one of these module definitions but instead we can use all three module definitions within the same project. This is what we mean by a universal loader. Hope you guys had fun. Keep on hacking