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 ...
随机推荐
- ThinkPHP框架部署
ThinkPHP框架部署 1.下载ThinkPHP框架 下载地址:http://www.thinkphp.cn/ ThinkPHP官方网站 下载ThinkPHP3.2.3完整版,下载后如下图所示: A ...
- 杭电ACM2080--夹角有多大II
http://acm.hdu.edu.cn/showproblem.php?pid=2080 /* //Author:nunu // #include <stdio.h> #include ...
- bootstrap时间插件 火狐不显示 完美解决方法
原文链接:http://www.phpbiji.cn/article/index/id/141/cid/4.html bootstrap时间插件火狐 bootstrap-datetimepicker火 ...
- WCF 服务的ABC之地址(五)
地址 Address 在WCF中,每个服务都有一个唯一的地址(Address). 地址包含两个重要的元素:服务位置及传输协议. 服务位置包含目标机器名.站点.通信端口.管道(或队列),以及一个可选的特 ...
- Spring-Mybatis 异常记录(1)
Spring applicationconfig.xml如下 <?xml version="1.0" encoding="UTF-8"?> < ...
- Turn.js 实现翻书效果的学习与总结
最近CTO给我分配了一个移动端H5开发的任务,主要功能是需要实现翻书效果,我听过主要需求后,当时是呀!!!接下来自己尝试使用fullPage.js和Swiper来实现翻书效果,结果效果都不是非常的理想 ...
- vmware虚拟机上网:NAT搭建局域网
若是你不知道的情况下,可以编辑虚拟机网络配置,然后恢复默认,vmware会自动给你分配好ip,默认使用的是vmware8,下面的是使用默认的配置 看图 注意:子网的ip一定要在如上图所示的范围 适配器 ...
- for xml path以及sql合并查询
sql中for xml path的用法. http://www.cnblogs.com/yanghaibo/archive/2010/06/04/1751405.html
- Spark菜鸟学习营Day6 分布式代码运行调试
Spark菜鸟学习营Day6 分布式代码运行调试 作为代码调试,一般会分成两个部分 语法调试,也就是确定能够运行 结果调试,也就是确定程序逻辑的正确 其实这个都离不开运行,所以我们说一下如何让开发的S ...
- oracle-linux下挂载"移动硬盘" NTFS类型
环境: ORACLE-LINUX 5.7 全新移动硬盘(未使用过) 移动硬盘空间3T 在默认情况下,Linux系统不支持NTFS分区挂载 1.服务器: A服务器和B服务器为一套ORACLE-RAC,移 ...