Prelude
Before I get into my advice about the benefits of using an Asynchronous Module Definition or AMD, there are a few things to note. First, this article assumes you have a working knowledge of JavaScript. Second, my namespace is located as /static/src/article and that I'm happy serving it from /static/dist/article (similar to Java's output). Third, and finally, I'm using grunt.js, mostly because it's easy to set up. I recommend checking to see if your back-end build system has tools available or if you're short on time and my setup is ok with you, check out the source used by this article.
What is an AMD?
An AMD (Asynchronous Module Definition) is a JavaScript module wrapped in a bit of boilerplate that allows a separate script to manage its dependencies in a sane way. It attempts to take the place of one of JavaScript's greatest issues, namely the ability to define modules in a reusable way.
It is, almost certainly, the most widely adopted system for providing programmatic loading of JavaScript in the browser. The definitive API documentation describes it as:
...a mechanism for defining modules such that the module and its dependencies can be asynchronously loaded. [AMD JS API Wiki]
Fortunately, unlike most standards, AMD is capable of working with modules that are outside of the standard. Unpackaged resources are generally a bit more work to deal with, but the loader will load them. In addition, nearly all AMD loaders support an easy-to-use plugin architecture, such that loading arbitrary things, and later building them, is both possible and plausible.
Why do I need it?
The importance of the asynchronous component cannot be overstated. Because dependencies are explicitly listed, it's possible to make very granular inclusions. Surely, you're reading this because you know about and want to compress your JavaScript, and AMD is a great platform for allowing this, however it's helpful for development too, since the source files will load faster while you're implementing new features.
More important than this though, it cuts down what actually goes in your built file. When loading separate files in dev and building them for production becomes easy, developers are freed to make the choice to break up a module based solely on logical groupings.
For example, if I only want to manipulate arrays, then I can simply define my Array manipulation module:
// Notice I can omit the module name. The loader assumes modules map to a file
// path unless it's told otherwise.
define(function () {
'use strict';
var methods = {},
// There are plenty of others, but I want to keep this brief.
EXPOSED = ['map', 'forEach', 'every', 'some', 'reduce'];
var aProto = Array.prototype;
var aSlice = aProto.slice;
var methodName = '';
var makeMethod = function (name) {
return function (array) {
//noinspection JSReferencingMutableVariableFromClosure
return aProto[name].apply(array, aSlice.call(arguments, 1));
};
};
for (var i = 0, len = EXPOSED.length; i < len; ++i) {
methodName = EXPOSED[i];
/**
* Create a wrapper for whatever array methods are listed.
* NOTE: This is not an appropriate real world solution.
*/
methods[methodName] = makeMethod(methodName);
}
return methods;
});
Then, I can include just that module and start working with arrays in some fantastic way. Assuming I put the array tools at /static/src/article/util/array, I would put:
/*global document, alert */
require([
'article/util/array'
], function (
array
) {
'use strict';
var titleText = array.map(['Build JS, ', 'World', '!'], function () {
// Interesting manipulations here.
return item;
};
alert(titleText);
});
What do you need to incorporate AMD into your build?
Generally, the ideal output is a single, monolithic file that contains all the dependencies of your app. In order to achieve this, you'll want to define a build profile which should look like:
exports.config = ({
// The name of this layer.
name: 'article',
// Where the output will go when we build.
dir: 'dist',
// Where the unbuilt JS resides.
appDir: 'src',
// Path from this to the path you'll use for relative declarations of packages.
baseUrl: '.',
// A list of packages you're using
packages: [
{
name: 'article'
},
{
name: 'mustache',
// I'm setting main here, since mustache doesn't use the default, which would be mustache/main
main: 'mustache'
},
{
name: 'has',
// Same as Mustache
main: 'has'
}
],
// Code branching.
has: {
// All has('love-for-ie-6')'s in the code will be replaced w/ true, which lets UglifyJS or Closure-compiler remove
// the if statement around them.
'love-for-ie-6': true
}
});
What's going on here? Well, first off, I am using exports.config so that I can use this in my grunt.js file. This only matters if you're using a NodeJS based build tool; otherwise, you should be able to get away with a simple object.
Next, I'm providing the name of my app, where the sources come from, and where they go once they've been built. I then enumerate all the packages with the minimum amount of information my build tool will need (in this case r.js). Finally, I list out things I know about the layer I'm building. In this case, I know my customer loves IE6, so I set that to true.
Let's break down this last bit a little further. The order of operations is (roughly):
- The build tool concatenates all the modules, recursively looking up their dependencies.
- The resources are all move into the dist directory.
- All has tests, that have an entry in the profile, are replaced with the boolean values you've given.
- The minifier runs and its dead code removal tool takes out the short-circuited branches.
So, the sources when staged, look something like:
// The has integration will take this from the profile above.
if (has('love-for-ie-6')) {
alert("I dislike you.");
}
else {
alert("You're awesome.");
}
// Then after the has replacements.
if (true) {
alert("I dislike you.");
}
else {
alert("You're awesome.");
}
// Finally, once compiler finishes it off.
alert("I dislike you.")
So, what's in a built file and why do I care?
Basically, they are a collection for function calls which take:
- The name of the module in fully qualified terms.
- An optional Array listing the modules dependencies.
- The function the returns the module (or kicks off the side-effects associated with the module).
The reason that this is interesting is that AMD loaders essentially have only one objective and that's to ensure that all modules have their dependencies loaded before they're executed. Whenever a define call happens, that sticks another key in the loaders list of available modules. So, if all the modules our script needs are in the built file, then the loader won't try to load them.
This process works a lot like an impromptu party. If Mike and James are already at your party and you call Tom to join in, you wouldn't send him to Mike's house to pick him up. Sure, you could, but that would be rude and a waste of time. It's the same with modules, once they are at the party (your clients browser), there's no reason to try and get them again.
What tools are available for building with AMD?
RequireJS
As I mentioned above, r.js is an excellent tool for building AMD modules and it is normally coupled with RequireJS to provide a full range of loading solutions.
Dojo
The primary toolkit of Vodori, Dojo is available at dojotoolkit.org. It comes fully AMD packaged and ready to build out of the box (as of 1.8). Indeed, Dojo 1.8.1's sources are rumored to be compatible with RequireJS (the issues were pretty minor previously). For AMD + Dojo examples, checkout our FE challenge and dojo-boilerplate.
curl.js
Though it seems less used than Dojo and RequireJS, curl.js has a rather nice compliment of plugins and solid documentation about using it with virtually any commonly deployed JS stack. One benefit of curl.js is that by providing script loading synchronously but hidden behind an asynchronous API, it can lead to significantly faster page load during development. [John Hann on David Walsh's blog]
What if my build project is truly massive?
The quick answer is to build many layers, which can be done with multiple profiles, then at the expense of one or two extra requests, you can load up your remaining resources in a conditional way. Some found this insufficient and came up with a radical alternative. Vodori has some pretty neat tricks up its sleeves and even more on the way. I'm hoping to share some of this in the near future.
Are there any gotchas you've come across?
You bet! Below are a couple that have really been sore points.
Random script blocks, with dependencies, in HTML
The most serious, because it is endemic to the pattern, is that if you have to deal with legacy code, where JavaScript, that relies on an AMD module, is buried inside the HTML, then the dependency might not load before you reach that unscrupulous scripter's tag. This should never come up with new construction, but if it does, or if you're simply improving existing code, the simplest option is to concatenate your files server side even when you're in development mode. If you're in a slick browser and your build tool offers source maps, then you can still hide the difference from yourself.
Deploying to several varied directories is more trouble than it's worth
Notice in this project, I put all my dependencies into a single src and transfer them to a single dist. We wound up with a more complicated directory structure for Pepper and it was too much work. If you take only one thing away from this blog post, this should be it. Use one source directory and one dist directory. Trust me, the configuration overhead is not worth it.