Created
April 7, 2011 05:50
-
-
Save jrburke/907115 to your computer and use it in GitHub Desktop.
Description of browser-friendly module APIs: AMD and Loader Plugins
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//******************************************* | |
// 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. | |
*/ |
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
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