JavaScript Madness: Dynamic Script Loading
Introduction
I've developed some pretty seriously Javascript intensive sites, where the sheer quantity of Javascript on the page is so much that I worry about the load time for the page getting too big. Often large chunks of the Javascript code are only used very rarely, and it seems a shame to load all that code that the user will probably never use. So I wanted a way to dynamically load Javascript functions on demand. This is sometimes called "lazy loading".
For example, my web paint-by-number site has pages where users can solve logic puzzles. Normally users want to solve the puzzles themselves, but when someone is developing a new puzzle, they may need to test solve the same puzzle dozens of time. Doing this gets a bit dull, so we have a simple AI puzzle solving program they can use to solve their own puzzles. This is a big hunk of rarely used Javascript. We don't want to load it with every puzzle page. We want to load it only when the user clicks the "helper" button for the first time.
There are several ways you can load additional Javascript on demand. The best known one is to use XMLHttpRequest() to fetch a file containing the Javascript, and then use the Javascript eval() to evaluate it.
This essay considers an alternative method: dynamically created <script> tags.
On-Demand Loading
One approach to dynamically loading "helper.js" is to create a
<script type="text/javascript" src="helper.js"></script>
tag and insert it into the current page's <head> block. You could do that with the following lines of Javascript code:
var head= document.getElementsByTagName('head')[0]; var script= document.createElement('script'); script.type= 'text/javascript'; script.src= 'helper.js'; head.appendChild(script);
This works fine on every modern browser I've tested (IE 5.0 and 7.0; Firefox 2.0; Safari 1.3; Opera 7.54 and 9.10; Konqueror 3.5; iCab 3.0). The only browser I'ved tried that did not work is Macintosh IE (version 5.2), which does not allow the script.src property to be changed either by direct assignment (as shown here) or by a setAttribute() call.
I tried to simplify this further by having the <script> tag already defined in the header of the document, like this:
<script id="loadarea" type="text/javascript"></script>
And then just setting the src attribute when I wanted to load a file, like this:
document.getElementById('loadarea').src= 'helper.js';
Unfortunately, this doesn't work so well. It's fine in Windows IE, Opera and iCab. In Gecko browsers, it only works once. If you try to load a second file by changing the src of the <script> tag again, the new source is not loaded. It doesn't work in Safari or Konqueror, and of course it generates the same error as the other method in Macintosh IE.
Pity. It'd be cool to be able to do this with one line of Javascript.
Detecting Load Completion
After we have loaded the helper code, we obviously want to call it to start it running. However, we can't just put a call to helper() after the commands above, because the browser may be loading the "helper.js" file asynchronously, which means that our call may occur before the function has finished (or even started) loading.
One web page suggested setting up some event handlers that will be called when the load is complete. We do that by adding the following lines to the previous code:
var head= document.getElementsByTagName('head')[0]; var script= document.createElement('script'); script.type= 'text/javascript'; script.onreadystatechange= function () { if (this.readyState == 'complete') helper(); } script.onload= helper; script.src= 'helper.js'; head.appendChild(script);
Here we set up two different event handlers on the newly created script tag. Depending on the browser, one or the other of these two handlers is supposed to be called when the script has finished loading. The onreadystatechange handler works on IE only. The onload handler works on Gecko browsers and Opera.
The "this.readyState == 'complete'" test doesn't actually entirely work. The readyState theoretically goes through a series of states:
0 uninitialized 1 loading 2 loaded 3 interactive 4 complete
But in fact, states may be skipped. In my experience with IE 7, you get either a loaded event or a completed event, but not both. It may have something to do with whether you are loading from cache or not but there seem to be other factors that influence which events you get. Sometimes I getloading or interactive events too, and sometimes I don't. It's possible the test should be "this.readyState == 'loaded' || this.readyState == 'complete'", but that risks triggering twice.
By the way, if you are reading about readyState on the net, it is important to understand that most of the discussion is in the context of the request object returned by XMLHttpRequest() calls. This is quite different. Almost all browsers support that, not just IE, and the values are numbers instead of strings. The complete (4) state in that case does reliably occur, though the others are a bit quirky in that context too.
Though the problem in IE is bad enough, there's more. So far as I can tell, no event is fired when the script load is complete in either Safari 1.2, Konqueror, or iCab. I've seen claims that it works on Safari 2.0, so long as you set up the handler before you attach the script element to the document, but haven't confirmed this. It's possible they were talking about the XMLHttpRequest() case.
On the whole, I decided that these loading events are not reliable enough on enough browsers to be usable.
Luckily, for my application, a much simpler approach suffices. It'll work for you if (1) you control the contents of the Javascript file being loaded, and (2) you always want to call the same callback function when it is loaded. If that's the case, just put a call to the callback function on the last line of the Javascript file. After the rest of the file has been loaded, the callback function gets called. What could be simpler?
In my case whenever we load the 'helper.js' file, we always want to execute the helper() function immediately after the load. In this case, it is one of the functions loaded, but it could equally well be a function defined before the load. So, we simply make the last line of the 'helper.js' file a call to the helper() function. Then it will always be executed immediately after everything else has been loaded. No flakey, non-portable event handling is required.
Here's a little test script for script loading events. When you click the "load script" link, a script will be loaded. You will see alerts when the script is started and when readystatechange and load events fire. On either of these events, the loaded function is called, which simply does another alert. The last line of the loaded script also does an alert. Clicking on the "run loaded function" link runs the function (if it is defined).
load script
run loaded function
Loading Only Once
So, the first time a paint-by-number user clicks on the helper button, we load the helper script and then run the helper() function from a call at the end of the script. But the next time someone clicks on the helper button, we don't want to load it again. We just want to execute the already loaded function.
There are many ways you could do this. We could check if the helper() function is defined, and load it only if it isn't, but there is an easier way. Simply name the function that loads the Javascript the same as the function that is loaded. In our case, we would do something like:
<input type="button" onclick="helper()" value="Helper"> <script language="JavaScript"> function helper() { var head= document.getElementsByTagName('head')[0]; var script= document.createElement('script'); script.type= 'text/javascript'; script.src= 'helper.js'; head.appendChild(script); } </script>
The script file being loaded looks something like this:
function helper() { : lots of code : } : more function and global variable definitions : helper();
So the "helper" button runs the helper() script when clicked. Initially the helper() function is the dummy function that just loads the 'helper.js' file. That file defines the real helper() function, replacing the previous function definition. The last line of the file calls helper() running the new helper function. The next time the "helper" button is clicked, the loaded version of the helper() function will be run, since the other version doesn't exist anymore.
Multiple Loads
As many people have pointed out, this method of creating a <script> tag to load some Javascript could be used as an alternative way to fetch JSON-formatted data from the server, instead of XMLHttpRequest(). If you do that, then you'll likely want to be making the same, or similar calls multiple times. This is likely to cause a couple easily fixed problems.
First, you need to worry about caching. Typically, the url you're going to be loading is going to be some kind of CGI program. After all, if the output doesn't vary, why bother loading it more than once? Usually browsers are fairly smart about not caching those, but you might want to take some steps to ensure that the second call doesn't just get you the cached copy of the result of the first call. So the CGI should probably be outputting headers to suppress caching. You might also put extra arguments on the URL, perhaps by keeping a count of the number of times you've called it, and adding a "?count=7" argument onto the end of the URL (where 7 is the current count). This makes the URL different on each call, and further ensures that you won't get a cached copy.
Second, you'll tend to accumulate a lot of <script> tags in your header. This doesn't necessarily do a whole lot of harm, but it seems sloppy. So you could probably delete them by doing head.removeChild(script). I think you can actually have the callback function do this immediately after loading. Removing the <script> tag does not undefine functions or variables that were defined by it, so you are done with it the moment it is done loading.
Comparison with XMLHttpRequest()
This method of loading Javascript code or data by dynamically creating new <script> tags in the header does have some advantages over XMLHttpRequest() calls:
- Can request a file from anywhere on the net, not just the server your page was loaded from.
- Works in IE even when ActiveX is turned off (though not when Javascript is turned off).
- Works with a few older browsers that don't support XMLHttpRequest, like Opera 7 (though not Macintosh versions of IE).
There are also some modest disadvantages, including:
- Returned data has to be formatted as Javascript code. XMLHttpRequest() can be used to fetch data in any format, XML, JSON, plain text, or whatever.
- Can only do GET requests, not POST requests.
- Whether the request is synchronous or asynchronous is pot luck, depending on the browser. With XMLHttpRequest() you can control this.
- When fetching JSON data from an untrusted source, there is no possiblity of checking the data before feeding it to the Javascript parser. With XMLHttpRequest() you can parse the data with something like json2.js instead of eval() for secure parsing.
An additional difference, which may or may not be an advantage depending on your appliction, is that functions and variables defined by loading a file by creating a new <script> tag are always created in the global context, while those created with an eval() call are defined in the current context. If you want the data loaded to be only locally defined, then this is an advantage for the XMLHttpRequest() approach. If you want to be able to load global functions and variables from anywhere in your program, this is an advantage for dynamic <script> tags.
On the whole, I think dynamic <script> tags are sometimes a better way to load additional blocks of code than XMLHttpRequest(), but for fetching data, XMLHttpRequest() usually wins out.
posted on 2014-04-12 06:05 Step-BY-Step 阅读(239) 评论(0) 编辑 收藏 举报