Supporting AMD and Script Tags
Let's Make a Framework is an ongoing series about building a JavaScript framework from the ground up.
These articles are tagged with lmaf. The project we're creating is called Turing. Documentation is available at turingjs.com.
Last week I started retrofitting Turing with AMD (Asynchronous Module Definition) support. This week I’ll complete most of this conversion to AMD by demonstrating how to use AMD modules both with and without a script loader.
Backwards Compatibility
James Burke, creator of RequireJS, contributed to Dojo’s script loader. As we’ve seen previously in this series, Dojo ships with a resource loader that’s capable of loading AMD modules, and is in fact built upon AMD. In contrast, jQuery conditionally supports AMD if a define function exists.
I wanted to do something slightly different: use define to express module dependencies, but still allow people to include Turing modules on a page without using a script loader. All modules are dependent on turing.core, and some are dependent on the dom and events modules. Turing contains some useful Underscore-like functionality in the turing.enumerable module, and I’ve always avoided reusing it in other modules because there wasn’t a way of expressing the dependency.
Therefore, I want Turing to be structured using AMD and to support these use cases:
- Load modules with script tags
- Use a monolithic, optionally minimised, build of the entire framework
- Load Turing modules with a script loader
This means we need an alternative method to jQuery’s conditional AMD support. Dojo’s built-in script loader is another solution, but it’s actually a huge amount of work and people might not always want to use a script loader.
So, what do we do? There are a few ways to approach this problem:
- Make the build process change calls to
defineto work without a script loader - Force people to use a script loader
- Patch
definewhen not available
The first option seems like it would require too much maintenance – people would have to choose an AMD Turing download or a non-AMD version. The second option allows us to build Turing using AMD’s nice, modular structure, but completely breaks backwards compatibility. The third option has potential, and this is the one I spent some time exploring.
Patching define
To support AMD’s define method when an AMD-compatible script loader isn’t available, I added a method to turing.core:
// turing.core.js
(function(global) {
var turing = {}, modules = {};
// `turing.core`'s code goes here
turing.define = function(module, dependencies, fn) {
if (typeof define === 'function' && define.amd) {
define(module, dependencies, fn);
} else {
if (dependencies && dependencies.length) {
for (var i = 0; i < dependencies.length; i++) {
dependencies[i] = modules[dependencies[i]];
}
}
modules[module] = fn.apply(this, dependencies || []);
}
};
// Export `define``
if (typeof define === 'undefined') {
global.define = turing.define;
}
}(typeof window === 'undefined' ? this : window));
// turing.dom.js
define('turing.dom', ['turing.core'], function(turing) {
// `turing.dom` source
});
By wrapping each module in a define statement, individual modules can now be loaded using RequireJS, and they’ll automatically load turing.core:
require(['turing.anim'], function(anim) {
turing('#results').html('Turing has loaded with the DOM module.');
turing.anim.chain(turing('#animate')[0]).move(1000, { x: '100px', y: '100px', easing: 'ease-in-out' });
});
The turing function here is the global turing method that will get exported to window. The turing.anim module has turing.core as a dependency, so we get the familiar turing function as expected.
This snippet is used by a functional test that I wrote to ensure Turing modules can be loaded with RequireJS. It’s a small Express app that can be found in test/functional/require.js.
Conclusion
jQuery is currently a monolithic framework (although the build process can be customised to remove unwanted libraries) which makes supporting AMD relatively easy. Conversely, Dojo is highly modular – it’s built on AMD and includes its own module loader.
Here I’ve presented an alternative solution that uses a lightweight version of AMD’s define method to support module loading through script tags, monolithic files, and AMD-compatible script loaders. It’s a potentially useful pattern for structuring large libraries and frameworks that have interdependent modules.
The code for this tutorial can be found in commit 4361042.