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).
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.
JavaScript Madness: Dynamic Script Loading的更多相关文章
- 自己编写jQuery动态引入js文件插件 (jquery.import.dynamic.script)
这个插件主要是结合jquery或者xhr异步请求来使用的,它可以把已经引入过的js文件记录在浏览器内存中,当下次再引入相同的文件就忽略该文件的引入. 此插件不支持浏览器刷新保存数据,那需要利用cook ...
- Javascript Madness: Mouse Events
http://unixpapa.com/js/mouse.html Javascript Madness: Mouse Events Jan WolterAug 12, 2011 Note: I ha ...
- Javascript中alert</script>的方法
Javascript中alert</script>的方法: <%@ page language="java" import="java.util.*&q ...
- javascript动态创建script标签,加载完成后调用回调
代码如下: var head = document.getElementsByTagName('head')[0]; var script = document.createElement('scri ...
- [Algorithms] Solve Complex Problems in JavaScript with Dynamic Programming
Every dynamic programming algorithm starts with a grid. It entails solving subproblems and builds up ...
- javascript正则找script标签, link标签里面的src或者 href属性
1. [代码]javascript 简单的search <script(?:(?:\s|.)+?)src=[\"\'](.+?)[\"\'](?!\<)(?:(? ...
- JavaScript动态创建script标签并执行js代码
<script> //创建一个script标签 function loadScriptString(code) { var script = document.createElement( ...
- elasticsearch update方法报错: Too many dynamic script compilations within, max: [75/5m]
PUT _cluster/settings { "transient" : { "script.max_compilations ...
- CRM 2013 Script Loading Deep Dive
关于CRM中脚本的加载次序梳理的很不错,可以看看 https://community.dynamics.com/crm/b/develop1/archive/2013/11/02/crm-2013-s ...
随机推荐
- 分享8款最新HTML5/CSS3功能插件及源码下载
1.HTML5/CSS3鬼脸表情下拉菜单 超级可爱 这款HTML5/CSS3鬼脸表情下拉菜单真的很特别,虽然菜单的实现并没有利用复杂的HTML5/CSS3技术,但是创意的确不错. 在线演示 源码下载 ...
- 【leetcode】15. 3Sum
题目描述: Given an array S of n integers, are there elements a, b, c in S such that a + b + c = 0? Find ...
- 《shell下sort排序命令的使用》
首先建立一个文件,很乱,没有规律: 正排序: 倒排序: Uniq 删除文件中的重复行:用此命令要先对文件进行排序. 对文件冗余,只要文件所有重复的字符显示一次: 显示1-7,不重复的行: 只显示1-7 ...
- jQuery: 图片不完全按比例自动缩小
有时我们会有这样的需求:让图片显示在固定大小的区域.如果不考虑 IE6 完全可以使用 css 的 max-width 限制宽度自动按比例缩小显示,但是这样有个问题,就是如果按比例缩小后,图片高度不够, ...
- python:执行一个命令行N次
经常希望可以执行一个命令行N次...windows下没有现成的工具(有?推荐给我!) 用python写一个... #!/usr/bin/evn python #coding: utf-8 " ...
- SQL中的类型转换
SQL中的类型转换一直是以块心病,因为用得比较少,所以每次想用的时候都要想半天,恰好这段时间比较空,整理整理.今天写个标题先.
- python 安装 setuptools Compression requires the (missing) zlib module 的解决方案
背景: 虚拟机centos下安装python辅助工具 setuptools报错,错误信息大概如下: Traceback (most recent call last): File "setu ...
- 开发流程习惯的养成—TFS简单使用
才开始用,所以是个很基础的介绍,欢迎大家一起交流学习 一.追本溯源 讲到开发流程,还要从敏捷开始,因为敏捷才有了开发流程的重视,整个流程也是按照敏捷的思想进行的,这里不再叙述敏捷的定义 敏捷的流程(个 ...
- Posix 共享内存区
要点 与mmap配合使用 open与shm_open的区别,open打开磁盘上的普通文件,shm_open创建和打开的文件在/dev/shm文件夹下,该文件夹对应的是内存 gcc编译时加参数-lrt ...
- MYSQL 配置slave故障
之前为主从配置,后来分割成2个单实例.现在环境需要,重新配置为主从,之前参数都已配置好,直接启动,如下: mysql> change master to master_host='192.168 ...