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:

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.