Applications

On-Demand Loading - Using AspectJS to Load Code as Needed and Transparently

The principles of good software-design hold that methods should do one thing alone, rather than conflate disparate functionality. Yet the conventional approach to 'on-demand' JavaScript commits that very sin by polluting methods with code for retrieving resources from the server.

Ideally, on-demand loading should occur without such disjunctive clutter, and this page explains how to employ AspectJS to this effect. For a detailed explanation of the rationale behind the techniques explored below, see the article entitled Transparency on Demand.

Note that the examples below also use loadJS_STH, loadJS_XHR and the XMLHTTPRequest library, each of which form part of the AspectJS distribution set and which are covered in detail elsewhere on this site.

Core Principle

Transparent on-demand loading pivots on the notion that calls to retrieve code from the server can be removed from functions that make such calls, and can be applied instead as prefixes to those functions.

In other words, invocation of a given function can be intercepted, such that a call to a library-loader executes first, followed by execution of the interceptee. This removes disjunctive syntax from the interceptee, thus restoring its cohesion.

Critically, a given library need be retrieved only once, as the code it contains remains in the execution environment until a new page is loaded. AspectJS gives control over affix lifetime, therefore a 'loading prefix' can be set to execute just once before it is removed, thus giving maximum efficiency.

Further to this, all prefix-related code can be placed in a library that is separate from all other code in the system, and this underlines the essential tenet of Aspect Orientation, which is that non-related, but cross-cutting concerns can be separated out, thus aiding the implementation and development of a system.

Loading JavaScript Asynchronously

Loading JavaScript transparently, on demand and asynchronously is a simple matter of attaching the function called loadJS_STH as a prefix to the method(s) that one wishes to trigger the download.

This can be seen in Example 1, where the call to AJS.addPrefix stipulates the global method called onMouseClick as the interceptee, and the inline function definition that calls loadJS_STH forms the prefix.

The fourth argument is of no matter here, hence it is null, and the fifth argument of '1' denotes that loadJS_STH should execute just once before being detatched. This means that subsequent clicks on the body element will result in just a conventional call to that function.

Note that the asynchronous nature of the call means that onMouseClick should not attempt to call functions in the library directly, because network latency means that the code is unlikely to have been loaded at the time of such calls.

Instead, the library contains a call to sayIt at its very end, which will execute only once the library has been imported fully. Given that the call onLibLoaded resides at the end of the file, the fact of its execution implies that it is safe to access functions and any data that may be contained in Hello.js. See the loadJS_STH tutorial for a deeper exploration of this issue.

Clearly, however, this is rather awkward, therefore the preferable, if more risky alternative is to load the desired code synchronously, as the next section demonstrates.


 <!-- Example 1 -->

 <html>
    <head>
       <script type = "text/javascript" src = "LoadJS_STH.js"></script>
       <script type = "text/javascript" src = "AJS.js"       ></script>
       <script type = "text/javascript">

      AJS.addPrefix (this,
                    "onMouseClick",
                     function ()
                        {
                        loadJS_STH ("Hello.js");
                        },

                     null,
                     1);

      function onMouseClick ()
         {

         // ...

         }

       </script>

    </head>

    <body onclick = "onMouseClick ()"> ... </body>

 </html>

 // -- Contents of Hello.js ------------

 function sayIt () { alert ("Hello World"); }

 sayIt ();

 --------------------------------------

 Output:

 Hello World
            

Loading JavaScript Synchronously

To load JavaScript transparently, and on demand, but synchronously, the principle is the same as that explored in the above section, and requires only a minor change to the parameters that are passed to addPrefix.

Example 2 illustrates this by making the prefix that is attached to onMouseClick call loadJS_XHR rather than loadJS_STH.

Here, there is no need for the call-back technique used in the asynchronous example shown above, as the interceptee will not execute until the library has been loaded. Given this, it is safe for onMouseClick to invoke sayIt itself.

In cases where the transaction with the server may complete unsuccessfully, the application can supply an error-handling call-back function that loadJS_XHR will invoke, and this error handler can then throw an exception, thus preventing onMouseClick from proceeding.


 <!-- Example 2 -->

 <html>
    <head>
       <script type = "text/javascript" src = "XHRFactory.js"></script>
       <script type = "text/javascript" src = "LoadJS_XHR.js"></script>
       <script type = "text/javascript" src = "AJS.js"       ></script>
       <script type = "text/javascript">

       AJS.addPrefix (this,
                     "onMouseClick",
                      function ()
                         {
                         loadJS_XHR ("Hello_2.js", XHRFactory.createXHRWrapper ());
                         },

                      null, 1);

       function onMouseClick ()
          {

          sayIt ();

          }

       </script>

    </head>

    <body onclick = "onMouseClick ()"> ... </body>

 </html>


 // -- Contents of Hello_2.js ------------

 function sayIt () { alert ("Hello World"); }

 --------------------------------------

 Output:

 Hello World
            

