Skip to content

Instantly share code, notes, and snippets.

@jrburke
Created April 7, 2011 05:50
Show Gist options
  • Save jrburke/907115 to your computer and use it in GitHub Desktop.
Save jrburke/907115 to your computer and use it in GitHub Desktop.
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.
*/
@Mparaiso
Copy link

Mparaiso commented May 9, 2012

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

@jrburke
Copy link
Author

jrburke commented May 9, 2012

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