7/26/2012 - 10:22 AM

Description of browser-friendly module APIs: AMD and Loader Plugins

Description of browser-friendly module APIs: AMD and Loader Plugins

// Level 1, basic API, minimum support
Modules IDs are strings that follow CommonJS
module names.

//To load code at the top level JS file,
//or inside a module to dynamically fetch
//dependencies, use *require*.
//one and two's module exports are passed as
//function args to the callback.
require(['one', 'two'], function (one, two) {


//Define a module
define(['one', 'two'], function (one, two) {

    //Return a value to define the module export
    return function () {};

//Allow named modules by allowing a string as the
//the first argument (support can be limited
//in Node by only allowing the ID
//to match the expected name by the Node loader)
define('three', ['one', 'two'], function (one, two) {

    //require('string') can be used inside function
    //to get the module export of a module that has
    //already been fetched and evaluated.
    var temp = require('one');

    //This next line would fail
    var bad = require('four');

    //Return a value to define the module export
    return function () {};

//'require', 'exports' and 'module' are special dependency
//names that map roughly to the CommonJS values, except this
//require allows the dependency array/callback style mentioned above
define(['require', 'exports', 'module'], function (require, exports, module) {

    //exports is particularly (only?) useful for circular
    //dependency cases. If exports is asked for, but there is
    //a return value for this function, favor the return value
    //unless another module has been given this module's exported
    //value already.
    exports.name = module.id;

    //module is important to get the module ID without
    //knowing the current module ID.

//A simple object module with no dependencies
//(very useful for configuration objects):
    color: 'blue',
    size: 'large'

// Level 2, sugar, particularly for converting existing CommonJS modules

//For many dependencies, it may be desirable to list dependencies
//vertically. This form also helps translate old CommonJS modules
//to this wrapped format. It uses Function.prototype.toString() to find
//require('moduleName') references and loads them before executing
//the definition function.
define(function (require) {
    //The function arg *must* be called require, and
    //dependencies *must* use that require name in order
    //for the parsing to work.
    var one = require('one'),
        two = require('two');

    //Return can still define a module.
    return {
        color: 'blue'

//The above define call can be thought of being converted to
//this form after require calls are parsed out:
define(['require', 'one', 'two'], function (require) {});

//For the full access to the CommonJS legacy variables, this form is also supported.
define(function (require, exports, module) {
    var one = require('one'),
        two = require('two');

    //Return can still define a module.
    exports.color = 'blue';

//The above define call can be thought of being converted to
//this form after require calls are parsed out:
define(['require', 'exports', 'module', 'one', 'two'], function (require, exports, module) {});

NOT ALL CommonJS modules can be converted to this syntax. An important
behavioral difference with this API: all dependencies are loaded *and*
executed before the current module definition function is called. So
in particular, CommonJS code that does these kinds of things will not
work the same, and may even generate an error. 
var a;
if (someCondition) {
    a = require('a1');
} else {
    a = require('b1');

//BAD: using a try catch to
//try to load a module that may or
//may not be available then doing something
//with it.
try {
   var a = require('a');
   //more stuff here
} catch (e) {
    //a may not exist

//Any sort of logic used to choose a module to require needs 
//to be handled by the callback-style require:
require([computedModuleName], function (mod) {});

//Or by using loader plugins, level 3:

//Level 3, Loader plugins
Loader plugins allow conditional loading/branching
of loading, and also more complex loading.
Plugins are just regular modules that implement a
load() API. A plugin is indicated by separating
the plugin's module name from the resource name
by a ! sign.

//This example uses the 'text' plugin to load a resource
//called some/template.html. So, text.js is loaded, then its
//load() method is called to load some/template.html. The
//load() method is passed a callback function to indicate when
//the resource is loaded.
define(['text!some/template.html'], function (templateString) {

Useful plugins:
* env: changes a resource name to include the environment (node or browser)
  in the resource name. I use this in RequireJS to allow running 
  the optimizer either in Node or Rhino:
  It can be seen as a replacement of the overlays feature in packages.
* text: to load a text file, useful for templates.
* i18n: can load a few modules to present one object
  to the application which is a combination of country, language.

This plugin approach can be used instead of the require.extensions in Node.
So things like a coffeescript or binary extensions could
be supported via coffee! and node! plugins.

Plugins also can implement some APIs to participate in a build optimizer, 
so they can inject their resources into a built file. This is very useful 
for browsers, but could also benefit node, by allowing single file JS 
utilities instead of delivering a whole package. Complete plugin 
API is here:

//Level 4, configuration and pathing and packages
Browsers should only use one path to look up a module. It is error 
prone and very bad for performance to look in more than one place. 
So it is important to configure where the baseUrl for all modules 
are found, and to allow some path mappings for modules that may not
be inside that baseUrl.

I use an object passed to require() in the top level script, but 
I am open to specifying a require.config() instead of overloading 
require() so much:

    baseUrl: 'scripts',
    //Optional path adjustments for
    //modules that are not in the baseUrl directory.
    paths: {
        'some/module': '../external/some/module'

1) CommonJS packages with a 'lib' and 'main' config give too many
options for configuration, and in a browser configuration those 
config values need to be passed down to the client. This is 
awkward and ugly. I do support it in RequireJS via package config:

However, I much rather prefer a stronger convention and 
to remove the 'main' and 'lib' features of CommonJS packages. 
In this way, a package manager does not have to parse the 
package.json and insert configuration in the application. 
It is much cleaner and easier to follow.

So I prefer to move to an approach where getting a module from a
package uses its explicit module name. So instead of doing
require('packageName'), use
require('packageName/index') or require('packageName/main')
instead. This means there is no configuration besides a baseUrl
is needed, and it is clearer all around what is going on.

2) I have found with other systems like Java and Python that having a 
classpath or a set of commonly used packages that are used across all 
applications to be a source of pain than an actual help. Version 
conflicts being the main issue, and tracking down the magic directories 
used by an execution environment being another.

By requiring each app to have its own packages relative to its own 
baseUrl, it makes the application much more understandable and robust. 
Since there is only one lookup path per module it is even clearer. 

So that is the other change I advocate: no more require.paths, 
no magic place to install modules. Having some basic modules
delivered as part of Node still may make sense though.

This also matches how web browser applications work today -- all
the scripts need to be visible relative to the HTML page on URLs
that are easy to discover.