Sunday, April 22, 2012

Dynamically Loading JavaScript Files In a Sequence

If you are building a large JavaScript application chances are you will have multiple JavaScript files that you will be adding to your web page. You can have a single huge JavaScript file that does everything and include that but chances are you need to break your code into some logical blocks (maybe custom classes) to make it more maintainable and to even add customization (e.g. themes or multiple language support etc).

These logical blocks can be multiple files which you then can include like this.

<html> 

<head>
   <script type="text/javascript" src="settings_default.js"></script>
   <script type="text/javascript" src="theme_default.js"></script>
   <script type="text/javascript" src="language_default.js"></script>
   <script type="text/javascript" src="main.js"></script>
</head>

<body onload="startApp();">

</body>

</html>


1)  In the above code, the body onload event fires the "startApp()" function that exists in the main.js file.

2) The settings_default.js has some default settings that will be used by all our JavaScript code.

3) The theme_default.js file loads the default theme, it also references some settings variables in the settings_default.js file.

4) The language_default.js loads the default language the text in the ui will be in, it also references some settings variables in the settings_default.js file.

Now this will work fine but what if you need to dynamically include 4 different files based on some customization / localization of your application to give your audience a  more personal experience (e.g. to load a different set of settings, theme and language).

Have a look at this example:

<html>

<head>

<script type="text/javascript">

  var environment = "japanese"; // can be default, french etc

  function loadJsFile(fileLocation){
    var fileref = document.createElement('script');
    fileref.setAttribute("type","text/javascript");
    fileref.setAttribute("src", fileLocation);
    document.getElementsByTagName("head")[0].appendChild(fileref);
  }

  loadJsFile("settings_" + environment + ".js");
  loadJsFile("theme_" + environment + "default.js");
  loadJsFile("language_" + environment + ".js");
  loadJsFile("main.js");

</script>

</head>

<body onload="startApp();">

</body>

</html>


1) In the above code we have changed the "environment" to "japanese" and we load custom settings_japanese .js, theme_japanese.js and language_japanese.js files dynamically by creating script tags and inserting them into the head tag.

This looks like an ideal solution to dynamically customize you application and load the required custom JavaScript file as needed but there are some serious issues here.


The Problems
There are two problems with the above dynamic approach.

1) Remember that all the custom JavaScript files require some variables that are defined in the settings_japanese .js file and therefore the settings_japanese .js HAS to be the first file loaded, or else we will get "variable not defined" errors.

But loading files dynamically like this CANNOT guarantee that they will load in the correct order and sequence. So even though we call:

loadJsFile("settings_" + environment + ".js")

before,

loadJsFile("theme_" + environment + "default.js");

there is a possibility that  theme_japanese.js will load before settings_japanese .js.

This is the nature of asynchronous HTTP calls.

2) Remember that the startApp() function which is invoked "onload" of the document lives in the main.js file, and even though the "onload" event should only trigger when all the JavaScript files are loaded there is a possibility that its fired before the main.js is available on your page. Although all modern browsers don't have this problem I have seen it in custom browsers used on mobile and TV etc.

So we need a way to ensure that startApp() is fired ONLY when all the JavaScript files are loaded successfully and are in the correct order. In other words, ONLY start the app when its ready to be executed in the correct state.


The Solution
To make sure your JavaScript files load in the correct order we need some way to detect when a file has completed loading and then trigger the next file to load and the next etc. and finally when all the files have loaded mark the application as "ready to be executed".

Here is how you can do it:

<html>

<head>

