On Demand Javascript Loading (with XMLHttp)

Some permalinks on this blog have recently changed. Are you sure you weren't looking for Converting to Pixels with Javscript?

Generally, when you want to link a javascript file in one of your HTML pages, you would do something like the following:


<script type = "text/javascript" src = "./myfile.js"></script>

This is fine in many cases, but what happens if you want to give your users the ability to customize the web page? Suppose you want your users to be able to add widgets and the logic for each of those widgets is cleanly separated into individual files? One option might be to simply include every javascript widget file that the user might want to invoke. But that's going to increase page bloat when (in many cases) it isn't necessary. Wouldn't it be better if we waited until the user requested a widget and then dynamically loaded the necessary javascript to create that widget?

Through some innovative use of XMLHttp, we can achieve exactly this. Read on to find out how.

The first thing we'll want to do is create a "namespace" to house our javascript functions. I'm envisioning some sort of XMLHttp wrapper, along with a function to test if a file actually exists on the server, as well as the actual function to "include" a javascript file on demand. Given that we have several concept going on here, I'm thinking it might make sense to actually create 2 namespaces: a jscore namespace (for housing generalized javascript functions) and an xb namespace (Cross-browser, for things like the cross-browser XMLHttp Wrapper). I feel that javascript core functions and cross-browser functions are close enough to warrant putting them both in the same file, which I will call sstchur.web.js.js (yes, .js two times).


if (!window.sstchur) { window.sstchur = {}; }
if (!sstchur.web) { sstchur.web = {}; }
if (!sstchur.web.js) { sstchur.web.js = {}; }
if (!sstchur.web.js.xb) { sstchur.web.js.xb = {}; }
if (!sstchur.web.js.core) { sstchur.web.js.core = {}; }
The above code simply sets up those namespaces I described above. The XMLHttp Wrapper is critical for what we're trying to do, so let's tackle that first.

sstchur.web.js.xb =
{
  HttpReq: function()
  {
    if (typeof window.XMLHttpRequest != 'undefined')
      { return new XMLHttpRequest(); }

    if (typeof window.ActiveXObject != 'undefined')
    {
      try { return new ActiveXObject('Microsoft.XMLHTTP'); }
      catch(everything) { return null; }
    }

    // No support for XMLHTTP?  No object for you!
    return null;
  }
};
 

