LoadLibrary-like Implementation in Javascript
Download Links
- Full Source (3134 Bytes)
- Google closure compiled source (472 Bytes)
- Google closure compiled source without the error handling (324 Bytes)
- Google closure compiled source without the error handling and using jquery for the DOM (230 Bytes)
Background
In Windows programming, there is this nice routine called LoadLibraryEx. Usually when you compile an application, you have static libraries that get compiled in and then dynamic libraries that are loaded at start time. This is fine, unless you want some functionality and some library to be truly optional.
For instance, say that you have a $100 program that is designed to work with your enterprise level $1,500 program; but you don't want to require the $1,500 program to be on the user's machine in order to use the basic features of the $100 program. In fact, you don't want to give them a single line of code from the $1,500 program in order for the $100 one to work fine.
That's where LoadLibrary comes in. You can load a library on-demand. If the library fails to load, then you can run a different code path.
Ok, But This is Javascript
Sure is. There are two reasons that you want such a facility here:
- You have a large Javascript application and don't want to block while loading megabytes of code.
- You have special features and functions that you only want to export to certain users.
IE has offered a way to alleviate the first problem for a while. It's script tag sports a defer directive for lazy loading.
The Mozilla based browsers, observing this is a decent idea, started supporting it themselves recently.
There are other, more traditional implementations also. For instance, the code snippet that Google Analytics gives you to drop into your site uses a document.write to do deferred loading.
So it's Solved. What's Your Point?
No, not exactly. The first problem is solved, some of the time, maybe.
What I really need is something like follows:
function advanced(){ extra.func1(); extra.func2(); } // instead of calling advanced directly, like so // advanced(); // // I just want to do a little extra work loadLib('extra.js', advanced);
This pattern mimics my essential requirements for LoadLibraryEx as described above:
- If the library is inaccessible, then not only does the code not run, but some optional failure case code is run.
- The dependency code is loaded on demand, as opposed to just "not blocking". It's a more graceful and tighter solution.
Implementation
As it turns out, raw document.write can screw up jQuery selectors in Quirks modes of various browsers. I don't know why and I don't care because their are other ways of achieving this.
var loadLib = (function() { var // a wrapper div to hold our script tags wrapper, // wait 5 seconds to load the library timeout = 5000, // functions to be run if the library loads callbackMap = {}, // functions to be run if the library fails to load failureMap = {}, // a cache so we don't load the library more then once existMap = {}; var ret = function (file, callback, options) { // Check to see if we have loaded this library before. if(existMap[file]) { callback(); } // If this is our first time here, // we'll need to create the wrapper div // and inject it into the document.body if(!wrapper) { wrapper = document.createElement('div'); wrapper.setAttribute('class', 'lazyloader'); document.body.appendChild(wrapper); } // We create the script tag to inject below. // However, we don't load it quite yet. If it loads // too fast (unlikely), then our handlers won't be called. // // However, if we have too much overhead (very unlikely) // then our failure case may be called when it wasn't really // needed. Either way, this justifies the injection at the // last possible moment. var script = document.createElement('script'); script.src = file + '.js'; // add the callback to a list, if applicable if(! callbackMap[file]) { callbackMap[file] = []; } callbackMap[file].push(callback); // add the failure to a list, if applicable if(options && options.failure) { if(! failureMap[file]) { failureMap[file] = []; // We'll have timeout milliseconds to // request and load the script. I have a lenient // default of 5 seconds. You can put it as high // or as low as you want, depending on use. // // Or, as in the example, you may not feel obliged // to specify failure cases at all, in which case, // this doesn't apply. // // We drop the code path in here because we don't want to // accidentally register the setTimeout function more then // once. Even if we do, we are detroying the failureMap, // so this ought not be a problem. // // However, Array.prototype.shift is unlikely to be necessarily // atomic so it's best to avoid any potential resource issues that // may arise as a result of some likely future browser bugs. setTimeout(function() { // Exit if the library has been loaded in this // time period. if(existMap[file]) { delete failureMap[file]; return; } // execute the failure code in order while(failureMap[file]) { (failureMap[file].shift()) (); } }, timeout); } failureMap[file].push(options.failure); } // Here is where we inject into the DOM wrapper.appendChild(script); } ret.fire = function(file) { // Cache the library as loaded existMap[file] = true; // Delete the fail callbacks. // This is just a matter of memory // management. if(failureMap[file]) { delete failureMap[file]; } // Execute the success code in order while(callbackMap[file]) { (callbackMap[file].shift()) (); } } return ret; })();
Hopefully, the code above is understandable. It will append a <script> tag outside of the header (this works, regardless of the standard) in a special container called lazyloader.
Function Execution
In order to pull off execution, we could use custom events, but as it turns out, we like to make life easy. Pretend our code is called extra and is stored in extra.js as above.
var extra = (function(){ var pub = {}; pub.func1 = function() { ... } pub.func2 = function() { ... } return pub; })(); // It's important to run this code OUTSIDE // of the above the definition, because // the object won't be defined until the // inner code is executed, of course. loadLib.fire('extra.js');
Multiple Dependencies
I've tried to implement loadLib a few times, and this has been the one I've stuck with. It supports failure code, blocking until loading, etc.
You can do various tricks with chaining for multiple dependencies also. Look at this example below for a fairly trivial way of doing it:
function reallySpecific() { // This sentinal makes sure that the // code won't run until all libraries // are loaded if(!(self.a && self.b && self.c)) { return; } a.func(); b.func(); c.func(); } // Since things are asynchronous, we have no gaurantee // which will be the last to load. So we just run it // blindly every time and check there. loadLib('a.js', reallySpecific); loadLib('b.js', reallySpecific); loadLib('c.js', reallySpecific);