Tiny, extensible implementation of the CommonJS Asynchronous Module Definition (AMD)
specification. The core implementation, define.js
, comes to 812 bytes
when compressed using Closure Compiler with advanced optimizations
and UglifyJS. Integration points in module definition and loading
allow for any kind of custom behavior to be added.
There are a quite a few implementations out there already:
The implementations that I have seen tend to be monolithic, including all the features that you might want in one package. What I wanted was a micro-library.
tAMD is very small and makes few assumptions about your use case. The
core implementation, define.js
, includes the features that the AMD
specification says must be included and very little else. All other
features are provided as addon components. Some of those are provided
in this repository to get you started. Because of this tAMD can be
customized to suit just about any requirements while remaining as small
as possible. You are invited to create your own components to suit your
needs. tAMD is intended to be easy to read and easily hackable.
tAMD is made up of several files that provide required and optional functionality. A ready-made combined file that includes all optional functionality is available at dist/tAMD.min.js
It is recommended that you build your own custom file to get a smaller size. There is a grunt task provided for this purpose. To use it you will have to clone this repository and install grunt. You will also need to have Java installed to run Closure Compiler.
In the project directory run grunt compile --components='...'
with
a space-separated list of the components that you want to include in
your custom build. The components that are available are:
component | description |
---|---|
define | minimal core |
hooks | provides extensibility via lifecycle callbacks |
normalize | automatically resolves relative module names |
plugins | supports plugins with the pluginname!resource syntax |
loader | downloads modules on demand, requires configuration |
debug | reports potential problems, such as missing modules |
jquery | jQuery compatibility with versioning support |
jquery-minimal | jQuery compatibility without versioning support |
The functionality provided by each component is described in detail below.
If you want the smallest possible build with no extra features, use this command:
grunt compile --components='define'
A more typical build might look like this:
grunt compile --components='define normalize plugins loader'
Minified builds will be output in dist/tAMD.min.js.
To make minification as effective as possible the source for define.js
defines global variables. These are wrapped in a private scope as part
of the compilation process. You can just grab files for the modules
that you want and combine them with your own system - but if you do so
you will get variable leakage.
The included grunt command has the additional advantages that it will perform aggressive minification using Closure Compiler's advanced optimizations and will make sure that your components are combined in the correct order with the correct dependencies (many of the included components have interdependencies).
This is the core of tAMD and is the only required component. It does not implement module loading, resolution of relative paths, multiple module versions, or any of that fancy stuff. Look for these features in the other modules below.
define.js
places two functions into the global namespace, define()
and require()
. The first, define()
, behaves exactly as is specified
in the AMD specification. require()
is similar in that it
lets you load dependencies asynchronously; but it does not define
a module. require()
has the same API as define()
, but it should not
be given a module name argument. For example:
require(['jquery'], function($) {
setInterval(function() {
$('.blink').css('visibility', 'hidden');
}, 500);
setTimeout(function() {
setInterval(function() {
$('.blink').css('visibility', 'visible');
}, 500);
}, 500);
});
There is also a synchronous version of require()
available that
matches the behavior of require()
in traditional CommonJS modules. To
get the synchronous version grab it as a dependency in a module or in an
async require()
callback:
// The outer require() is asynchronous.
require(['require'], function(require) {
var $ = require('jquery'); // This is synchronous!
$('body').append('<p>greetings</p>');
});
If you have a project where all of your JavaScript assets are loaded
together and you want the organizational powers of AMD modules in the
smallest package possible, then define.js
by itself may be ideal for
you. Just include define.js
before any calls to the define()
function.
The hooks component provides friendly integration points into tAMD. You can register callbacks to be invoked when a module is declared or before a module is published and made available to other modules. Your callbacks can modify properties of a module before it is created, cancel creation of a module, or perform some side-effect like logging or dependency loading.
See Customizing for details.
When this component is included dependencies that are given as relative paths are automatically normalized. Relative paths will probably not work as you expect unless you include this component or one like it.
For example, if you have a module called myFeature/view
and another
called myFeature/model
, since both modules have the same myFeature/
prefix, you can include one from the other with a relative reference:
define('myFeature/view', ['./model'], function(Model) {/* ... */});
This component includes a module called tAMD/normalize
, which exports
a function that takes a module id, which may or may not be a relative
path, and a context and returns the normalized module name.
The plugins component adds support for AMD plugins. These allow an AMD implementation to load resources other than JavaScript, or to load JavaScript resources that are not written as AMD modules.
A module name that is handled by a plugin has the format
pluginname!resource
where resource can be any string - the plugin
determines how the resource is looked up. When this component
encounters a dependency name with that format, it requires a regular
module the same name as the plugin and hands of responsibility for
loading the special module.
One use for plugins is to download text resources that should not be
executed. You might use this to download templates. First define
a text
plugin:
define('text', ['jquery'], function($) {
return {
load: function(resource, require, done) {
$.get(resource).success(done);
}
};
});
The plugins component looks for the load
function exported from the
plugin definition and calls it to load text modules. Here is how this
plugin could be used:
require(['text!/templates/hello.txt'], function(hello) {
alert(hello);
});
Plugins might also be used to load stylesheets, compile CoffeeScript, or anything else that you can dream up.
For more details, see the AMD plugin specification.
Note that this implementation does not currently support the 'dynamic'
property on plugins or a config
argument to the plugin load
function.
This component allows you to specify mappings between module names and URLs. When a module name is referenced as a dependency, if it is not already loaded, then corresponding URLs will automatically download and execute.
Mappings are created using the map()
function in the loader
module:
require(['tAMD/loader'], function(loader) {
loader.map(
['jquery'],
['https://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js']
);
});
The first argument to map()
is a list of module ids, and the second is
a list of URLs. If any of the given module ids is referenced as
a dependency then the given URLs will be loaded in order. If two
different dependencies match up with two different URL lists then those
lists will download in parallel.
Note that the synchronous version of require()
does not trigger lazy
loading. loader.js only works on asynchronous dependencies.
If you are using normalize.js
make sure to include it before loader.js
so that relative paths are normalized before loader.js
tries to look
them up. If you use the provided compiler it will make sure that the
component ordering is correct.
With loader.js
you have to specify a URL mapping for every module.
This component is most useful when combined with a server-side script
combining task that outputs URL mappings automatically; or when you only
have a few lazily loaded modules.
This component outputs various warnings and error messages to the console
that can be helpful during development. For example, a module may
depend on another module that is supposed to be lazily loaded by
loader.js
; but for some reason the second module never loads. With
debug.js
running you will see a console warning after 2 seconds in
this scenario. Without debug.js
The first module would silently wait
forever.
Debug checks to make sure that console
is defined before outputting
messages - so it should not produce errors in browsers that do not
implement console.log
and related functions. To see messages you will
have to use a browser that implements console.warn
and
console.error
.
As an independent component, debug.js
can be loaded in development for
diagnostics and can be left out in production to save space. It can be
included as a separate file and does not have to be compiled. But to
use debug you do have to have the hooks component compiled into your
tAMD build.
jQuery can be loaded as an AMD module. But only if the AMD
implementation has a truthy property, define.amd.jQuery
. This
component sets that property and also supports loading multiple jQuery
versions on the same page.
Whenever a copy of jQuery is loaded, this component publishes multiple AMD modules with different degrees of version information. For example after loading jQuery 1.10.1, these module names are defined, all of which point to that copy of jQuery:
jquery
jquery-1
jquery-1.10
jquery-1.10.1
If you then load jQuery 1.10.2 then jquery
, jquery-1
, and
jquery-1.10
will be redefined to point to the newer jQuery version,
and you will get a new module id, jquery-1.10.2
.
This component will call jQuery.noConflict(true)
when a jQuery is
loaded that is older than one that is already present. That means that
the global variables jQuery
and $
will always point to the latest
jQuery version that has been loaded.
This is a one-line component that sets define.amd.jQuery
to true
so
that jQuery can be loaded as an AMD module. Use this component if you
only need one jQuery version on a page.
The hooks component provides friendly integration points into tAMD. You can register callbacks to be invoked when a module is declared or before a module is published and made available to other modules. Your callbacks can modify properties of a module before it is created, cancel creation of a module, or perform some side-effect like logging or dependency loading.
There are three lifecycle events that you can register callbacks for:
- define :
hooks.on('define', [moduleName], function(id, dependencies, factory, next){})
- publish :
hooks.on('publish', [moduleName], function(id, moduleValue, next){})
- require :
hooks.on('require', [moduleName], function(id, contextId, next){})
Use the "define" hook to run a callback as soon as a module is declared
via a call to define()
before its dependencies are resolved or its
factory is invoked:
require(['tAMD/hooks'], function(hooks) {
hooks.on('define', 'someOldModule', function(id, dependencies, factory, next) {
revisedDeps = dependencies.map(function(dep) {
return dep === 'jquery' ? 'jquery-1.4' : dep;
});
next(id, revisedDependencies, factory);
});
});
The above example intercepts the definition of a module called
"someOldModule" and replaces its dependency on jQuery with an older
jQuery version. The dependencies array is an array of module names. By
modifying the arguments given to next
you could also change the name
of the module or replace or wrap its factory.
factory
refers to the function or object that contains the definition
for a module - the last argument to define()
.
The last argument to a callback will always be a function that is called to signal that the callback is complete. In this way lifecycle callbacks can operate asynchronously. If that function is never called then the define, publish, or require operation will never complete.
In a "define" hook, if you choose not to invoke next
the module
definition will effectively be cancelled. In that case the module's
dependencies are not resolved and its factory is never invoked.
The "publish" hook is similar; except that its callbacks are invoked
after the module's factory is executed. In a "publish" hook you can
change the name of a module, tweak or replace the API that the module
exports, or prevent the module from becoming available as a dependency
to other modules by not invoking next
. For example:
require(['tAMD/hooks'], function(hooks) {
hooks.on('publish', 'jquery', function(id, moduleValue, next) {
var version = moduleValue.fn.jquery; // moduleValue === jQuery in this case
next('jquery-'+ version, moduleValue);
});
});
This example changes the name of any jQuery modules to include a version number as part of the module name.
The "require" hook is invoked during dependency lookups. Whenever
define()
or either the sync or async versions of require()
are
invoked with dependencies, each dependency name is run through any
"require" hooks before the dependency is actually given.
Two arguments are given to "require" callbacks: the name of the module
that is required and the name of the module where the require event
originated. If a dependency is referenced by async require()
or by
a define()
call with no module name then the second argument will be
undefined
. Here is a quick and dirty example of how to use
a "require" hook to normalize relative module paths:
require(['tAMD/hooks'], function(hooks) {
hooks.on('require', function(id, contextId, next) {
var contextParts = contextId ? contextId.split('/') : [];
var idParts = id.split('/');
var dir, module, normalized;
if (idParts[0] === '.') {
dir = contextParts.slice(0,-1);
module = idParts.slice(1);
normalized = dir.concat(module);
} else {
normalized = idParts;
}
next(normalized.join('/'), contextId);
});
});
With this code in place, you could express express module dependencies like this:
define('tAMD/myComponent', ['./hooks'], function(hooks) {
/* ... */
});
In which case the arguments given to the "require" hook callback would
be "./hooks"
and "tAMD/myComponent"
.
This is just an example. There is a better implementation of relative path resolution in the normalize component.
In the example above there was no module name argument given to the "require" hook. The hook can be given a module name, which will cause it to act only on that module, as with "define" and "publish". But with all three hook types if you exclude the module name argument then the hook will act on all modules.
As with "define" and "publish", you can cancel "require" events by not
invoking next
. If a "require" event is triggered by a dependency list
in a define()
call then the corresponding "define" event will also be
cancelled.
Copyright 2012-2013 Jive Software
Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.