One of the most important features of modern web sites overall, is their ability to respond quickly and come back with the information requested by the user. We live in an “attention economy” on the Web 2.0, and if your site does not respond pretty much instantly, the user will find a site that does.
One way that modern sites achieve this, is by loading in smaller increments – for example, loading the detailed content for an item on a list can be deferred using AJAX.
Another way to break down the loading of a web application into smaller chunks, is by loading various other resources, specifically JavaScript and CSS files, on demand.
Doing this in a compatible and clean way, can be tricky.
The Challenge
Let’s look at some of the requirements that a good solution to this problem must fulfill.
First of all, some of the JavaScript frameworks we work with, do provide some sort of resource management. But we work with many different frameworks, and sometimes with no framework at all, so our solution needs to be stand-alone as well as compatible with most major JavaScript frameworks.
Secondly, we target all modern browsers – and therefore, our solution must be fully cross browser , targeting Internet Explorer 6 through 8, Firefox, Opera and Chrome, and hopefully any other standards compliant future browsers.
JavaScript is (or can be) object-oriented, meaning that a class in one script could potentially extend a class in another script. Therefore, since we are going to load resource asynchronously, we must ensure that scripts are loaded and executed in order .
Another concern is notification of resource readiness. Since resources are loaded asynchronously, and sometimes have already been loaded once, our script needs to provide a callback-notification when the resource is ready and available.
And finally, we don’t want to write code for every project to keep track of what’s been loaded and what has not. In other words, we need a load-once method, so that classes, widgets and stylesheets can be automatically loaded the first time they are needed.
Limitations
We’re going to accept certain limitations of this script.
For one, we’re not going to attempt to do a lot of error handling – if a resource can’t be loaded, this is a problem that needs to be solved by the developer, and not really something you can provide a “pretty” solution for anyway.
And secondly, some resource management scripts attempt to inject scripts in a way that allows scripts from foreign domains to run without security limitations. This has certain other drawbacks that I won’t get into, but we’re going to assume that you’re loading scripts from your own domain.
Our Solution
Our script comes in the form of a classless singleton object, which provides two methods for loading resources on demand:
Loader.load( url, [callback-function], [context-object], [driver-name] ); Loader.once( url, [callback-function], [context-object], [driver-name] );
The Loader.load() method will allow you to load the same resource repeatedly – for example, you will be able to execute the same JavaScript more than once. Just keep in mind that duplicate class declarations, and duplicate initialization, for example, could cause problems. Most likely, you will only want to use this method if you want to intentionally overwrite existing variables.
The Loader.once() method will ensure that the same resource is only loaded once. Note that the callback will be called when the resource is ready , not only the first time when the resource actually loads.
Arguments for the two functions are identical:
- url : required – relative (to your page) or absolute URL to a JavaScript or CSS resource.
- callback-function : optional callback-function – called when the resource (and any resources requested before it) has loaded.
- context-object : optional object to use as the context (this) for the callback-function. Use null (or leave out) if calling context is unimportant.
- driver-name : optional driver-name, e.g. “css” or “js” – this determines how the loaded resource is handled. If unspecified, the loader will try to determine the driver by file-extension, but will default to “js” if the file extension at the end of the URL does not match “.css” or “.js”.
Conclusion
When you need to load a collection of resources, remember that they will be loaded in the order you request them. For example, loading components for a widget-based framework such as ExtJS is possible – you can load a component class declaration, and then load another class that extends it, but you must request them in order.
Another thing to keep in mind, when you need to know when a collection of resources are ready, you don’t need to attach a callback to every request – just attach your callback-function to the last request, as this won’t execute until all previously requested resources are ready.
Source Code
Finally, here is the source code for you to cut and paste:
/*
Version: 1.1
Developer: Rasmus Schultz
License: GPL v3 <http://www.gnu.org/licenses/gpl-3.0-standalone.html>
Copyright 2009, Gorges Web Sites <http://www.GORGES.us>
Removing this notice from the source code would be bad karma.
*/
var Loader = {
q: [], // the Queue for pending items to be loaded
reg: {}, // a registry to ensure items are loaded only once
load: function(url, cb, context, driver, once) {
var dd = url.split('.').pop();
if (!Loader.drivers[dd]) dd = driver || 'js';
var p = new Loader.Proxy(
{ url: url, cb: cb || function(){}, reg: once, driver: dd, context: context }
);
Loader.q.push(p);
Loader.next();
return p;
},
once: function(url, cb, context, driver) {
this.load(url, cb, context, driver, 1);
},
next: function() {
for (i=0; i<Loader.q.length; i++) {
var l = Loader.q[i];
if (l.state == 1) return; // already loading
if (l.state == 0) return l.load(); // not loading (and not yet loaded)
}
}
}
Loader.Proxy = function(opt) {
this.driver = opt.driver;
this.context = opt.context || this;
this.url = opt.url;
this.reg = opt.reg;
this.state = 0; // inactive
this.cb = opt.cb;
this.load = function() {
if (this.reg &amp;amp;amp;amp;&amp;amp;amp;amp; Loader.reg[this.url]) return this.loaded(); // already loaded once
this.state = 1; // loading
var hd = document.getElementsByTagName("head")[0];
var el = Loader.drivers[this.driver](this, this.url + (this.url.indexOf('?') == -1 ? '?' : '&amp;amp;amp;amp;') + new Date().getTime());
hd.appendChild(el);
}
this.loaded = function() {
this.state = 2; // loaded
if (this.reg) Loader.reg[this.url] = 1;
this.cb.call(this.context);
Loader.next();
}
}
Loader.drivers = {
js: function(proxy, url) {
var el = document.createElement('script');
el.type = 'text/javascript';
el.src = url;
var me = proxy;
if (el.attachEvent) { // IE
el.attachEvent('onreadystatechange', function() {
if (el.readyState == 'loaded' || el.readyState == 'complete') me.loaded();
});
} else { // DOM
el.onload = function() { me.loaded(); }
}
return el;
},
css: function(proxy, url) {
var el = document.createElement('link');
el.rel = 'stylesheet';
el.type = 'text/css';
el.href = url;
el.media = 'all';
new (function(link, proxy){
this.index = document.styleSheets.length;
this.link = link;
this.proxy = proxy;
var me = this;
this.check = function() {
try {
var s = document.styleSheets[me.index];
if ((s.rules || s.cssRules).length) { // DOM || FF
window.clearInterval(me.int);
me.proxy.loaded();
}
} catch (e) {};
}
this.int = window.setInterval(this.check, 100);
})(el, proxy);
return el;
}
}

