Monday, June 11, 2012

Injecting a private jQuery, Part 3: jQuery and Plugins Asynchronously

Ok...  Part 3 of these blog posts and we are FINALLY going to look at jQuery being "injected".

First, what does 'Injection" mean?

If you look up "JavaScript Injection", you will see a lot of negative posts surrounding malicious code being "injected" into a page or cross-site scripting and how to protect against it.  However what I am talking about and, it you want to Google about it, you might try Dynamically loaded JavaScript.

To understand this, let's first understand what happens when we load jQuery and the UI in the "classic" code.  lets say we have the following code in our html file:


<link type="text/css" href="css/themename/jquery-ui-1.8.21.custom.css" rel="Stylesheet" /> 
<script type="text/javascript" src="js/jquery-1.4.4.min.js"></script>
<script type="text/javascript" src="js/jquery-ui-1.8.21.custom.min.js"></script>


This code is obvious, we load a css, then we load the jQuery library, then we load the UI plug-in.  Because this code is directly entered into our html, it will load synchronously - that is one-at-a-time.  The jQuery library will not load/run until the css is finished and (more importantly), the ui library will not load/run until the jQuery library in finished loading/running.

However, in today's ever complex web 2.0 (or greater) applications, we could be loading tons of JavaScript files and libraries.  Some with dependencies (like jQuery and jQuery-ui) and some with no dependencies. Combine those JavaScript files with CSS files and you could have a mighty long load time as each file is loaded and executed sequentially and synchronously.  So your user could be waiting a long time before they are given control over the page.  How can we lessen that "wait" time?

Enter scripting injection or dynamically loaded scripts.  Basically what happens is that through JavaScript, you ask the DOM to create a script element for you and then you ask the DOM to add that script to the "head" or "body" section of your page.  Here is a sample:

  //Create a 'script' element 
  var scrptE = document.createElement("script");

  // Set it's 'type' attribute 
  scrptE.setAttribute("type", "text/javascript");

  // Set it's 'language' attribute
  scrptE.setAttribute("language", "JavaScript");

  // Set it's 'src' attribute
  scrptE.setAttribute("src", "myjsfile.js");

  // Now add this new element to the head tag
  document.getElementsByTagName("head")[0].appendChild(scrptE);

Very straight forward...  Ask the DOM for a new "script" element, set the element's "type", "language" and (most importantly) the "src" attributes, then append the element to the "head" of the document.  What happens then is that the JavaScript file (myjsfile.js) will load asynchronously - meaning that if you were to add several JavaScript files this way, they would all start loading at the same time - not sequentially!

So, if we were to wrap this code in a function like (ignore the callback function for now):

   function loadJS(jsFile, callback) {
     var scrptE = document.createElement("script");

     // Set it's 'type' attribute 
     scrptE.setAttribute("type", "text/javascript");

     // Set it's 'language' attribute
     scrptE.setAttribute("language", "JavaScript");

     // Set it's 'src' attribute
     scrptE.setAttribute("src", jsFile);

     // Now add this new element to the head tag
     document.getElementsByTagName("head")[0].appendChild(scrptE);
   }


We could then load our non-dependent JavaScript files like this:

   LoadJS('scripts\file1.js', null);
   LoadJS('scripts\file2.js', null);
   LoadJS('scripts\file3.js', null);

All 3 files would load at the same time. So why can we only do this with "non-dependent" files? Because we have no idea in what order these files will finish loading. Although we have called the files in the 1, 2, 3 order, they may very well complete in 3, 1, 2 order.  So this scenario:

   loadJS('scripts/jquery-1.4.4.min.js', null);
   loadJS('scripts/jquery-ui-1.8.21.custom.min.js', null);

Would not work - because it would be possible for the ui library to finish loading (and start executing) before jQuery, and, as I documented in Part 2, there is a dependency between these two JavaScript files.

Before we discuss how to deal with dependencies, lets first look at the more "generic" issue concerning: what if I want to run some code after my JavaScript loads?

Enter "onload" and the "onreadystatechange" event.  I wont go into detail here, but most modern browsers (chrome, firefox) have an event on the script element called "onload" that fires once the script is fully loaded. Of course IE does NOT have this event.  Instead, IE fires a series of events as the script is loading.  the event that fires is called "onreadystatechange" and when this event fires, you check for the readyState property which will eventually end with a state of "loaded" or "complete" (depending on whether the JavaScript was already cached - and sometimes the event will fire in BOTH conditions).  Again, you can Google this in more detail if you want, but this explanation should be enough to understand this additional piece of code that we can add to our loadJS() function.  Before appending the script tag to the DOM, we add this additional piece of code:

   // Add the jQuery script reference
   var scriptAdded = false;
   scrptE.onload = scrptE.onreadystatechange = function () {
      if (!scriptAdded && (!this.readyState ||
                            this.readyState == 'loaded' || 
                            this.readyState == 'complete')) {
         scriptAdded = true;

         // Call callback method to confirm the loading of the script file
         callback();
      };
   };

By adding these lines, we create a cross-browser generic way to know when our script finishes loading so that we can then execute some code based on that fact.  Note the "callback" function is called when the script completes its loading.  The "scriptAdded" variable is used to make sure that IE does not fire the callback method more than once if onreadystatechange fires for BOTH loaded and complete states.

We can now load JavaScript dependencies like this:

   loadJS('scripts/jquery-1.4.4.min.js', function() {
      loadJS('scripts/jquery-ui-1.8.21.custom.min.js', null);
   });

This code snippet will load the jQuery library and, when its finished loading, it will load the jQuery ui library.




No comments:

Post a Comment