Transparency on Demand

Richard presents a technique for loading JavaScript libraries transparently at the point of need.


October 10, 2007
URL:http://www.drdobbs.com/architecture-and-design/transparency-on-demand/202401087

Richard is an independent software developer, trainer, and consultant based in London. He can be contacted via www.dodeca.co.uk.


The "on-demand" approach to deploying JavaScript applications incrementally confers great benefits, but nontrivial use can pollute functions with expressions that are unrelated to their essential nature. Aspect-Oriented Programming (AOP) can resolve this problem completely; yet AOP in JavaScript is an unexplored area of web development. In this article, I show the need for such techniques in web development, and explain the principles of function interception in JavaScript. In short, I present an elegant technique for loading JavaScript libraries transparently at the point of need.

JavaScript Library Inclusion

JavaScript programs are associated with HTML documents by means of script elements where, in the simplest case, opening and closing <script> tags delimit the JavaScript syntax embedded within a page. However, many pages may require the same functionality, and this mandates placing the relevant code in discrete files that bear a .JS extension. Such libraries can then be pulled into the execution environment by specifying the file name as the value for the src attribute in an empty <script> element; see Listing One.


<html>
   <head>
      <script type = "text/javascript" 
              src  = "MyLib.js"></script>
   </head>
   <body> ... </body>
</html>
Listing One

JavaScript's interpreted nature aside, this is identical to the #include mechanism supported by C/C++, but the Pareto principle (en.wikipedia.org/wiki/Pareto_principle) also indicates the seeds of trouble. Generally, 90 percent of a program's runtime is spent executing only 10 percent of its code, implying that functionality should be loaded into the execution environment only when needed lest resources be consumed redundantly. Compiled applications accommodate this by using DLLs, but without similar intervention a client-side JavaScript application can download large tracts of code that are never actually executed.

Such inefficiency should be avoided assiduously in web deployment; it consumes bandwidth unnecessarily, increases download times, and stresses both server and client needlessly. The poor performance that results can render an otherwise competent application unpopular if not unusable, and this concern can only grow as web developers become ever more adventurous in their use of JavaScript.

JavaScript On Demand

The only solution to this is to adopt a "lazy" approach to loading, otherwise known as "on-demand JavaScript," where code is retrieved from the server as the need arises. This can circumvent the redundancy problem, as only a fraction of an application need be downloaded initially, with additional functionality being acquired as necessary. This avoids the redundant consumption of bandwidth, expedites delivery, and reduces the stress on both server and client. There are two principal routes to implementing the technique:

These techniques yield the same net result, but choosing one over the other presents certain tradeoffs. On the one hand, the script-tag technique is asynchronous, whereas XHR allows synchronous as well as asynchronous requests. In contrast, the script-tag technique allows retrieval of code from anywhere, whereas XHR allows requests only to the originating domain.

Noncohesion

Obviously, the mechanics of these techniques should be implemented within discrete functions, thus reducing the act of loading a resource to a simple call. However, the optimum designs for complex and powerful applications demand a degree of granularity to match—large programs are often split into many small modules, and in this case loading on demand can cause code to become littered with extraneous calls. Listing Two demonstrates this, where LoadLib_STH loads a library using the script-tag approach and LoadLib_XHR uses the XHR-based technique (LoadStylesheet is self-explanatory).


function DisplayMenu (...)
   {
   if (...) { LoadLib_XHR ("..."); }
   else     { LoadLib_XHR ("..."); }
   LoadStylesheet ("...");
   LoadLib_STH    ("...");
   // Code for displaying a menu   
   } 
function LoadLib_XHR    (LibName) { ... }
function LoadLib_STH    (LibName) { ... }
function LoadStylesheet (LibName) { ... }
Listing Two

In this example, using on-demand techniques necessitates populating DisplayMenu with calls to load code that will be required subsequently; yet those calls are extrinsic to the business of displaying menus. The loading syntax is therefore disjunctive, and this degrades the functional cohesion (en .wikipedia.org/wiki/Cohesion_%28computer _science%29) of the code, thus harming readability and comprehension. In other words, the various concerns (or "aspects") of the system cut across each other and are therefore "entangled."

Moreover, functions like DisplayMenu bloat the application, thereby decreasing efficiency. Furthermore, the relevant resources need be retrieved only once, which means that repeated calls to DisplayMenu will cause redundant calls to the loading functions—those calls serve their purpose just once before becoming deadweight that impinges on performance. All of these points run contrary to the very aim of loading on-demand, which is to pay only for what is used.

Function Interception

Happily, these problems can be resolved decisively. All objects in JavaScript are implemented as associative arrays that hold name-value pairs, meaning that the interpreter accesses a given object-member by using the member's name as a key with which to look up the associated value. Where the member-name corresponds to a method, the associated value is a reference to the body of that method, and it follows that calling the method causes the interpreter to retrieve the relevant reference, whence it executes the function body. Figure 1 illustrates this.

Figure 1: Normal object-method binding.

The value that is associated with a given member name can be changed using surprisingly simple syntax, and this means that the reference to a given object-method can be replaced with a reference to a function that acts as a proxy. This will cause calls to the original method to be "intercepted" by the proxy, which can do whatever it likes thereafter; see Listing Three.


var MyObj =                // Define the object literally
   {
   MyMethod : function ()   // Define a method
      { 
      ...
      }
   };