We start by defining a new class (function) called HttpReq. When invoked, we check first to see if we can use the W3C standard XMLHttpRequest object, and if so, we return a new instance of it. If that fails, we check to see if ActiveX is available (IE never plays by the rules), and if so, we try to invoke a Microsoft.XMLHTTP ActiveX object (just because ActiveX is availlable doens't mean the XMLHTTP object will be). If that fails, we'll end up returning null. Note that it is the programmer's responsibility to check if an attempt to instantiate HttpReq returns null.

To create a new instance of an HttpReq, simply do the following:


var xb = sstchur.web.js.xb;
var xReq = new xb.HttpReq();

The next step is to define the functions that make up the jscore namespace (specifically, ifFileExists(..) and includeJS(..)). We'll start with ifFileExists(..):


sstchur.web.js.core =
{
  // Shortcut to the sstchur.web.js.xb namespace
  xb: sstchur.web.js.xb,

  // Inlucded files array
  m_included: new Array(),

  ifFileExists: function(_file, _callbackFn)
  {
    var xReq = new this.xb.HttpReq();
    xReq.open('GET', _file, true);
    xReq.send(");

    xReq.onreadystatechange = function()
    {
      if (xReq.readyState == 4)
      {
        if (xReq.status == 200)
        {
          if (typeof _callbackFn == 'function')
          {
            var responseText = xReq.responseText;
            _callbackFn(responseText);
          }
        }
      }
    };
  },

We start by creating a shortcut to the xb namespace (just for convenience) and then creating a private associative array, m_included, which keep track of which javascript files have already been included. Next comes the ifFileExists(..) definition.

The ifFileExists(..) function take two parameters. The first is the file whose existence we want to verify, and the second is a callback function that we want to execute if that file does in fact exist. In order to do this, we first create an XMLHTTPRequest object by leveraging our cross-browser HttpReq class. We then set the method to 'GET', the file to, _file (which the programmer would have passed in as a parameter), and the asynchronous flag to true, so that this HttpReq is not blocking.

The xReq.send(''); command simply executes the XMLHTTPRequest on the given file. In order to make anything interesting happen, we need to wireup an onreadystatechange even to our HttpReq object. If you're at all familiar with AJAX, then this function shouldn't look the least bit confusing to you. We simply make sure that the object is ready (readyState = 4) and that the response from the server was okay (status == 200). If so, we know the files exists on the server, so we can go ahead and execute the callback function (if it exists).

For the callback function to be useful, we need to give it some information (in this case, the responseText that came back from the server when on HttpReq object completed its request). We first verify that _callbackFn is in fact a function, and if so, we execute the function, passing in the responseText as a parameter.

Next, we need to define the function that will include a javascript file in the HTML page. It looks like this:


  includeJS: function(_data, _guid)
  {
    // Already included this file?  Don't bother
    if (this.m_included[_guid] == 'included')
      { return; }

    // Mark this file as included
    this.m_included[_guid] = 'included';

    // Now, actually include the file
    eval(_data);
  }
};

The includeJS(..) function also takes two parameters. The first is simply a string of plain text javascript, and the second is a unique ID that will be associated with the javascript text (this is how we make sure that we're not including the same javascript multiple times).

Our first step is a simple check to see if the given _guid has already been marked in the array as 'included'. If so, we simply do a premature return from this function. If not, we go ahead and mark the _guid as 'included'. The final step is to simply eval the _data, so that it becomes part of the page.

I know, I know: "eval" is just one letter removed from "evil." I do have a solution that does not make use of eval, but it uses synchronous XMLHTTP (which is blocking) that is arguably worse than using eval. Furthermore, as long as your use of eval is kept to an absolute minimum, it can be an acceptable tool in some cases (and I believe this is one of those cases).

Believe it or not, that actually brings us to the end of the sstchur.web.js.js file. Given that we haven't done all that much, you might be wondering exactly how you go about using these functions that we just wrote. Fortunately, it's pretty darn easy. Let's assume the existence of a simple javascript file, someFile.js, which looks like this:


alert('Hi, my name is "someFile.js"');
 

Say you want to load this javascript on-demand. Something like this would do the trick:


<html>

  <head>
    <script type = "text/javascript" src = "./sstchur.web.js.js"></script>

    <script type = "text/javascript">
      function loadOnDemand()
      {
        var core = sstchur.web.js.core;
        var file = './somefile.js';
        core.ifFileExists(file, includeProxy(file));

        function includeProxy(_file)
        {
          return function(_responseText)
          {
            core.includeJS(_responseText, _file);
          }
        }
      }
    </script>
  </head>

  <body>

    <button id = "btn" onclick = "loadOnDemand()">Load File</button>

  </body>
</html>

Naturally, we need to include our jscore library (sstchur.web.js.js). Next we create a function, loadOnDemand(), which we'll execute when the button, btn, is clicked. The first 2 lines of that function are trivial. The thid make use of our handy-dandy ifFileExists(..) functions. If somefile.js exists, we'll execute the function includeProxy(..).

The includeProxy(..) make use of javascript closures (beyond the scope of this little article), which returns the actual function that will execute when the HttpReq completes. Note that the inner function makes use of the includeJS(..) we wrote. We simply pass in the _responseText (javascript data string) and as the unique ID for this javascript, we're passing in the actual file name. This makes sense because filesname (including path) will naturally be unique.>

If you load this file in a browser and click the button, you should see the somefile.js execute (an alert that say 'Hi, my name is "someFile.js"'). Subsequent clicks should result in nothing (verifying that the file is not being included multiple times).

In order for this example to work, the request to the sample HTML file needs to actually execute through a web server. In other words, you can't simply fire up your web browser and load the file via File | Open. You need to have a local web server set up, whereby you can actually browse to the file through http.

I can see multiple instances where this concept would be userful. One thing that interests me is developing a specific include construct in javascript that allows one javascript file to include other javascript files (on-demand of course). This seems like it would be slightly more involved, but still very doable — perhaps a future article.

That's it! Comments welcome.

Leave a Reply

Your email address will not be published. Required fields are marked *