YUI Loader: Managing custom JavaScript & CSS dependencies
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:
- Minimizing the amount of HTTP requests being sent.
- Wanting to have JavaScript files modular and load in any necessary dependencies.
- Wanting to load JavaScript without blocking.
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.

Nice writeup. 2 thoughts:
Wouldn’t this combo file technique ensure that your js never gets cached across your site and instead gets cached on a combination basis?
That combiner certainly prevents you from utilizing a CDN as well am I correct?
@Adam Crabtree: It only ensures that it’s cached on a combination basis. It’s a trade-off IMO. You can either make more HTTP requests and cache your JS or you can combo them into one request that will get cached and risk it re-caching an already requested JS file (in the combo request). But if you’ve broken your scripts up to only be included when needed (page-specific), chances are you’re requesting new files anyway.
Some people prefer having a build-time process to concat the scripts – this would probably be a better route than I described.
I think in general, you’ll just have to see what works best for your application. If it’s small and doesn’t contain much JS, it might be worth it to have the build-time process combine all your files into one .js file that you request.
You could definitely use a CDN with this technique. You would just need to set the comboBase property to the domain (CDN) of your choice. In this example I’m just using a comboBase relative path. You could do something like comboBase: “http://cdn.yoursite.com/min-js?f=”
[...] I’ve had my head buried in 7shifts lately, it’s been fun and exhausting to say the least. One of the obstacles we had to overcome was asset loading. We kicked this problem in the ass, read my full write-up over at the 7shifts developer blog. [...]
Interesting, I’d heard that YUI did something like this with there CDN but didn’t really understand it exactly. I take it that dynamic combination does cause too much of a hit to the latency, though I’d be curious about exact numbers.
I’m actually in the process of building something that’ll solve all your tradeoffs. It’s part FOSS library and part service. I’ll try and update you when I make the official beta announcement later this month.
Hit me up at dude at noderiety period com. I’d be curious to talk to you more about this.