There is no doubt that managing dependencies and modules with JavaScript has been a hot topic lately. As web apps use more and more JavaScript, libraries like LABJS (used by Twitter) and RequireJS seem to be getting a lot of attention – as they should, they rock.

The problem that most developers are trying to solve before turning to one of these libraries are:

When we were trying to solve some of these problems for 7shifts, we turned to the YUI3 Loader. I feel like it hasn’t gotten very much attention due to the lack of examples for loading custom application-specific assets.

I’m going to explain in detail how we’re managing dependencies in 7shifts.

Setup

First, we’re going to create a file where we outline all of our JavaScript and CSS modules (and their dependencies).

I’m going to assume your application has a layout like this:

webroot
    |-- js
    |    `-- vendors
    |-- css
    |    `-- vendors
    |-- img
    `-- index.html

Create a file called App.Modules.js in your js folder. Use your own application namespace. I’m just using App for the sake of this explanation.

Inside App.Modules, we’re going to write an object literal containing all of our modules. Although we’re not calling YUI’s addModule function directly, here are a list of options that will correspond to the object setup below.

var App = App || {};
App.Modules = {
    // Stylesheets
    css: {
        "jquery.tooltip.css": {
            path: "vendors/jquery.tooltip.css",
            // YUI automatically assumes our modules are all js files, UNLESS we specify otherwise. 
            // So here is where we specifically give it a type: 'css'
            type: "css"
        }
    },
    // JavaScripts
    js: {
        "App.User": {
            path: "App.User.js"
        },
        "App.Availability": {
            path: "App.Availability.js",
            requires: ["highcharts", "jquery.tooltip"]
        },
        // Vendors
        "highcharts": {
            path: "vendors/highcharts.js"  
        },
        "jquery.tooltip": {
            path: "vendors/jquery.tooltip.min.js",
            requires: ['jquery.tooltip.css']
        }
 
    }
}

With all of the files defined in this modules file and created, we can now assume that our directory structure looks like this:

webroot
    |-- js
    |    |-- App.User.js
    |    |-- App.Availability.js
    |    `-- vendors
    |             |-- highcharts.js
    |             `-- jquery.tooltip.min.js
    |-- css
    |    `-- vendors
    |            `-- jquery.tooltip.css
    |-- img
    `-- index.html

Now that we’ve got our modules setup, we can jump over to index.html (or whatever is rendering your global html layout).

Before I go any further, I want to let you know that we’re using the CakePHP framework with a PHP Minify vendor. Below you’ll notice that some paths might not be setup in your specific application (min-js and min-css folder). I describe how to setup PHP Minify in part 2 of this post.

Including YUI

Now we need to actually add YUI, the YUI Loader component and our App.Modules.js file. At the very bottom of your page, right before the closing body tag, add this:

<!-- Include YUI -->
<script type="text/javascript" src="js/App.Modules.js"></script>
<script type="text/javascript" src="http://yui.yahooapis.com/combo?3.2.0/build/yui/yui-min.js&3.2.0/build/loader/loader-min.js"></script>
<script type="text/javascript">
    // Store a new YUI instance on our App namespace
    App.Loader = YUI({
        charset: 'utf-8',
        timeout: 10000,
        combine: true,
        // Since we're concatenating all of our JS files via a minify library (PHP Minify), 
        // we want to separate them by a , instead of a & sign.
        // YUI automatically assumes that we want /min-js?f=js/App.User.js&js/App.Availability.js 
        // etc, but in reality, we want ?f=js/App.user.js,js/App.Availability.js
        filter: {
            'searchExp': "&",
            'replaceStr': ","
        },
        groups: {
            css: {
               // This gets appended to the "path" field for each one of your modules in App.Modules.js
                root: 'css/',
               // Load in our object literal defined in our App.Modules.js file
                modules: App.Modules.css,
                combine: true,
               // This comboBase url is using the PHP Minify library to take in
               // a bunch of comma separated css files and minify them into one request
                comboBase: '/min-css?f='
            },
            js: {
                root: 'js/',
                modules: App.Modules.js,
                combine: true,
                // This is doing the same as the comboBase above, only the URL is
                // slightly different for JS files. This makes sure that whenever one of 
                // our JS modules requires (depends on) a css module, that it uses a
               // separate combo-base (min-css above). This comboBase is only
               // for concatenating JS files
                comboBase: '/min-js?f='
            }
        }
    });
 
