So here it is – my submission to the AMD loader fray.
The aim here is to be minimally working and not comprehensive, complete or compatible. Some of my projects are so tiny they don't warrant a loader three times their own size, while still benefitting from the modularity and dependency resolution they provide.
This loader makes use of commonly available ECMAScript 6 features such as Promises and won't work in environments that lack them.
Note that in case a circular dependency is detected, an Error will be reported. This is in contrast to some other AMD loaders that quietly soldier on.
This package provides one function called define
. It is usually called
with two parameters, an array of module IDs and a function to which these
dependencies should be passed (see Examples).
In order to resolve the dependencies, the module IDs are used to find
scripts which are then loaded and executed. These scripts are expected to
call define
themselves in order to provide a definition for the module.
The module is usually the return value of the function passed as define
's
second parameter, but see Built-in
pseudo-modules for alternatives.
In the simplest case, module IDs are converted into URLs by appending
.js
. It is possible to specify a baseUrl
in the
configuration pointing to the location of the modules.
Much like path names, module IDs are considered to consist of components
separated by /
. The paths
configuration allows specifying replacements
for prefixes of such components, making it possible to create individual
»namespaces« of modules loaded from different locations.
Finally, if a module specifies its dependencies using IDs starting with a
.
, they will be interpreted relative to that module's own ID. In the
following example, if the module is loaded as mod/mod
, it will attempt to
load its dependency using the module ID mod/sub
.
define([ './sub' ], function (sub) {
// Use the submodule as sub.
});
Loader plugins provide another special kind of module ID.
Here's an example of how to load Mike Bostock's D3:
<!doctype html>
<title>dietAMD example</title>
<script src="define.js"></script>
<script>
define([ '//d3js.org/d3.v4.min' ], function (d3) {
d3.select('body').text('Hello, D3!');
});
</script>
[see]
One provided pseudo-module is require
. When specified as a
dependency, a function is passed to the module function. This function may
be used to request further dependencies by passing an array of module IDs
and a callback to be invoked with the requested modules.
This can be used to load modules dynamically, for example based on some runtime conditions.
define([ 'require' ], function (require) {
require([ 'mod' ], function (mod) {
// Use the requested module.
});
});
As an extension to the AMD standard, if no callback is passed to require
,
a Promise for an array of dependencies will be returned.
define([ 'require' ], function (require) {
require([ 'mod' ]).then(function (deps) {
// The requested module is in deps[0].
});
});
Another pseudo-module is module
. When specified as a dependency,
an object is passed to the module function. The object contains a property
named exports
which references a newly created object.
If the module function's return value is falsy, then the object
referenced by the exports
property at the time the module function
returns will become the module and the module function is expected to
define anything it wishes to export on this object.
define([ 'module' ], function (module) {
// This module exports one function.
module.exports.f = function () {};
});
A third pseudo-module is exports
. When specified as a dependency, a new
object is created and passed to the module function. If the module
function's return value is falsy and the module
pseudo-dependency
has not been requested, then this object will become the module.
define([ 'exports' ], function (exports) {
// This module exports one function.
exports.f = function () {};
});
If define
is called with a module function that expects arguments (i.e.
has a non-zero length), but without specifying any dependencies, then the
dependencies [ 'require', 'exports', 'module' ]
are passed to the module
function.
Loader plugins provide other resource types than JavaScript code.
If a dependency contains a !
character, the module specified by the ID
before the !
is loaded and its load
method is invoked, passing the
remaining part of the dependency string as a parameter. The load
function
should expect three arguments: id
, require
and onLoad
. The first
argument will receive the trailing part of the dependency string. The
second argument receives a require
function that can be used to load
resources relative to the module that invoked the loader plugin. And the
final parameter is a function to be invoked with the value of the resource
when it is available.
The domReady
module is included in this project. It evaluates to a
function that will invoke its argument exactly once after the DOM has
loaded completely. This is how it's used:
<!doctype html>
<title>dietAMD example</title>
<script src="define.js"></script>
<script>
define([ 'domReady' ], function (domReady) {
domReady(function () {
document.body.textContent = 'hello, world!';
});
});
</script>
[see]
The domReady
module can also be used as a loader plugin to slightly
reduce the boilerplate. In this case, the dependency resolves once the DOM
is ready and the document is passed to the function as the value of the
dependency.
define([ 'domReady!' ], function (doc) {
document.body.textContent = 'hello, world!';
});
The text
, json
and xml
plugins are also included. They evaluate to
the contents of the specified module ID loaded as the respective data type.
define([ 'text!file.txt' ], function (file) {
var div = document.createElement('div');
div.textContent = file;
document.body.appendChild(div);
});
When define
is called, it normally uses the module ID from the dependency
that caused the file to be loaded as the ID of the module being defined.
That's why ordinary modules shouldn't specify an id
themselves when
calling define
.
However, libraries are typically created by combining several modules into
a single file. This results in several calls to define
from within that
same file, making it impossible to deduce the names of the modules being
defined.
That's why the module ID may be explicitly specified as the first argument
to define
. Just like regular modules, the main module of the library
should not specify a module ID.
// Define a sub-module
define('sub', { 'hello': 'world' });
// The library's main module
define([ 'sub' ], function (sub) {
return { 'hello': 'hello, '+ sub.hello };
});
[see]
Modules defined in this way by libraries are also available outside of the library that defined them.
The configuration is very minimal. The config
method returns a new loader
with the specified configuration which can be used without affecting the
global environment. Modules will inherit the loader they were loaded with
when resolving their dependencies.
// Create a loader for a specific baseUrl.
var libDefine = define.config({ 'baseUrl': 'lib/' });
libDefine([ 'mod' ], function (mod) {});
The loaders created by config
are completely independent of each other.
This means that even modules that have already been loaded will be loaded
again when a new loader is created. This is necessary because a module's
dependencies might be resolved differently under a new configuration.
The global define
is special in that it resolves dependencies using the
»most recent« loader on the dependency chain. Installing a configured
loader globally isn't recommended as it will break this behaviour. Even
dependencies of a module loaded using a specially configured loader will
then always use this global instance instead of the one they inherit.
// Not recommended!
define = define.config({
'paths': { 'd3': '//d3js.org/d3.v4.min' },
});
The configuration options and their meanings are:
-
paths
As a first step when finding a dependency, if a prefix of the module ID components matches a property in this object, it is replaced with the corresponding value. For the purpose of prefix detection, the module ID is considered to consist of components separated by/
excluding an optional extension separated by.
. -
baseUrl
If a dependency does not start with/
or a URL scheme, this string is inserted at the front. It defaults to the empty string, so modules are loaded from the same location as the main page.
As a last step, .js
is appended to the module name to form the final URL.
This is not configurable.
I'm sure you're burning to know how to create modules that will work whether or not an AMD loader is present. If you're not into build tools, you can use a »universal module definition« (UMD) and this is my suggestion:
(function(r){return r.define||function(e,m){r.Mod=(m||e)($)};}(this))
([ 'jquery' ], function ($) {
return {
// …
};
});
If no AMD loader is present, the module will be made available through the
global name Mod
in this example.
If an AMD loader is present, a global binding for the module is not created. Consequently, if any code in your project provides a loader, then modules using this UMD will only be available through AMD.
The downside of this pattern and most common source of errors is that the
list of dependencies ($
in the example) must be repeated in the »mangled«
line and manually kept in sync.
There are certainly a lot of unimplemented features – in fact, everything that isn't mentioned above.
And don't even get me started about »source scanning« to support
require
…
The upshot is that the code is small, so it's straightforward to add anything when needed.
If the previous section put you off, you might like to consider the following alternative AMD loaders:
If you're anything like me, you'll quickly get fed up with the sheer number of available loaders and their respective bloat and shortcomings, so just write your own :P
The build process is based on BSD make. If you're on a BSD platform, set
the DESTDIR
environment variable to your web deployment location and run
the familiar
make all install
On other platforms you can use bmake, for example.
The build uses UglifyJS2. If not installed globally, set the
UGLIFYJS
environment variable to its location.
The make release
command installs the results of the build process in the
dest
directory of the source tree ready for packaging.