JS.Packages
JS.Class, as of version 2.0, comes with a package system that lets you load scripts
on demand with support for dependency management. Its lets you name objects and the URLs
for the files that contain them, so your application code only has to deal with knowing
which objects it needs, not where to find those objects. Where possible, JS.Package
will download scripts in parallel to improve load speed, and it makes sure interdependent
scripts are loaded in the correct order. When used in a web browser, it uses script tag
injection rather than XMLHttpRequest, so it works cross-domain. It has also been tested
as a dependency manager on SpiderMonkey, Rhino and V8.
For example, say I want to do something with YUI’s CSS selector.
I just need to require it, and supply a function to run once all the requisite code
has loaded:
require('YAHOO.util.Selector', function() {
var myDivs = YAHOO.util.Selector.query('div.mine');
// .. more code (elided)
});
Setting up
Before you can use the require statement, you need the following loaded into the
page:
JS.Package, the dependency manager (package.js)- A list of dependencies
The first two are dealt with with a couple of script tags (or load() calls if using
a server-side platform). The third needs to be written out to describe all the other files
in your application and how they relate to each other.
Alternatively, use the loader.js file, described in more detail below.
The package listing
To describe your packages, you list the external script files used by your application, stating which JavaScript objects are provided by the file and which objects it depends on. For example, here’s a typical package definition for YUI:
JS.Packages(function() { with(this) {
var cdn = 'http://yui.yahooapis.com/';
var yui = cdn + '2.7.0/build/';
file(yui + 'yahoo-dom-event/yahoo-dom-event.js')
.provides('YAHOO',
'YAHOO.lang',
'YAHOO.util.Dom',
'YAHOO.util.Event');
file(yui + 'selector/selector-min.js')
.provides('YAHOO.util.Selector')
.requires('YAHOO');
file(yui + 'animation/animation-min.js')
.provides('YAHOO.util.Anim',
'YAHOO.util.ColorAnim')
.requires('YAHOO');
file(yui + 'dragdrop/dragdrop-min.js')
.provides('YAHOO.util.DD')
.requires('YAHOO',
'YAHOO.util.Dom');
file(yui + 'slider/slider-min.js')
.provides('YAHOO.widget.Slider')
.requires('YAHOO',
'YAHOO.util.Anim',
'YAHOO.util.DD');
}});
(Yes, YUI has its own loader utility, I’m just using this as a well-known example.)
Notice how two of the stated dependencies for YAHOO.util.DD are provided by the same
file – YAHOO and YAHOO.util.Dom. The package system will spot this and only loads
each file once. Object detection is used to figure out whether each file has been loaded
and a file is not requested unless some of its objects appear to be missing.
Where possible, the package system will attempt to load scripts in parallel where it
spots that execution order doesn’t matter. If the load order of a set of scripts is
important, you must make sure you make this clear using the requires() statement.
A file will not be requested until all the objects it requires are present.
In addition to requires(), there is a statement called uses() that specifies a
‘soft dependency’, i.e. an object the package needs but that does not necessarily
need to be loaded first. For example, HashSet uses a Hash
for storage but you could load Hash after the Set package just fine. On the other
hand, HashSet mixes in Enumerable and this must be loaded
before HashSet is defined. And, Hash is itself based on Enumerable. So the package
config for this might look like:
JS.Packages(function() { with(this) {
file('/js-class/enumerable.js') .provides('JS.Enumerable');
file('/js-class/hash.js') .provides('JS.Hash')
.requires('JS.Enumerable');
file('/js-class/set.js') .provides('JS.Set',
'JS.SortedSet',
'JS.HashSet')
.requires('JS.Enumerable')
.uses( 'JS.Hash');
});
The advantage of using uses() is that it helps the package system optimise the
downloading of packages, since if the load order does not matter the packages can
be downloaded in parallel.
Custom loader functions
Some libraries, such as the Google Ajax APIs, have their own systems for loading code on demand that involve more than simply knowing the path to a script file. Our package system allows you to specify packages that use a loader function rather than a path to load themselves; the function should take a callback and call it when the library in question is done loading. For example, here’s how you’d incorporate Google Maps into your library:
JS.Packages(function() { with(this) {
file('http://www.google.com/jsapi?key=MY_GOOGLE_KEY')
.provides('google.load');
loader(function(cb) { google.load('maps', '2.x', {callback: cb}) })
.provides('GMap2', 'GClientGeocoder')
.requires('google.load');
}});
The callback (cb) is a function generated by the package system that continues
to load and run dependent code once the custom loader has finished its work. If you
don’t call cb (or pass it to a function that will call it for you as above), code
that depends on this library will not run.
JS.Packages also provides post-load setup hooks that let you run some code after
a file loads. For example, a strategy for loading YUI3
might involve loading the seed file, creating a new global instance of the library,
then using YUI’s own loader functions to load further modules. Some sample code:
JS.Packages(function() { with(this) {
file('http://yui.yahooapis.com/3.0.0pr2/build/yui/yui-min.js')
.setup(function() { window.yui3 = YUI() })
.provides('YUI', 'yui3');
loader(function(cb) { yui3.use('node', cb) })
.provides('yui3.Node')
.requires('yui3');
}});
Loader functions can also be used to generate library objects that are expensive to
create without necessarily loading code from external files. Just remember to call cb
yourself when the generated object is ready:
JS.Packages(function() { with(this) {
loader(function(cb) {
window.ChocolateFactory = new WonkaVenture();
// Perform other expensive setup operations
cb();
})
.provides('ChocolateFactory');
}});
The JS.Class loader.js file
The JS.Class distribution comes with a file called loader.js. This contains the package
system and dependency information for all JS.Class components. This lets you load a small
file initially and download the rest of JS.Class into your app on demand. You must host the
library on your servers, but it can figure out where it’s being served from automatically to
load other packages. (Note: keep all the JS.Class files in one directory, as they
appear in the download, otherwise the loader will not be able to find them.) A simple
example:
<!-- In HEAD -->
<script src="http://example.com/js-class/loader.js" type="text/javascript">
</script>
<!-- Elsewhere in your app -->
<script type="text/javascript">
require('JS.SortedSet', function() {
// ...
});
</script>
You can also use this file on server-side platforms such as SpiderMonkey, Rhino or V8
but you must tell the library the path where it is stored as this information cannot be
determined automatically on these platforms. You do this using the JSCLASS_PATH variable,
for example:
JSCLASS_PATH = 'path/to/js.class/';
load(JSCLASS_PATH + 'loader.js');
require('JS.SortedSet', function() {
// do something with SortedSet
});
Note that as of version 2.1.4, loader.js does not contain the JS.Class core. If you need
JS.Class, JS.Module, JS.Singleton or JS.Interface you must specify these as
requirements using the require() function or by putting them in your JS.Packages
configuration.