Chiron, the Centauren, mentor of Achilles and Ajax, taught that one ought think like a man, fight like a beast, eat like a pig, and digest with at least a dozen modular stomachs. In the Greek mythos, Chiron traded his immortality to free Prometheus from the perpetual woe of Internet Explorer. This recently excavated bass relief is a rare Hellenic depiction of Chiron in his true, bovine majesty.
There are scarce innovations in Tale's «Chiron» JavaScript library—mostly proven code cribbed from other free libraries. However, its heart is an advanced module system, the likes of which we have not yet seen. This brief article will explore how to use modules.js to create and use modules.
Chiron strives to provide parallels with Python. So, like their parselmouthe analogs, Chiron modules are singleton objects, loaded from a file on demand. They load once and are thereafter instantly available to all other modules.
Chiron modules free the developer from worries about package dependencies and global name space conflicts. modules.js does not modify any existing JavaScript objects, not even to add a single object to the global window object like every other popular library.
Using modules.js only requires one <script> tag. When you load modules.js with the script's src attribute, you may specify one or more JavaScript module URL's delimited by ampersands (&) that modules.js will load in order. As advertised, modules.js does not write itself into the global object, so the module system will only be available to these scripts†.
A web page that uses modules.js will only use one <script> tag. With this, it will load modules.js and pass the names of other scripts as ampersand delimited URL's to the query string. Each of those modules will be loaded in turn, automatically once the module loader is ready.
Supposing that you had a page of content, index.html; a style sheet of presentation, index.css; and a script of behaviors, index.js, you would include in your HTML header:
<script src="modules.js?./index.js"></script>
Some relevant points from this example include:
Also, you might be concerned that this will cause HTTP Request bloat if you include a deep tree of discrete modules. This is true, but using the preload script in Subversion, you can wrap, concatenate, pack, and compress your commonly loaded modules. This doesn't implicitly load the modules, but can allow modules.js to perform exactly one HTTP request to fetch the text of your modules. This can maximize the value of gzip compression since all of your files will be combined to use the same pattern table.
<script src="modules.js?preload.js&./index.js"></script>
A module is just like any other JavaScript. Write a JavaScript file and include it on your page. The only difference is that when you create names by declaring functions and variables and adding some of them to your context object (this) instead of polluting the global name-space, you're creating names for your module alone.
Most existing JavaScripts will function perfectly as modules. Supposing that you've made all of your scripts dependencies available either in the old fashioned way of augmenting the window and DOM elements, or by including other scripts into your module's scope using the include and require functions, your script will work exactly the same way it did before.
Take the opportunity to refactor a little; remove all those nasty, fully-qualified names that start with YAHOO, dojo, jQuery, mootools or what-not. Get rid of all that code that makes sure your modules were loaded in the right order. Get rid of all that code that creates your global name-space object if some other module hasn't already.
Then, choose which names you want to export and add them to the context object, this, which is your module object instead of a window.
Inside a module, you use the include or require functions to request a module from a JavaScript URL relative to modules.js or your module.
require blocks and returns the Module object, or passes it to a continuation. include requires the module and copies its names into your module and returns the module (details follow). The module loader will assure that the dependencies of your dependencies are met.
var base = require('base.js');
require('base.js', function (base) {
});
include('base.js');
include('./common.js');
The module loader's job is to manage dependencies and provide functions to each of its modules so that they can acquire names from one another. The module loader goes a bit out of its way by also providing module scope logging functions and some other tools.
The environment that modules.js creates for each module is analogous to the environment the browser provides to scripts.
In a script:
Effectively, this is the same thing as concatenating all your scripts and putting them inside this code block (although with blocks don't behave quite the same way in all browsers; assume this is in a standards compliant browser):
(function () {
with (window) {
...
}
).call(window);
modules.js provides a few more scopes and a different context object. The module is a singleton object of type Module that contains any names the module intends to export. The moduleScope is a scope that modules.js manages by providing certain "global" functions like include, require, and log that are bound to the module context so they can grab modules relative to the current module's URL, among other things. Additionally, modules.js wraps your module in an enclosure so that your var variables and functions are effectively private to the module, just like they would be private to an object if they were in an object constructor function.
In a module:
So, you can imagine that your module is wrapped in this chunk of code:
with (window) {
with (moduleScope) {
with (module) {
(function () {
...
}).call(module);
}
}
}
This means that you can write to your module's exported names by augmenting this (the module object), and you can read any name without qualifying moduleScope or module beforehand.
To create a function:
this.f = function () {};
To use it in this or any other module:
f();
Also, var and function statements create variables in the anonymous scope, so they are private to your module.
function private () {
}
var private = 10;
moduleScope includes the following names:
Consider demo.html, a page that displays a list of all the names in a browserBase.js module:
demo.html:
<html> <head><!-- of a man --> <script src="modules.js&./demo.js"></script> </head> <body><!-- of a cow --> </body> </html>
demo.js:
include('base.js'); /* from base import * */
var browser = require('browserBase.js'); /* import browser_base as browser */
/* when the DOM has loaded */
browser.ready.observe(function () {
document.body.innerHTML = dir(browser).join(', ');
/*
dir is a function provided by base.js
that constructs a List of all the
names in the given module, or this module
by default.
*/
});
That's how you create modular JavaScript's with modules.js.
There are several ways to get the same functionality as <script> tags and global variables littered throughout your web page, least among which being that you can fall back to writing a module that adds the module loader to the window object so your other scripts can use it.
Take the high road and separate your behavior completely from your presentation and content. You can use CSS selectors to determine where you bless DOM elements with new behaviors, or XML name-spaced attributes like this example for client side includes.
<script src="modules.js?csi.js"></script> <div xmlns:csi="http://cixar.org/javascript/csi" csi:src="loadMe.html"/>
Another fun solution is to visit all script tags that have one language other than "javascript" and evaluate their innerHTML or XML CDATA content as JavaScript except inside a module context. Browsers will ignore scripts if they don't recognize the language.
<script language="chironscript">
include('base.js');
include('browserBase.js');
include('widget/scroll.js');
$('a', 'b').each(Scroll);
</script>
Alternately, a script could visit the DOM and use a name-spaced URL with an anchor to call a function on the DOM element to imbue it with behaviors.
<div xmlns:chiron="http://cixar.com/javascript" chiron:type="widget/marquee.js#Marquee"></div>
Also mind that, as a byproduct of not having a global variable for the module loader, each time that you include modules.js in a page will create a new instance of the module loader.
by Kris Kowal with thanks to Mike Stone, Ryan Ernst, and Ryan Paul for proofreading various drafts and incarnations of this document
Copyright © 2004-2007 Kris Kowal. MIT License.