Multiple Loading-Prefixes

Clearly, there are times when multiple interceptees will require the loading of multiple libraries. One approach here is to load those libraries from within a single prefix that is attached to all the interceptees concerned. This is perfectly adequate where those interceptees all require retrieval of the same code.

However, where they share common requirements, while having other specific needs, this tactic will only introduce inefficiencies. This is because a call to a given interceptee will cause some libraries to be downloaded that it does not actually need, and which are never used subsequently by other interceptees because they are never invoked by the action's of the user concerned.

The next listing illustrates this using loadJS_XHR, although the same would apply were loadJS_STH employed. Here Func_A requires the presence of Lib1_Func and Lib2_Func (to be found in Lib1.js and Lib2.js respectively), whereas Func_B requires Lib1_Func and Lib3_Func (the latter of which resides in Lib3.js).

Prefixing both Func_A and Func_B with a call to a library loader means that a call to, say, Func_A will cause all three libraries to be downloaded. This is fine as long as Func_B is called subsequently. If not, however, the download of Lib3.js will be turn out to be redundant because Func_A requires nothing contained in that library.


 function loadLibs ()
    {
    loadJS_XHR ("Lib1.js");
    loadJS_XHR ("Lib2.js");   // This load will be redundant if Func_B is called
    loadJS_XHR ("Lib3.js");   // ...and this will be redundant if Func_A is called
    }

 AJS.addPrefix (this, "Func_A", loadLibs, null, 1);
 AJS.addPrefix (this, "Func_B", loadLibs, null, 1);

 ...

 function Func_A ()
    {
    Lib1_Func ();
    Lib2_Func ();
    }

 function Func_B ()
    {
    Lib1_Func ();
    Lib3_Func ();
    }


 // -- Contents of Lib1.js -------------

 function Lib1_Func () { alert ("Lib1_Func"); }


 // -- Contents of Lib2.js -------------

 function Lib2_Func () { alert ("Lib1_Func"); }


 // -- Contents of Lib3.js -------------

 function Lib3_Func () { alert ("Lib1_Func"); }
            

One solution might be to apply multiple prefixes to the interceptees in question, where each prefix loads just one library, thus allowing the downloads that a given method triggers to be specified precisely. The next listing illustrates this, using the scenario in the previous listing.

Here Func_A and Func_B now have two prefixes each, namely loadJS_XHR. The two prefixes for Func_A cause the download of Lib1.js and Lib2.js, and the prefixes attached to Func_B cause the download of Lib1.js and Lib3.js.

However, this is not the solution (in this case) because, while execution of Func_A will cause the prefix that loads Lib1.js to be detached, The Func_B prefix that does the same thing will remain in place.

A better solution is required, and this is covered in the next section.


 // Contents of libraries same as in previous listing

 AJS.addPrefix (this, "Func_A", function () { loadJS_XHR ("Lib1.js"); }, null, 1);
 AJS.addPrefix (this, "Func_A", function () { loadJS_XHR ("Lib2.js"); }, null, 1);

 AJS.addPrefix (this, "Func_B", function () { loadJS_XHR ("Lib1.js"); }, null, 1);
 AJS.addPrefix (this, "Func_B", function () { loadJS_XHR ("Lib3.js"); }, null, 1);

 ...

 function Func_A ()
    {
    Lib1_Func ();
    Lib2_Func ();
    }

 function Func_B ()
    {
    Lib1_Func ();
    Lib3_Func ();
    }
            

Managing Redundant Prefixes

The solution to the problem of redundant prefixes is to keep track of those that download a given library, and remove all of them on execution of just one of their interceptees. This suggests the possibility of a small utility that is capable of managing this process, and such a resource is indeed possible.

The listing (which is also provided as part of the AspectJS distribution set) defines a small factory-function called createLoadingMgr ('Create Loading Manager') that generates 'loading manager' objects, and which works as described below.

Assuming that there is a library that should be loaded on invocation of any one of a set of methods, client code should call createLoadingMgr just once, passing the name of that library, and a reference to the XHRFactory object. In response, the function will return a 'Loading Manager' object, which subsumes an array that will hold Affix objects returned from calls to AJS.addPrefix, and which exposes just a single method called addLoadingPoint.

For each function in the application that should trigger the download of a given library (a 'loading point'), client code should call addLoadingPoint, passing a reference to the loading-point's owner object (which corresponds to an 'IOwner' argument in the AspectJS API). The client should also pass a second string-argument representing the name of the loading point.

When called, addLoadingPoint calls AJS.addPrefix to add a prefix to the method that the caller stipulates, and pushes the resulting Affix object onto the LoadingMgr's array. (The prefix being the the inner function that createLoadingMgr defines internally.)

