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): | |
define({ | |
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. | |
*/ | |
//BAD | |
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: | |
https://github.com/jrburke/requirejs/blob/optimizer/build/jslib/env.js | |
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: | |
http://requirejs.org/docs/plugins.html | |
*/ | |
//*********************************************** | |
//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: | |
*/ | |
require({ | |
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: | |
http://requirejs.org/docs/api.html#packages | |
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. | |
*/ |
This comment has been minimized.
This comment has been minimized.
There is no real execution difference between level 1 and 2, just a different way to express it. Level 2 requires a Function.prototype.toString() call for dependency calls, but that should be it. Some browsers, like the PS3 and older opera mobile ones do not have usable Function.prototype.toString() value, so for those cases level 1 is best. The r.js optimizer will convert level 2 syntax to be level1 one with a dependency array so the built code is usable in those browsers. |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
This comment has been minimized.
very interesting thanks , i'm new to AMD and the level 2 is what i would use as it mimics more the node.js module syntax, are there some fundamental differences between the 2 though ?
Anyway ,AMD is the way to go . it is a bit difficult to understand for a beginner like me , but once one fully grasps the usefullness of that thing onenever goes back.
great job.
Marc