Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
Ben Nadel at Scotch On The Rock (SOTR) 2010 (London) with: RichardCooper
Ben Nadel at Scotch On The Rock (SOTR) 2010 (London) with: RichardCooper ( @seopo )

Deferred Module Definition For 3rd-party Libraries In JavaScript

By Ben Nadel on

As I've been using 3rd-party tracking libraries in my JavaScript applications, I've noticed an emergent pattern that is kind of cool. The 3rd-party library will provide some sort of super light-weight "proxy" that can be consumed immediately while the actual library loads asynchronously. I've never used this approach myself, so I wanted to put together a little learning demo.


 
 
 

 
 
 
 
 

Run this demo in my JavaScript Demos project on GitHub.

Imagine that I have some sort of 3rd-party event-logging JavaScript library that gets loaded from a remote CDN (Content Delivery Network). Since important events can be triggered at any time during the lifecycle of an application, it might be important for an application to log events before the JavaScript library is loaded. To facilitate this, we'll define a light-weight proxy to the library that simply queues events in the time before the library is loaded. Then, when the library does finally load, those queued events will be flushed to the remote tracking service.

To see this in action, I've created an interval that logs an event every 1,000 milliseconds. It does this using a "logger" that is not fully loaded until the user explicitly load its (using jQuery's $.getScript() method):

  • <!doctype html>
  • <html>
  • <head>
  • <meta charset="utf-8" />
  •  
  • <title>
  • Deferred Module Definition For 3rd-party Libraries In JavaScript
  • </title>
  • </head>
  • <body>
  •  
  • <h1>
  • Deferred Module Definition For 3rd-party Libraries In JavaScript
  • </h1>
  •  
  • <p>
  • <a href="#" class="load">Load library</a>.
  • </p>
  •  
  • <script type="text/javascript" src="../../vendor/jquery/jquery-2.1.0.min.js"></script>
  • <script type="text/javascript">
  •  
  • // We need to give our 3rd-party library some sort of hook that the user can
  • // start consuming before the library is fully loaded. This will simply provide
  • // a log() method that will queue up the requests to be flushed at a later time
  • // when the 3rd-party library is loaded.
  • // --
  • // NOTE: I would NEVER put all the code on one line like this; but, 3rd-party
  • // libraries tend to provide copy / paste code in this nature to make it ?easier?
  • // for the user to integrate.
  • window.logger = []; window.logger.log = function() { this.push( arguments ); };
  •  
  • // ---
  • // Now that we have our logger "hook" in place, we can start using it before the
  • // library is actually loaded. To demonstrate, let's kick off an interval where
  • // we start recording the interval event.
  • // ---
  •  
  • var intervalCount = 0;
  •  
  • setInterval(
  • function intervalOperator() {
  •  
  • console.warn( "Interval", [] );
  •  
  • logger.log( "Interval Executed", ++intervalCount );
  •  
  • },
  • 1000
  • );
  •  
  • // ---
  • // We're going to wait to load the 3rd-party library until the user explicitly
  • // clicks the "load" button. Then, we'll asynchronously load the script and
  • // inject it into the page. At that point, the real library will "hydrate" the
  • // transient hook.
  • // ---
  •  
  • $( "a.load" ).click(
  • function handleClick( event ) {
  •  
  • return( $.getScript( "./logger.js" ) && false );
  •  
  • }
  • );
  •  
  • </script>
  •  
  • </body>
  • </html>

As you can see, when this page is loaded, the logger module is defined as an Array with a light-weight .log() method. This light-weight version is used in the setInterval() operator until such time that the library is explicitly loaded.

And, this is the 3rd-party library that gets loaded:

  • ;(function sandbox( win, doc, undefined ) {
  •  
  • "use strict";
  •  
  • // If the user has consumed the pre-load library hook, we have to make sure not to
  • // overwrite the reference as this may break the user's application.
  • var logger = win.logger = ( win.logger || [] );
  •  
  • // I log the given event with the optional meta-data.
  • logger.log = function( eventType, data ) {
  •  
  • console.info( "Event: [", eventType, "] with data [", JSON.stringify( data || "" ), "]." );
  •  
  • };
  •  
  • // ---
  • // ---
  •  
  • // Now that the 3rd-party library has been fully loaded, we have to flush the queue.
  • // Since this object is really an array at heart, we can just keep shifting items off
  • // the front and piping them through to the newly-defined .log() method.
  • while ( logger.length ) {
  •  
  • logger.log.apply( logger, logger.shift() );
  •  
  • }
  •  
  • })( window, document );

Notice that when the library goes to define itself, it makes sure not to overwrite the existing reference [if it exists]. This way, we won't accidentally break references, to the logger proxy, in the parent application.

Once the module is fully hydrated, we have to flush the queued events. Since the light-weight proxy was an Array that made use of the native .push() method, flushing the queue is a simple matter of shifting events off the "front" of the logger and piping them into the real .log() method.

If I run this page and wait a few seconds before loading the 3rd-party library, I get the following console output:


 
 
 

 
 Deferred modules provide light-weight proxy objects that can be consumed prior to library load. 
 
 
 

As you can see, the first several interval events result in no output - the events are just queued up. Then, once the 3rd-party library loads, the module is hydrated and the queued events are flushed. Going forward, all calls to .log() in the parent application are sent directly to the actual .log() method.

This is a fun approach! Of course, it probably only works with libraries that have a very small API; anything of significant size or logic would likely be too hard to proxy before loading. That said, for small 3rd-party libraries, this seems like a interesting idea.



Reader Comments

Post A Comment

You — Get Out Of My Dreams, Get Into My Comments
Live in the Now
Oops!
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.