<script type="text/javascript">
   var environment = "japanese"; // can be default, french etc

   function loadJsFilesSequentially(scriptsCollection, startIndex, librariesLoadedCallback) {
     if (scriptsCollection[startIndex]) {
       var fileref = document.createElement('script');
       fileref.setAttribute("type","text/javascript");
       fileref.setAttribute("src", scriptsCollection[startIndex]);
       fileref.onload = function(){
         startIndex = startIndex + 1;
         loadJsFilesSequentially(scriptsCollection, startIndex, librariesLoadedCallback)
       };

       document.getElementsByTagName("head")[0].appendChild(fileref)
     }
     else {
       librariesLoadedCallback();
     }
   }

   // An array of scripts you want to load in order
   var scriptLibrary = [];
   scriptLibrary.push("settings_" + environment + ".js");
   scriptLibrary.push("theme_" + environment + "default.js");
   scriptLibrary.push("language_" + environment + ".js");
   scriptLibrary.push("main.js");

   // Pass the array of scripts you want loaded in order and a callback function to invoke when its done
   loadJsFilesSequentially(scriptLibrary, 0, function(){
       // application is "ready to be executed"
       startProgram();
   });

</script>

</head>

<body>

</body>

</html> 


1) The first thing to note is that in line 26 we create an array called scriptLibrary and insert into that array a collection of JavaScript files we want to execute in order.

2) In line 33 we pass the scriptLibrary array to the loadJsFilesSequentially function along with the index of the first file we want to load (which is 0) and a callback function that should be executed once all the files are loaded (in other works the application is "ready to be executed").

3) Now lets look at the loadJsFilesSequentially in detail.

  • On line 9, we check if an entry exists in the scriptLibrary based on the index we pass in, initially its 0 and therefore the file in the array will be "settings_japanese.js" (keep in mind the "environment" is defined to "japanese" in line 6)
  • Starting from line 10 we dynamically create a </script> tag and define its properties
  • The key to this function is found in line 13, where we define an onload function to the newly created script tag. After we attach the script tag to the head in line 18 and the file is successfully fetched via an http call and inserted into your pages DOM, the onload event of the script tag fires and our code

  •   startIndex = startIndex + 1;
      loadJsFilesSequentially(scriptsCollection, startIndex, librariesLoadedCallback)
    

    fires. Thanks to JavaScript "closures" the local private references to the variables scriptsCollection, startIndex and librariesLoadedCallback are still kept in memory. (Closures can be a tricky thing to understand, simply put when we use the 'function' keyword inside another function like we do above a closure is created by the JavaScript engine and a reference to all local private variables that were created when the main function was first called are maintained)
  • So we increment to startIndex in line 14 and in line 15 we recall the loadJsFilesSequentially function. As the startIndex is not incremented the next file in the scriptsCollection is loaded
  • This continues until we have loaded all the scripts in the scriptsCollection array and in line 9 our check indicated that all files are loaded and callback is called in line 21, which in turn marks the app as "ready to be executed" and the app is then started via startProgram()

++ Important: You have to be very careful when you write a recurring function like this (a function that calls itself) to make sure you have an exit clause or else you will end up with an infinite loop :)

Get the Code or Fork this at - http://github.com/newbreedofgeek/dynamic-js-script-loader

So there you go, the function above gives you a way to dynamically load Javscript files in a predefined sequence, which is very useful if you have many JavaScript files that depend on each other.

Happy Coding and as always, if you have any questions post them in the comments.

4 comments:

  1. Thank you Mark, very comprehensive solution and, above all, working fine.
    Initially, it didn't due to the "top.environment.gen2AppLibraryLocation" in line 10, which references to nothing in the script.
    Anyway, fine piece of code which does the job very well, thanks a lot.

    ReplyDelete
  2. Great stuff.
    It took a lot of hair pulling to figure why I got "variable not defined" only sometimes and sometimes didn't.
    Thanks.

    ReplyDelete
  3. @Micro & @JP - thanks and I'm glad you found this helpful. The "top.environment.gen2AppLibraryLocation" should not have been in my code example and it's probably what caused your JS error. I've corrected my code. Thanks :)

    ReplyDelete
  4. Extremely well written. Have come across several solutions which are effective, but they dont take into consideration the time delay it takes to download a js file on client's browser and then load, which infact hampers the sequence. Your's precisely handles this issue.

    thanks... you saved my day..

    ReplyDelete

Fork me on GitHub