Given this, a subsequent call to a loading-point method causes its prefix to execute first. This loads the required library, and then iterates through the array, popping each Affix object from the end of the set. As each Affix is removed, the code calls its remove method to detach the prefix from its interceptee, thus ensuring that future invocations of other loading points do not trigger a redundant loading-operation.

Note that the loop will also attempt to remove the prefix that performed the load (in other words that function that is executing there and then). This will fail silently because affixes cannot remove themselves or their siblings, but this is not a problem because each affix was applied originally with an execution count of 1. This means that the prefix concerned will detach automatically as soon as it has executed.


 function createLoadingMgr_XHR (URL, XHRFactory, onError)
    {
    var Prefixes = [];

    function loadIt ()
       {
       loadJS_XHR (URL, XHRFactory.createXHRWrapper (), onError);

       while (Prefixes.length > 0) { Prefixes.pop ().remove ("createLoadingMgr_XHR - loadIt"); }

       }

    // -----------------------------------------------------------------------

    return {

       addLoadingPoint : function (LPOwner, LPName, onError)
          {
          Prefixes.push (AJS.addPrefix (LPOwner,
                                        LPName,
                                        loadIt,
                                        onError,
                                        1,
                                       "createLoadingMgr_XHR.addLoadingPoint"));
          return true;

          }

       };

    }
            

Obviously, the loading manager depicted above uses loadJS_XHR, but libraries can be loaded using the Script-Tag Hack, therefore a loading manager implementation that uses loadJS_STH is presented here as well (and is also included in the AspectJS distribution set).

Note that both these functions return true in order to allow them to operate simply as a call-back to findIn - this is explained fully below.

Additionally, note that the 'createLoadingMgr_XHR.addLoadingPoint' and 'createLoadingMgr_STH - loadIt' strings are 'call point' arguments that aJS_DbC and AJS_Logger employ in their respective outputs to inform developers of the location of (bad) calls to AJS.addPrefix (should you use those resources). This is also explored more fully below.


 function createLoadingMgr_STH (URL)
    {
    var Prefixes = [];

    function loadIt ()
       {
       loadJS_STH (URL);

       while (Prefixes.length > 0) { Prefixes.pop ().remove ("createLoadingMgr_STH - loadIt"); }

       }

    // -----------------------------------------------------------------------

    return {

       addLoadingPoint : function (LPOwner, LPName)
          {
          Prefixes.push (AJS.addPrefix (LPOwner,
                                        LPName,
                                        loadIt,
                                        null,
                                        1,
                                       "createLoadingMgr_STH.addLoadingPoint"));
          return true;

          }

       };

    }
            

Using a Loading Manager

Example 3 demonstrates using createLoadingMgr and a LoadingMgr object.

Here there are three functions, each of which should trigger the loading of a library called 'MyLib.js' when they are executed, but for which the loading prefix should be removed once the library has been retrieved. Given this, the code creates a LoadingMgr object by calling createLoadingMgr, passing it the name of the library.

It then calls addLoadingPoint for each load-triggering function, passing the name of the function and its owner-object. Subsequent calls to any one of the three functions will cause MyLib.js to be loaded, along with the automatic removal of the prefixes from each of the load-triggering functions.

Clearly, this example can be extended to the creation of arbitrary numbers of LoadingMgr objects, one for each library of interest, and each LoadingMgr can corral an arbitrary number of load-triggering functions.


 // Example 3

 // Contents of Hello_2.js same as in Example 2

 function myFunc_1 () { sayIt (); }
 function myFunc_2 () { sayIt (); }
 function myFunc_3 () { sayIt (); }

 var LoadingMgr = createLoadingMgr_XHR ("Hello_2.js", XHRFactory);


 LoadingMgr.addLoadingPoint (this, "myFunc_1");
 LoadingMgr.addLoadingPoint (this, "myFunc_2");
 LoadingMgr.addLoadingPoint (this, "myFunc_3");

 ...

 myFunc_2 ();

 --------------------------------------

 Output:

 Hello World
            

Managing Disparate Load-Triggering Functions

Clearly, situations could arise where a given library has many load-triggering functions that are distributed across the application. In this case, explicit management of a loading manager could become tedious, time consuming and error prone.

It is at this point that findIn comes into its own, because it allows the application of loading prefixes, through a loading manager, using just wildcards, and this is demonstrated in Example 4.

Here a hypothetical and entirely fictitious object is defined as having three children, each of which has a method called 'someMethod'. The aim is to apply loading prefixes to each instance of someMethod, such that invocation of any one of them will cause the loading of Hello_2.js, and the automatic removal of the prefixes.

