This article will explain an AJAX technique that I am using in the development of my current startup project Kliqd. Those who have already been following the project will know that Kliqd makes extensive use of AJAX. Kliqd is designed so that as much of the HTML creation as possible happens on the client side. The client side JavaScript asks the server for data via JSON and then uses it to create the pages.
Recently I came up with an idea for how to further streamline this AJAX technique and make it even more efficient. Most pages on Kliqd contain extra functionality that isn't going to be used every time the page is viewed. For example, if a user of Kliqd is viewing another user's profile they will see buttons to follow that user, befriend that user, or invite that user to an event. Each of those buttons is tied to JavaScript logic that is used to open modal dialogs so that members can send a friend request, send an event invitation, and so on.
What if it was possible to load that JavaScript logic on the fly, when it was needed, rather than right from the start? So if a user opens a profile and never clicks the friend button, then the logic for opening the friend dialog will never be loaded. This would save bandwidth and time on the front end, causing the page to load faster and display faster. With this goal in mind I started experimenting with a way to load JavaScript functions via AJAX and execute them after the page is done loading.
The First Experiment
First I created a basic page which loaded a *.js file via AJAX, used eval() to interpret it, and then called the function which was contained within the downloaded file:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | <html> <head> <title>Ajax JavaScript Loader</title> <script type='text/javascript' src='jquery-1.3.2.min.js'></script> <script type='text/javascript'> $.ajax({type: "GET", url: "1_loader.js", success: function(responseText) { eval(responseText); internalFunction(); //This function defined in the JavaScript code loaded. } }); </script> </head> <body> This is a basic test of loading JavaScript code via AJAX and then running it. </body> </html> |
1 2 3 4 | function internalFunction() { alert("Success."); } |
This experiment worked, demonstrating that I could interpret a function using eval() and then call it. I immediately went on to create a second experiment, but I didn't realize that I had missed an important detail about the first experiment which would cause my second version to fail.
The Second Experiment
Feeling ambitious I created a more complicated piece of logic which allows the programmer to include JavaScript logic after the page has already finished loading. Requests to include JavaScript files are put into a buffer and handled one by one in the order they were issued. There is even a handler so that you can execute a piece of JavaScript or a function call after all the logic is loaded via AJAX. As I hinted earlier, though, the following code does not work. As you read through it, see if you can spot the subtle bug that I missed. Afterwards I will explain what went wrong:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 | <html> <head> <title>Ajax JavaScript Includer</title> <script type='text/javascript' src='jquery-1.3.2.min.js'></script> <script type='text/javascript'> //The list of loaded resources. var loaded = new Array(); //The list of resources that have yet to be loaded. var loading = new Array(); //Whether or not we are currently loading a resource via AJAX. var currentlyLoading = false; function loadAjax(resourceUrl) { $.ajax({type: "GET", url: resourceUrl, success: function(responseText) { //Set up the variables and functions loaded. eval(responseText); //This item is loaded, remove from queue. loading = loading.slice(1,loading.length); //Add it to the already loaded queue. loaded[loaded.length] = resourceUrl; //Now check the load queue for another item to load. if(loading.length>0) { //Load the first item in the queue via AJAX. loadAjax(loading[0]); } else { //We are done loading logic via AJAX. currentlyLoading = false; } } }); } function include(resourceUrl) { //Check to see if this resource has already been loaded. for(var i=0; i<loaded.length; i++) { if(loaded[i]==resourceURL) { return; } } //Store the in the loading queue. loading[loading.length] = resourceUrl; //Now check to see if we are already running an ajax request. if(currentlyLoading == true) { return; //The current loading process will work its way through the queue. } loadAjax(resourceUrl); currentlyLoading = true; } //This makes it easy to execute a function after all items to be included are //loaded. function waitForInclude(afterSafeFunction) { if(loading.length==0) { afterSafeFunction(); } else { //Wait 100ms and then try again. setTimeout("waitForInclude("+afterSafeFunction+")",100) } } include("2_setFoo.js"); include("2_outputFoo.js"); waitForInclude(function() { setFoo("Set in ajax."); outputFoo(); }) </script> </head> <body> This is a basic test of loading JavaScript code via AJAX and then running it. </body> </html> |
1 2 3 4 5 6 | var foo = 'test'; function setFoo(bar) { foo=bar; } |
1 2 3 4 | function outputFoo() { alert(foo); } |
At first glance it appears that nothing is wrong, but when the code is run, it says that the functions setFoo() and outputFoo() cannot be found. What is wrong? To understand what was wrong I had to understand the difference between the first experiment, which does work, and this second experiment. Simply put, the first experiment calls the function within the same code block in which it was created using eval(). The second experiment attempts to call the function from outside the code block. To demonstrate what I mean, notice the difference between the following code and the code in the first experiment:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | <html> <head> <title>Ajax JavaScript Loader</title> <script type='text/javascript' src='jquery-1.3.2.min.js'></script> <script type='text/javascript'> //Sadly internalFunction can not be accessed here, only with the scope of the //success function in which it was created using eval(); function nonScope() { internalFunction(); } $.ajax({type: "GET", url: "1_loader.js", success: function(responseText) { eval(responseText); internalFunction(); //This function defined in the JavaScript code loaded. nonScope(); } }); </script> </head> <body> This is a basic test of loading JavaScript code via AJAX and then running it. </body> </html> |
The eval() function creates the function within the scope of the AJAX response function. Essentially it creates a function within a function:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | <html> <head> <title>Ajax JavaScript Loader</title> <script type='text/javascript' src='jquery-1.3.2.min.js'></script> <script type='text/javascript'> //Sadly internalFunction can not be accessed here, only with the scope of the //success function in which it was created using eval(); function nonScope() { internalFunction(); } $.ajax({type: "GET", url: "1_loader.js", success: function(responseText) { function internalFunction() { alert("Success."); } internalFunction(); //This function defined in the JavaScript code loaded. nonScope(); } }); </script> </head> <body> This is a basic test of loading JavaScript code via AJAX and then running it. </body> </html> |
That function within a function can only be accessed within the function in which it was created. Once I identified the problem it was relatively easy to fix.
The Third Experiment
I made a few simple changes to my code from the second experiment so that rather than defining a function within a function, the loaded JavaScript defines its functions as members of a global object which serves as a type of namespace for all the logic loaded via AJAX:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 | <html> <head> <title>Ajax JavaScript Includer</title> <script type='text/javascript' src='jquery-1.3.2.min.js'></script> <script type='text/javascript'> //We have to declare a global object that we will use as a namespace for storing //the eval loaded functions and variables, otherwise they will only exist within //the scope of the ajax response function, which is be useless. var fooNamespace = new Object(); //The list of loaded resources. var loaded = new Array(); //The list of resources that have yet to be loaded. var loading = new Array(); //Whether or not we are currently loading a resource via AJAX. var currentlyLoading = false; function loadAjax(resourceUrl) { $.ajax({type: "GET", url: resourceUrl, success: function(responseText) { //Set up the variables and functions loaded. eval(responseText); //This item is loaded, remove from queue. loading = loading.slice(1,loading.length); //Add it to the already loaded queue. loaded[loaded.length] = resourceUrl; //Now check the load queue for another item to load. if(loading.length>0) { //Load the first item in the queue via AJAX. loadAjax(loading[0]); } else { //We are done loading logic via AJAX. currentlyLoading = false; } } }); } function include(resourceUrl) { //Check to see if this resource has already been loaded. for(var i=0; i<loaded.length; i++) { if(loaded[i]==resourceURL) { return; } } //Store the in the loading queue. loading[loading.length] = resourceUrl; //Now check to see if we are already running an ajax request. if(currentlyLoading == true) { return; //The current loading process will work its way through the queue. } loadAjax(resourceUrl); currentlyLoading = true; } //This makes it easy to execute a function after all items to be included are //loaded. function waitForInclude(afterSafeFunction) { if(loading.length==0) { afterSafeFunction(); } else { //Wait 100ms and then try again. setTimeout("waitForInclude("+afterSafeFunction+")",100) } } include("4_setFoo.js"); include("4_outputFoo.js"); waitForInclude(function() { fooNamespace.setFoo("Foo was set using a function loaded via AJAX, and output using another function loaded via AJAX."); fooNamespace.outputFoo(); }) </script> </head> <body> This is a basic test of loading JavaScript code via AJAX and then running it. </body> </html> |
1 2 3 4 5 6 | fooNamespace.foo = 'Not yet set.'; fooNamespace.setFoo = function(bar) { fooNamespace.foo=bar; } |
1 2 3 4 | fooNamespace.outputFoo = function() { alert(fooNamespace.foo); } |
This example finally works, proving that it is possible to load and run JavaScript functions after the page has finished loading. This means that it is possible to have on demand downloading of JavaScript code, preventing wasted bandwidth, and decreasing page load times by making it possible to download and display the page very quickly, then download the JavaScript logic as it is needed.
Conclusion
I put together a slightly more complicated example containing my AJAX JavaScript includer code as a library. To get started download the following sample code, which includes the library:
JavaScript Include Ajax Example
The file include.js contains two simple functions that are all you need to get started:
include(fileUrl);
This is fairly obvious. It downloads the file at fileUrl via AJAX and executes its contents. Be sure to create functions as members of a global namespace as in my third experiment.
waitForInclude(functionToExecute);
This function will execute functionToExecute() after the queue of JavaScript files to download and execute has been completely processed.
If you would like to use the code feel free to do so. A few ideas that could be added might be callback functions that could be used to display an AJAX spinner, or advance a progress bar while JavaScript logic loads in the background. Please let me know if you use my code on any interesting projects because I would love to keep track of whether or not this technique catches on, and if so who starts using it.
5 comments so far. What are your thoughts?
that's all cool js/ajax tips, thank you very much for sharing. But your layout is too complicated to read
agree
great article! Just what i needed! But what's up with the layout??
dude.. you need some serious copy paste training. The code snippet is so poorly formatted. I wonder if you checked your post before or after publishing.
ur layout suck