</script>

Awesome. So we’ve got our modules defined and YUI Loader setup and ready to go. Now what?

I’m glad you asked. Now we want to be able to call a module and have it load (along with it’s dependencies). To do that we can call our newly created YUI instance App.Loader.

// Calling the App.Availability module will produce the following two HTTP requests:
// min-css?f=css/vendors/jquery.tooltip.css
// min-js?f=js/vendors/highcharts.js,js/vendors/jquery.tooltip.min.js,js/App.Availability.js
// A bunch of files are minified and included before App.Availability.js due to our dependency
// chain defined in the App.Modules.js file for App.Availability
App.Loader.use('App.Availability', function(){
    alert("App.Availability and all it's dependencies have been loaded!");
});

So assuming you have a combo handler (PHP Minify) set up on your end (under the directories min-css and min-js), this will combine and return your minified and gzip’d assets.

You can pass as many arguments to the .use() function as you want. The arguments must all be module names and the last parameter is always the callback function that get’s fired when all of the dependencies have been loaded. You can see the .use() function in action here.

The Problem

What if you want to call App.Loader.use() in your included files or anywhere on the fly? Go ahead, try. It will break. The reason it breaks is because you’re trying to call it before it’s defined. Since we have our YUI instance being created right before the closing body tag, it’s going to make it difficult to call scripts on the fly.

The Solution

Load modules into a queued array before App.Loader is defined, then fetching the queued scripts and looping through them after App.Loader has been defined. Basically, having something that can add to the queue and fetch from the queue. Create a file called App.js and place it in your js directory. Place the code below in that file. Now include that file in the head of your document using a script tag.

var App = App || {};
App.Scripts = (function(){
 
    var __scripts = [];
 
    return {
        queue: function(){
            __scripts.push(Array.prototype.slice.call(arguments));
        },
        fetch: function(){
            return __scripts;
        }
    }
 
})();

App.Scripts includes code that adds to the queue and fetches from the queue. Now it’s time to fetch from the queue after we’ve loaded in YUI and defined our App.Loader YUI instance. After App.Loader = YUI({…. add this:

// Fetch all the scripts from a queue
var scripts = App.Scripts.fetch();
if(scripts.length){
    for(var i = 0, l = scripts.length; i < l; i++){
        App.Loader.use.apply(App.Loader, scripts[i]);
    }
}

The code above will loop through any queued modules and call our App.Loader.use() function on them. Since this snippet of code is being included after our App.Loader is defined, we won’t get any “App.Loader has not been defined” errors.

Now it’s time to actually use the queue.

Calling App.Scripts.queue() will allow us to build an array of queued modules and have them execute them after our App.Loader has been defined. You can use it exactly how you would use the App.Loader.use() function above:

<script type="text/javascript">
    // This can be called before App.Loader (YUI instance) has been defined.
    App.Scripts.queue('App.Availability', function(){
        alert("Availability and all of it's dependencies have been loaded!");
    });
</script>

By using a technique like this, you will without a doubt, reduce the amount of HTTP request that you’re sending.

Here is a before/after of HTTP requests on 7shifts:

View and download this entire example from Github

The other half of this tutorial is tying PHP Minify into all of this. Read http://7shifts.com/blog/using-php-minify-in-cakephp/.

Found this some-what interesting? Maybe you’ll like my tweets! Follow me.

Special thanks to David Mosher to introducing me to YUI.