To acheive this, the code calls findIn, specifying MyObj as the object to search, and 'someMethod' as the search string. It also specifies addLoadingPoint as the 'onFound' call-back argument that findIn accepts. Given these arguments, findIn searches the object for every instance of 'someMethod', and invokes addLoadingPoint for each one that it locates.

MyObj.Child_2.someMethod is then invoked, which causes the loading of Hello_2.js, and the removal of all the loading prefixes, after which someMethod avails itself of the code in Hello_2.js.

Here it can be seen why addLoadingPoint returns true. Every time findIn locates a method that fits the search criteria, it invokes any call-back that its caller supplied. If the call-back returns true then findIn continues its search. Making addLoadingPoint return true means that findIn will continue searching, and will therefore cause the application of loading prefixes to every loading point in a given object structure.

Gratifyingly, this also allows us to keep code-verbiage to a minimum. This is because the alternative would be to wrap addLoadingPoint in another function, and have that return true, which would constitute painfully needless inefficiency.


 // Example 4

 // Contents of Hello_2.js same as in Example 2

 var MyObj =
    {
    Child_1 :
       {
       someMethod : function () { sayIt (); }

       },

    Child_2 :
       {
       someMethod : function () { sayIt (); }

       },

    Child_3 :
       {
       someMethod : function () { sayIt (); }

       }

    };


 var LoadingMgr = createLoadingMgr_XHR ("Hello_2.js", XHRFactory);

 findIn (MyObj, "someMethod", LoadingMgr.addLoadingPoint);

 ...

 MyObj.Child_2.someMethod ();

 --------------------------------------

 Output:

 Hello World
            

Logging the Application of Loading-Prefixes

To pull all these threads together: Example 4 is an excellent demonstration of the AspectJS library and its accessories operating in concert, and goes so far as to combine four of the resources in the distribution set - to wit: the AJS object, loadJS_XHR, the XHRFactory object, and findIn.

However, using AJS_Logger in addition, to report calls to AJS-related methods demonstrates the high modularity of the product still further, and Example 5 does just this.

Here the code defines and passes two call-back functions to AJS_Logger, which direct logging output to the display device. Following that, the code performs the same steps as Example 4.

As a result, the three calls that the loading manager makes (at findIn's behest) to AJS.addPrefix are reported initially. The call to MyObj.Child_2.someMethod then causes invocation of the loadIt function within the loading-manager object.

This calls the remove method of each Affix object (generated originally by the calls to AJS.addPrefix), and these calls also appear in the logging trace output. Hello_2.js is then retrieved, after which execution continues through MyObj.Child_2.someMethod, which calls sayIt - a function than now exists happily at global scope.

This example could be taken a stage still further, by calling aJS_DbC before the call to AJS_Logger, whereupon logging activity would occur as before, whilst any incorrect calls that the application made to AspectJS methods would be trapped and reported.


 // Example 5

 // Contents of MyObj same as in Example 4

 function onMCall (Obj, Method, IArgs, CallPoint)
    {
    alert ("Called: "  + Obj + "." + Method + " - " + " CallPoint: " + CallPoint);
    }

 function onMRtn (Obj, Method, IArgs, IRtn, CallPoint)
    {
    alert ("Returned: " + Obj  + "." + Method + " - CallPoint: " + CallPoint);
    }

 AJS_Logger.enable (AJS, onMCall, onMRtn);

 var LoadingMgr = createLoadingMgr_XHR ("Hello_2.js", XHRFactory);

 findIn (MyObj, "someMethod", LoadingMgr.addLoadingPoint);

 ...

 MyObj.Child_2.someMethod ();

 --------------------------------------

 Output:

 Called: AJS.addPrefix -  CallPoint: createLoadingMgr_XHR.addLoadingPoint
 Returned: AJS.addPrefix - CallPoint: createLoadingMgr_XHR.addLoadingPoint

 Called: AJS.addPrefix -  CallPoint: createLoadingMgr_XHR.addLoadingPoint
 Returned: AJS.addPrefix - CallPoint: createLoadingMgr_XHR.addLoadingPoint

 Called: AJS.addPrefix -  CallPoint: createLoadingMgr_XHR.addLoadingPoint
 Returned: AJS.addPrefix - CallPoint: createLoadingMgr_XHR.addLoadingPoint

 Called: Prefix.remove -  CallPoint: createLoadingMgr_XHR - loadIt
 Returned: Prefix.remove - CallPoint: createLoadingMgr_XHR - loadIt

 Called: Prefix.remove -  CallPoint: createLoadingMgr_XHR - loadIt
 Returned: Prefix.remove - CallPoint: createLoadingMgr_XHR - loadIt

 Called: Prefix.remove -  CallPoint: createLoadingMgr_XHR - loadIt
 Returned: Prefix.remove - CallPoint: createLoadingMgr_XHR - loadIt

 Hello World