function Proxy () { ... }  // Define our interloper
MyObj["MyMethod"] = Proxy; // Replace the reference 
 ...
MyObj.MyMethod ();         // Unbeknownst to the caller,
                          // Proxy will now execute 
Listing Three

Cohesion Restored

This in hand, the cohesion problems of loading on demand evaporate. If the proxy calls a library-loader before invoking the original method-body, then calls to the intercepted method will trigger a download automatically and transparently, followed by execution of the method itself. Figure 2 depicts the essential concept.

[Click image to view at full size]

Figure 2: Function interception.

Critically, this technique permits complete separation of the code that creates an interception from the "interceptee," thus removing adventitious syntax from the points at which library loading is employed, and allowing all interception-setting code to be located in one place. This disentangles concerns and restores cohesion, while conserving the benefits of loading on demand, thus yielding the JavaScript equivalent of DLLs; see Listing Four.


function LoadLib_XHR (LibName) { ... }
function MyFunc () { MyOtherFunc (); }
function Proxy ()
   {
   LoadLib_XHR ("MyLib.js");           // Load synchronously using XHR
   this["MyFunc"] = OriginalRef;      // Replace the ref here in case
                                    // MyFunc is recursive
   return OriginalRef.call (this, arguments);
   }
var OriginalRef = this["MyFunc"];
this["MyFunc"] = Proxy;               // Intercept is set at a distance
                                   // from clients of MyFunc
  ...
MyFunc ();                          // Call routed through Proxy,
                                   // library is loaded transparently
  ...
MyFunc ();                         // Call is now entirely conventional
// -- Contents of MyLib.js ------------
function MyOtherFunc () { ... }
Listing Four

After calling the library-loader, the proxy restores the reference in the owner-object to its original value. This precludes needless performance-degradation subsequently, thus ensuring that we pay only for what we use. Note also that the proxy completes this step before invoking the interceptee, otherwise recursive calls would cause it to reexecute redundantly. Furthermore, note that JavaScript functions are objects in the "class" sense, and support a method called "call." Using this, the proxy can pass its arguments array to the original function, thus passing on any arguments supplied by the interceptee's caller and preserving transparency.

Optimum Implementation

This technique can be generalized to allow the arbitrary interception and loading of methods and libraries respectively, and employing a function that contains an inner function is the optimum approach here. In this scheme, the outer function takes arguments denoting the interceptee, its owner-object, and the name of the library to be loaded; on execution, it replaces the interceptee-reference in the owner-object with a reference to the inner function.

This forms a closure (and what a superb use for those exotic creatures) that makes the outer function's parameters available to the inner function when it executes. It follows that calling the interceptee invokes the inner function, which calls the library-loader before replacing the reference to itself with the original reference to the interceptee. Listing Five (available online; see "Resource Center," page 5) demonstrates this.

AspectJS

The reality is that function interception in JavaScript constitutes a form of meta-programming in an interpreted language, and has many aspect-oriented applications beyond loading on demand. Given that wider context, you can view a call to a library-loader as a "prefix" to the interceptee, and this introduces the notion of functions that act as "suffixes," where a method is called after, rather than before, the invocation of the interceptee.

This leads to the notion of "wrapping" an interceptee with such affixes, which leads from there to the question of affix lifetime. In the on-demand example, the prefix executes just once before it is detached, but there may be instances where an affix should execute for a certain number of times, or even infinitely; the ability to remove an affix explicitly is also desirable. Moreover, multiple affixes may be desirable—in Listing Two there are three loading calls, which suggests applying three discrete prefixes to DisplayMenu, and this introduces the notion of changing their execution order if needed. Then there is the question of exception handling, error checking, and diagnostics, as well as performance, all of which raise significant design questions.

A clear case exists, therefore, for a general-purpose function-interception library that addresses these points, thereby obviating re-invention of the wheel; and AspectJS (www.dodeca.co.uk/ajs_About_AspectJS.htm) is one such resource that is freely available. Encapsulating the mechanics of function interception, it allows (among other things) unlimited numbers of affixes for a given function, with fine control over their type, number, longevity, and execution order. In deference to a venerable tradition, therefore, Listing Six (available online) demonstrates loading a JavaScript library transparently on demand using AspectJS.

This uses the AddPrefix method of a singleton object called AJS to set up the interception. The first two parameters signify the OnMouseClick method of the global object as the interceptee, and the third specifies LoadLib_XHR as the prefix. The fourth is an optional parameter that is passed to the prefix—in this case the name of the library to be loaded—and the final argument specifies that the prefix should be removed after just one execution. The net result is that clicking on the client area of the browser's window causes Hello.js to be loaded, thus allowing a customary greeting to be displayed.

An Open Door

Resolving the downside to on-demand JavaScript lets you exploit its full potential, and generalizing the solution opens the door to a wealth of aspect-oriented techniques. Consolidating the principles of function interception in a reusable library lets you affix methods arbitrarily to virtually any function call. This permits the serious application of AOP techniques to professional web development, thus allowing remote tracing, debugging, and error reporting, along with client-side performance measurement, user-interaction modeling, and others that, as yet, are only a twinkle in the developer's eye.

Terms of Service | Privacy Statement | Copyright © 2024 UBM Tech, All rights reserved.