I have recently spent a lot of time thinking about modularization of javascript for larger projects. Without any form of modularisation code will go haywire very easily. There are a number of schools of thought on how this can be achieved, and have made some conclusions of my own about what the ‘correct’ way to do it is. This article won’t even touch on (a)synchronous module loading, which is an integral part of this but deserves an article of it’s own.
There are many different patterns, but all go after the same result: Ease of code reuse and testing.
So we are creating a library for goat teleportation. You might have some code that does the teleportation, and some that tracks statistics. Also, we must prevent goats teleporting themselves, so we will have some security methods.
So you would end up with some code like this.
//track stats
var stats = {
success: 0,
failure: 0,
partial: 0
}
//udpate stats
function stats_add(how){
stats[how]++;
}
//the teleporter!
function teleport_goat(goat, user){
if(is_user_a_goat(user)){
throw new UserIsAGoatError();
}
//a variable to store our success/failure in
var result;
/* ... teleportation code omitted ... */
//update stats
stats_add(result);
//return based on result.
if(result === 'success'){
return true;
}else if(result === 'partial'){
throw new PartialGoatTeleporationError('messy');
}
return false;
}
//our use access control
function is_user_a_goat(user){
var goat_check = true; //guilty until proven innocent!
/* ... goat detection algorithm omitted ... */
return goat_check;
}
OK, so we have done some bad things there. We have created a bunch of global variables, which can be modified by anyone and any other code. We can resolve this first issue with the simplest form of modularization: wrapping code in an IIFE (Immediately Invoked Function Expression), this forces you to expose only what you want.
(function(global){
//same code as above
//now expose our functionality
global.teleport_goat = teleport_goat;
})(this);
OK, so we have exposed only the minimum we want to , but our function teleport_goat
is
still likely to get overridden by any other implementation of a goat teleporter that you
might have on the same page. So another method is to create an object with our methods and
attach that to a global. That way we have a more library specific name and less chance of
collision, and also the developer could move the functionality to another global variable
afterwards if they chose.
(function(global){
//same code as top
//now expose our function via an object that is in our namespace
global.GoatTeleporter = {
teleport: teleport_goat
};
})(this);
Now as we add functionality we can apply it to our namespace. For example, we may want to expose a function to get stats. We can do this without added any extra globals.
(function(global){
//same code as top
//code to get stats (not by reference!)
function get_stats(){
return {
success: stats.success,
failure: stats.failure,
partial: stats.partial
}
}
//now expose our function via an object that is in our namespace
global.GoatTeleporter = {
teleport: teleport_goat,
stats: get_stats
};
})(this);
Awesome. Now we have some other patterns we can use to extend this. For example, various part of this code can see each other’s bits. Similiar to the global leakage in the very first code. We want to ensure our code is as de-coupled as possible, so we could use a different stats functionality, wihout touching the other code.
So we try the Augmented Module Pattern
. In this pattern we keep the namespace,
but sub-namespace it with each module load. Here we break the stats package into
it’s own definition
//Top Namespace (in global scope)
var GoatTeleporter = {};
//Stats Module Augments the top namespace.
//We pass in the current state of the module, and return our Augmented version
GoatTeleporter = (function(GT){
//track stats
var stats = {
success: 0,
failure: 0,
partial: 0
}
//udpate stats
function stats_add(how){
stats[how]++;
}
//code to get stats (not by reference!)
function get_stats(){
return {
success: stats.success,
failure: stats.failure,
partial: stats.partial
}
}
//export by creating a namespace in GT
GT.Stats = {
add: stats_add,
get: get_stats
};
return GT;
})(GoatTeleporter);
//the teleporter!
GoatTeleporter = (function(GT){
function teleport_goat(goat, user){
if(is_user_a_goat(user)){
throw new UserIsAGoatError();
}
//a variable to store our success/failure in
var result;
/* ... teleportation code omitted ... */
//update stats (using the other module!)
GT.Stats.add(result);
//return based on result.
if(result === 'success'){
return true;
}else if(result === 'partial'){
throw new PartialGoatTeleporationError('messy');
}
return false;
}
//our use access control
function is_user_a_goat(user){
var goat_check = true; //guilty until proven innocent!
/* ... goat detection algorithm omitted ... */
return goat_check;
}
//export the main function only.
GT.Teleport = teleport_goat;
return GT;
})(GoatTeleporter);
After this code executes, GoatTeleporter
will have 2 properties, Stats
and Teleport
.
This pattern is pretty good, we’ve seperated concerns of the 2 modules, allowing replacement
of one, without affecting the other. But there are still some drawbacks. If we wanted a different
Stats package, we couldn’t keep the other one, as they would have the same name GoatTeleporter.Stats
.
More importantly, the Teleporter module has to know the name of the stats module and the name
of the global scope. This makes switching it out more difficult. Finally, the Teleporter Module has
become dependant on the Stats module. It will fail if the Stats module is not present. So we
need a way to manage dependencies.
We are now nearly at AMD, there is just one step in between - which is almost AMD. To get around
the module name knowing requirement, and the global namespace altogether, we define a single global
function define
. This function takes 3 parameters:
Our code changes to this:
//Stats Module
define('GoatTeleporterStats', [ /* no dependencies */ ], function(){
//track stats
var stats = {
success: 0,
failure: 0,
partial: 0
}
//udpate stats
function stats_add(how){
stats[how]++;
}
//code to get stats (not by reference!)
function get_stats(){
return {
success: stats.success,
failure: stats.failure,
partial: stats.partial
}
}
//export by creating a namespace in GT
var Stats = {
add: stats_add,
get: get_stats
};
return Stats;
});
//the teleporter!
define('GoatTeleporter', ['GoatTeleporterStats'], function(Stats){
function teleport_goat(goat, user){
if(is_user_a_goat(user)){
throw new UserIsAGoatError();
}
//a variable to store our success/failure in
var result;
/* ... teleportation code omitted ... */
//update stats (using the other module, passed in as Argument to the `define` function)
Stats.add(result);
//return based on result.
if(result === 'success'){
return true;
}else if(result === 'partial'){
throw new PartialGoatTeleporationError('messy');
}
return false;
}
//our use access control
function is_user_a_goat(user){
var goat_check = true; //guilty until proven innocent!
/* ... goat detection algorithm omitted ... */
return goat_check;
}
//export the main function only.
return teleport_goat;
});
Nothing is exported now, so to actually use the code in a App, we’d need to define
something that had a dependency on the GoatTeleporter
and then inside the factory
the function would be available. e.g. in the main app
define('app', ['GoatTeleporter'], function(teleport){
var goats = new GoatArray(100)
while(goats.length){
teleport(goats.shift());
}
//100 goats teleported. Done.
});
This is getting pretty sweet. But we still need to know that the GoatTeleporter is called GoatTeleporter
(i.e. in it’s define
function this is what we named it.).
AMD adds a final layer of awesomeness. That is anonymous modules and relative module paths.
What this means is we can choose not to provide the first argument to our define
function. There are
some caveats, but they are worth it. In order for this to work, we stick to one-module-per-file.
Then if you request a module based on relative path from your current module, and in the file
is an anonymous module, it will get passed into your app. This means that whole linked groups
of dependant modules can be moved around or incorporated in another app with little difficulty.
If there’s anything to learn from this rambling article, it’s this:
| Use AMD for modularization and Module loading in client-side Javascript.
I can’t recommend RequireJS highly enough (and if you have actually read to the bottom of this, then you should read the source code of RequireJS as well. It’s pretty funky how it works.)
Before I discovered AMD, I wrote a define
library for Asynchronous Module Loading and
dependency management - it doesn’t conform to the AMD spec at all and is much like the
final code snippet in the article. However it is fairly easy to understand, if you can
follow the rabbit-hole of callbacks involved. The source is on github.
Disclaimer: It probably only works in Chrome, untested anywhere else