javascript异步动态加载js和css文件的实现方法、原理
发表日期:2015-06-29 文章编辑:本站编辑 来源:未知 浏览次数:
主要几个框架或者插件是如何实现异步加载事件响应的。
一.LABjs
这个项目位于github上面,其本意就是Loading And Blocking JavaScript,就是一个异步脚本加载器,相对于原始的粗暴script标签方式而言,提供了弹性的性能优化(做到js文件在浏览器中尽可能的并行加载,并且能够提供顺序执行的保证),还能在高级浏览器中实现先加载然后执行的功能。作为一个异步js加载器还是非常优秀的。其在异步加载的成功事件响应方面的实现如下:
// creates a script load listener
function create_script_load_listener(elem,registry_item,flag,onload) {
elem.onload = elem.onreadystatechange = function() {
if ((elem.readyState && elem.readyState != "complete" && elem.readyState != "loaded")
|| registry_item[flag]) return;
elem.onload = elem.onreadystatechange = null;
onload();
};
}
从上面可见,基本上就是利用onload事件和onreadystatechange事件来完成的,最后一个registry_item[flag]就是对同源的文件可以通过AJAX来实现的。但是没有涉及到css文件的加载,也没提到js文件不存在的时候如何来检测。
二.RequireJS
RequireJS主要定位于a file and module loader for javascript,就是作为一种模块化开发过程中的模块加载器,由于模块是以文件形式存在,所以也就相当于一个文件加载器,但是实现了模块管理的功能。其主要也仍然是在处理js文件的加载,没有考虑css文件的加载。那我们看一下他主要的事件监听实现吧:
//Set up load listener. Test attachEvent first because IE9 has
//a subtle issue in its addEventListener and script onload firings
//that do not match the behavior of all other browsers with
//addEventListener support, which fire the onload event for a
//script right after the script execution. See:
//https://connect.microsoft.com/IE/feedback/details/648057/script-onload-event-is-not-fired-immediately-after-script-execution
//UNFORTUNATELY Opera implements attachEvent but does not follow the script
//script execution mode.
if (node.attachEvent &&
//Check if node.attachEvent is artificially added by custom script or
//natively supported by browser
//read https://github.com/jrburke/requirejs/issues/187
//if we can NOT find [native code] then it must NOT natively supported.
//in IE8, node.attachEvent does not have toString()
//Note the test for "[native code" with no closing brace, see:
//https://github.com/jrburke/requirejs/issues/273
!(node.attachEvent.toString && node.attachEvent.toString().indexOf('[native code') < 0)
&& !isOpera) {
//Probably IE. IE (at least 6-8) do not fire
//script onload right after executing the script, so
//we cannot tie the anonymous define call to a name.
//However, IE reports the script as being in 'interactive'
//readyState at the time of the define call.
useInteractive = true;
node.attachEvent('onreadystatechange', context.onScriptLoad);
//It would be great to add an error handler here to catch
//404s in IE9+. However, onreadystatechange will fire before
//the error handler, so that does not help. If addEventListener
//is used, then IE will fire error before load, but we cannot
//use that pathway given the connect.microsoft.com issue
//mentioned above about not doing the 'script execute,
//then fire the script load event listener before execute
//next script' that other browsers do.
//Best hope: IE10 fixes the issues,
//and then destroys all installs of IE 6-9.
//node.attachEvent('onerror', context.onScriptError);
} else {
node.addEventListener('load', context.onScriptLoad, false);
node.addEventListener('error', context.onScriptError, false);
}
从上面的注释可以明白对于IE6-8都是采用的onreadystatechange来监听加载成功事件,而对于404的事件是没有办法做出区分的,所以也就没能完成此功能,只是对于高级浏览器比如chrome之类的采用了onload监听成功事件,onerror监听失败事件。由于主要针对js文件的加载,所以也没有针对css文件的加载做出明确的实现。
三.YUI3
YUI3作为一款非常优秀的js框架,其框架的代码都经过了yahoo的使用场景考验,应该是非常完善和趋于完美的实现了,那我们同样来窥探一下他实现异步获取文件的模块代码吧:
// Inject the node.
if (isScript && ua.ie && (ua.ie < 9 || (document.documentMode && document.documentMode < 9))) {
// Script on IE < 9, and IE 9+ when in IE 8 or older modes, including quirks mode.
node.onreadystatechange = function () {
if (/loaded|complete/.test(node.readyState)) {
node.onreadystatechange = null;
onLoad();
}
};
} else if (!isScript && !env.cssLoad) {
// CSS on Firefox <9 or WebKit.
this._poll(req);
} else {
// Script or CSS on everything else. Using DOM 0 events because that
// evens the playing field with older IEs.
if (ua.ie >= 10) {
// We currently need to introduce a timeout for IE10, since it
// calls onerror/onload synchronously for 304s - messing up existing
// program flow.
// Remove this block if the following bug gets fixed by GA
// https://connect.microsoft.com/IE/feedback/details/763871/dynamically-loaded-scripts-with-304s-responses-interrupt-the-currently-executing-js-thread-onload
node.onerror = function() { setTimeout(onError, 0); };
node.onload = function() { setTimeout(onLoad, 0); };
} else {
node.onerror = onError;
node.onload = onLoad;
}
// If this browser doesn't fire an event when CSS fails to load,
// fail after a timeout to avoid blocking the transaction queue.
if (!env.cssFail && !isScript) {
cssTimeout = setTimeout(onError, req.timeout || 3000);
}
}
this.nodes.push(node);
insertBefore.parentNode.insertBefore(node, insertBefore);
从YUI3的实现来看完成了js和css文件的加载,基本上符合我们的要求。对于js文件,在IE6-8用onreadystatechange事件来监听,但没有办法监听到error事件所以放弃了;其他浏览器则通过onload和onerror来实现,基本上和上面LABjs和Requirejs类似。对于css文件,在IE6-8上面同样采用的是onreadystatechange来实现,并且同样没办法来实现error事件的监听;其他浏览器如果支持onload事件则采用此方法,如果不支持(比如firefox<7和一些低版本的webkit内核)则只能通过不断的轮询css节点来实现了。从注释当中可以看出,在IE10下面服务器缓存设置返回304的时候有一个bug,需要通过异步的方式来触发监听方法,具体可以再测试一下。YUI3中对于css加载的轮询方式如下:
if (isWebKit) {
// Look for a stylesheet matching the pending URL.
sheets = req.doc.styleSheets;
j = sheets.length;
nodeHref = req.node.href;
while (--j >= 0) {
if (sheets[j].href === nodeHref) {
pendingCSS.splice(i, 1);
i -= 1;
self._progress(null, req);
break;
}
}
} else {
// Many thanks to Zach Leatherman for calling my attention to
// the @import-based cross-domain technique used here, and to
// Oleg Slobodskoi for an earlier same-domain implementation.
//
// See Zach's blog for more details:
// http://www.zachleat.com/web/2010/07/29/load-css-dynamically/
try {
// We don't really need to store this value since we never
// use it again, but if we don't store it, Closure Compiler
// assumes the code is useless and removes it.
hasRules = !!req.node.sheet.cssRules;
// If we get here, the stylesheet has loaded.
pendingCSS.splice(i, 1);
i -= 1;
self._progress(null, req);
} catch (ex) {
// An exception means the stylesheet is still loading.
}
}
从上面轮询的方式来看,对于webkit内核的则通过检查style的sheet节点是否附加上了来测试,其他比如firefox和opera则通过检查sheet.cssRules是否生效来完成。但是始终都是没有办法解决404的问题。所以也就只能这样了。。。
四.jQuery
大名鼎鼎的jQuery在实现ajax封装了所有的异步加载功能的时候,为script加载专门分了文件的,具体可以看到如下实现:
script = jQuery("<script>").prop({
async: true,
charset: s.scriptCharset,
src: s.url
}).on(
"load error",
callback = function( evt ) {
script.remove();
callback = null;
if ( evt ) {
complete( evt.type === "error" ? 404 : 200, evt.type );
}
}
);
document.head.appendChild( script[ 0 ] );
从上面代码看基本上和上面类似,看来对于js而言没有什么太多的方法,所以基本上按照以上几种实现即可。jquery并没有对css文件加载做专门的处理,所以还无从参考。
五.Seajs
Seajs在阿里系还是有很大的使用范围的,并且目前推广的还不错,所以陆续有很多公司开始采用了。其也主要是推行模块化开发的方式,因此也会涉及到异步记载模块文件的方式,所以也涉及到了文件的异步加载。其request模块实现如下:
function addOnload(node, callback, isCSS) {
var missingOnload = isCSS && (isOldWebKit || !("onload" in node))
// for Old WebKit and Old Firefox
if (missingOnload) {
setTimeout(function() {
pollCss(node, callback)
}, 1) // Begin after node insertion
return
}
node.onload = node.onerror = node.onreadystatechange = function() {
if (READY_STATE_RE.test(node.readyState)) {
// Ensure only run once and handle memory leak in IE
node.onload = node.onerror = node.onreadystatechange = null
// Remove the script to reduce memory leak
if (!isCSS && !configData.debug) {
head.removeChild(node)
}
// Dereference the node
node = undefined
callback()
}
}
}
function pollCss(node, callback) {
var sheet = node.sheet
var isLoaded
// for WebKit < 536
if (isOldWebKit) {
if (sheet) {
isLoaded = true
}
}
// for Firefox < 9.0
else if (sheet) {
try {
if (sheet.cssRules) {
isLoaded = true
}
} catch (ex) {
// The value of `ex.name` is changed from "NS_ERROR_DOM_SECURITY_ERR"
// to "SecurityError" since Firefox 13.0. But Firefox is less than 9.0
// in here, So it is ok to just rely on "NS_ERROR_DOM_SECURITY_ERR"
if (ex.name === "NS_ERROR_DOM_SECURITY_ERR") {
isLoaded = true
}
}
}
setTimeout(function() {
if (isLoaded) {
// Place callback here to give time for style rendering
callback()
}
else {
pollCss(node, callback)
}
}, 20)
}
从seajs的实现来看,主要完成了js和css的异步加载,其主要实现还是和YUI3的get模块实现方式基本一致。并且实现方式还是简单粗暴的,具体细节还不如YUI3的实现精细,但是对于大多数场景还是够用了的。
另外从labjs和seajs上面可以注意一个细节,为了防止内存溢出,还是在js文件加载完毕之后会删除其对应的script节点。因为对于js而言已经执行,其内存中已经保存了相关的环境变量,css文件则不一样删除则会将对应的style样式一并清除。
—————分割线———
上面讨论了几种实现方式,看来js都比较好处理,大家也都实现的很简单,主要分IE6-8采用onreadystatechange事件,判断readystate状态来完成;其他浏览器则通过监听onload事件来完成,但都无法完全通过onerror事件来监听404状态。对于css文件则实现比较难一点,如果浏览器本身支持onload方法便好说,不支持则通过轮询sheet的cssRules是否生效或者对应的节点是否生成。难道就没有好一点的办法么?
通过google查询,可以通过new Image()的方式来加载css文件的地址,然后由于mime类型错误,所以会触发img的onerror事件从而来达到模拟css文件加载成功的事件,如果不支持此方法的再没办法的采用轮询的方式来完成。那到底什么时候采用此方式呢?为此,我做了一个测试页面,来测试各种浏览器的对于js和css文件的异步加载事件的支持情况,具体代码如下:
<!DOCTYPE html>
<html>
<head>
<title>test</title>
</head>
<body>
<div id="testresult" style="margin:50px;">-------------------------start----------------------<br/></div>
<script>
/**
*@fileoverview the loader plugin for asynchronous loading resources
*@author ginano
*@website
*@date 20130228
*/
(function(){
var headEl=document.getElementsByTagName("head")[0],
dom=document.getElementById('testresult');
var Loader={
/**
*加载js文件
* @param {Object} url
*/
importJS:function(url,str){
var head ,
script;
head = headEl;
script = document.createElement("script");
script.type = "text/javascript";
script.onreadystatechange=function(){
dom.innerHTML+='suppport:js-readystatechange-'+this.readyState+' event-----------------------'+str+'<br/>';
};
script.onload=function(){
dom.innerHTML+='suppport:js-load event-----------------------'+str+'<br/>';
};
script.onerror=function(){
dom.innerHTML+='suppport:js-error event-----------------------'+str+'<br/>';
};
script.src = url;
head.appendChild(script);
},
/**
*加载css文件
* @param {Object} url
*/
importCSS:function(url,str){
var head,
link,
img,
ua;
head = headEl;
link = document.createElement("link");
link.rel="stylesheet";
link.type = "text/css";
link.href=url;
link.onerror=function(){
dom.innerHTML+='<div style="color:green">suppport:css-error event-----------------------'+str+'<br/></div>';
};
link.onload=function(){
dom.innerHTML+='<div style="color:green">suppport:css-load event-----------------------'+str+'<br/></div>';
};
link.onreadystatechange=function(){
dom.innerHTML+='<div style="color:green">suppport:css-readystatechange-'+this.readyState+' event-----------------------'+str+'<br/></div>';
};
head.appendChild(link);
img=document.createElement('img');
img.onerror=function(){
dom.innerHTML+='<div style="color:green">suppport:css-img-error event-----------------------'+str+'<br/></div>';
};
img.src=url;
}
};
dom.innerHTML+='browser Info:'+window.navigator.userAgent+'<br/>';
//测试正常文件
Loader.importJS(
'http://yui.yahooapis.com/2.9.0/build/yahoo/yahoo-min.js',
'rightJS'
);
//测试404js文件
Loader.importJS(
'http://www.ginano.net/1.js',
'wrongJS'
);
//测试css文件
Loader.importCSS(
'http://yui.yahooapis.com/2.9.0/build/fonts/fonts.css',
'rightCSS'
);
//测试404css文件
Loader.importCSS(
'http://www.ginano.net/1.css',
'wrongCSS'
);
})();
</script>
</body>
</html>
由于需要各种浏览器测试结果,所以在http://browsershots.org/上面打开测试页面http://www.ginano.net/test-browser-load-js-css-event.html ,跑了半天每个浏览器都有一张如下所示的截屏。
该平台支持173种浏览器,通过将结果整理得到如下的数据:
事件支持情况
浏览器
onreadystatechange
onload
onerror
Img.onerror
5~8
JS
200
Loading,loaded,第二次为complete
–
–
–
404
Loading,loaded,第二次为complete
–
–
–
CSS
200
Loading,complete
ok
–
ok
404
Loading,complete
ok
–
ok
9.0-10
JS
200
Loading,loaded
ok
–
–
404
Loading,loaded
–
ok
–
CSS
200
Loading,complete
ok
–
ok
404
Loading,complete
ok
–
ok
Chrome
1~9
(webkit:530-534)
JS
200
–
ok
–
–
404
–
–
ok
–
CSS
200
–
–
–
–
404
–
–
–
–
10~19
(webkit:534.15-535.21)
JS
200
–
ok
–
–
404
–
–
ok
CSS
200
–
–
–
ok
404
–
–
–
ok
20~26
(webkit:536.11-537.11)
JS
200
–
Ok
–
–
404
–
–
ok
–
CSS
200
–
ok
–
ok
404
–
–
ok
ok
Firefox
1~8
JS
200
–
ok
–
–
404
–
–
ok
–
CSS
200
–
–
–
ok
404
–
–
–
ok
9~20
JS
200
–
Ok
–
–
404
–
–
Ok
–
CSS
200
–
Ok
–
ok
404
–
–
ok
ok
Opera
<=11.61
JS
200
Loaded
Ok
–
–
404
–
–
–
–
CSS
200
Undefined?
Ok
–
ok
404
–
–
–
ok
404
–
–
–
ok
备注:
在9.64版本,js两种情况都还会触发onreadystatechange-interactive;
在11.61版本中,js-404会触发onerror事件。
11.64~12.50
JS
200
–
OK
–
–
404
–
–
OK
–
CSS
200
–
OK
–
OK
404
–
–
–
OK
Safari
3.2.3
(webkit:525.28)
JS
200
–
ok
–
–
404
–
ok
–
–
CSS
200
–
–
–
–
404
–
–
–
–
5.0
(webkit:533-534)
JS
200
–
ok
–
–
404
–
–
ok
–
CSS
200
–
–
–
ok
404
–
–
–
ok
6.0
(webkit:536)
JS
200
–
ok
–
–
404
–
ok
CSS
200
–
ok
–
ok
404
–
–
ok
OK
从上面的结果总结规律如下:
1.js文件
1.1 IE8及以下版本,通过onreadystatechange事件监听,判断readystate状态是否为load或者complete从而触发成功事件。具体可查阅上表
1.2 其他浏览器直接通过onload事件即可完成加载成功事件的监听。
1.3 由于始终无法保证onerror事件的支持,只是对能够支持的加上即可
2.css文件
2.1 所有浏览器对onerror的支持都不完美,所以只是尽量处理
2.2 IE浏览器/firefox9.0级以上/opera/chrome浏览器20及以上/safari浏览器6.0以上都支持css的onload事件,因此通过监听onload即可。
2.3 chrome浏览器9.10到19.0/safari浏览器5.0到5.9/firefox浏览器8.9一下则通过img的onerror事件即可模拟出css文件的加载成功事件
2.4 其他浏览器,比如chrome浏览器9.0及以下则只能通过轮询css样式节点是否附加成功来判断了
备注:YUI3的注释当中提到了IE10的bug不知道修复与否,但是目前测试结果是ok的所以没有做单独处理。
鉴于上面的基本规律,我再问的文件加载器模块当中的实现代码如下:
/**
*@fileoverview the loader plugin for asynchronous loading resources
* there isn't method to resolve the problem of 404
*@author ginano
*@website
*@date 20130228
*/
define('modules/loader',[
'modules/class',
'modules/ua',
'modules/util'
],function(Class,UA,Util){
var LoadedList={},
headEl=document.getElementsByTagName("head")[0],
isFunction=function(f){
return f instanceof Function;
};
var Loader=new Class('modules/loader',{
/**
*加载js文件
* @param {Object} url
*/
static__importJS:function(url,callback){
var head ,
script,
//成功之后做的事情
wellDone=function(){
LoadedList[url]=true;
clear();
Util.log('load js file success:'+url);
callback();
},
clear=function(){
script.onload=script.onreadystatechange=script.onerror=null;
head.removeChild(script);
head=script=null;
};
if(LoadedList[url]){
isFunction(callback)&&callback();
return;
}
head = headEl;
script = document.createElement("script");
script.type = "text/javascript";
script.onerror=function(){
clear();
Util.log('load js file error:'+url);
};
if(isFunction(callback)){
//如果是IE6-IE8
if(UA.browser=='ie' && UA.version<9){
script.onreadystatechange=function(){
//当第一次访问的时候是loaded,第二次缓存访问是complete
if(/loaded|complete/.test(script.readyState)){
wellDone();
}
}
}else{
script.onload=function(){
wellDone();
}
}
//始终保证callback必须执行,所以需要定时器去完成,测试结果表明早期的大量的浏览器还不支持
//timer=setTimeout(function(){
// wellDone();
//},10000);
}
script.src = url;
head.appendChild(script);
},
/**
*加载css文件
* @param {Object} url
*/
static__importCSS:function(url,callback){
var head,
link,
img,
firefox,
opera,
chrome,
poll,
//成功之后做的事情
wellDone=function(){
LoadedList[url]=true;
clear();
Util.log('load css file success:'+url);
callback();
},
clear=function(){
timer=null;
link.onload=link.onerror=null;
head=null;
};
if(LoadedList[url]){
isFunction(callback)&&callback();
return;
}
head = headEl;
link = document.createElement("link");
link.rel="stylesheet";
link.type = "text/css";
link.href=url;
link.onerror=function(){
clear();
Util.log('load css file error:'+url);
};
if(isFunction(callback)){
//如果是IE系列,直接load事件
if(UA.browser=='ie'
|| (UA.browser=='firefox' && UA.version>8.9)
|| UA.browser=='opera'
|| (UA.browser=='chrome' && UA.version>19)
|| (UA.browser=='safari' && UA.version>5.9)
){
//IE和opera浏览器用img实现
link.onload=function(){
wellDone();
};
head.appendChild(link);
}else if(
(UA.browser=='chrome' && UA.version>9)
|| (UA.browser=='safari' && UA.version>4.9)
|| UA.browser=='firefox'
){
head.appendChild(link);
//如果是非IE系列
img=document.createElement('img');
img.onerror=function(){
img.onerror=null;
img=null;
wellDone();
};
img.src=url;
}else{//轮询实现
head.appendChild(link);
poll=function(){
if(link.sheet && link.sheet.cssRules){
wellDone();
}else{
setTimeout(poll,300);
}
};
poll();
}
}else{
head.appendChild(link);
}
},
/**
*异步加载所需的文件
* @param {Array} urls
* @param {Function} callback
* @param {Boolean} [option=true] isOrdered 是否需要按序加载,默认是需要按序加载
*/
static__asyncLoad:function(urls,callback,isOrdered){
var _self=this,
isOrder=!(isOrdered===false),
isAllDone=false,
now,
i,
urls= ('string'===typeof urls)?[urls]:urls;
len=(urls instanceof Array) && urls.length,
/**
*根据后缀判断是js还是css文件
* @param {Object} url
* @param {Object} done
*/
load=function(url, done){
if(/\.js(?:\?\S+|#\S+)?$/.test(url)){
_self.importJS(url,done);
}else{
_self.importCSS(url,done);
}
},
orderLoad=function(){
now=urls.shift();
if(now){
load(now,orderLoad);
}else{
callback && callback();
}
};
if(!len || len<1){
return;
}
//如果有顺序
if(isOrder){
orderLoad();
}else{
//如果没有顺序加载
for(i=0,now=0;i<len;i++){
load(urls[i],function(){
now+=1;
if(now==len){
callback && callback();
}
});
}
}
}
});
return Loader;
});
经过测试以上实现方式还是具有非常好的兼容性的,如果大家测试有什么bug可以尽管在评论中予以指正。
一.LABjs
这个项目位于github上面,其本意就是Loading And Blocking JavaScript,就是一个异步脚本加载器,相对于原始的粗暴script标签方式而言,提供了弹性的性能优化(做到js文件在浏览器中尽可能的并行加载,并且能够提供顺序执行的保证),还能在高级浏览器中实现先加载然后执行的功能。作为一个异步js加载器还是非常优秀的。其在异步加载的成功事件响应方面的实现如下:
// creates a script load listener
function create_script_load_listener(elem,registry_item,flag,onload) {
elem.onload = elem.onreadystatechange = function() {
if ((elem.readyState && elem.readyState != "complete" && elem.readyState != "loaded")
|| registry_item[flag]) return;
elem.onload = elem.onreadystatechange = null;
onload();
};
}
从上面可见,基本上就是利用onload事件和onreadystatechange事件来完成的,最后一个registry_item[flag]就是对同源的文件可以通过AJAX来实现的。但是没有涉及到css文件的加载,也没提到js文件不存在的时候如何来检测。
二.RequireJS
RequireJS主要定位于a file and module loader for javascript,就是作为一种模块化开发过程中的模块加载器,由于模块是以文件形式存在,所以也就相当于一个文件加载器,但是实现了模块管理的功能。其主要也仍然是在处理js文件的加载,没有考虑css文件的加载。那我们看一下他主要的事件监听实现吧:
//Set up load listener. Test attachEvent first because IE9 has
//a subtle issue in its addEventListener and script onload firings
//that do not match the behavior of all other browsers with
//addEventListener support, which fire the onload event for a
//script right after the script execution. See:
//https://connect.microsoft.com/IE/feedback/details/648057/script-onload-event-is-not-fired-immediately-after-script-execution
//UNFORTUNATELY Opera implements attachEvent but does not follow the script
//script execution mode.
if (node.attachEvent &&
//Check if node.attachEvent is artificially added by custom script or
//natively supported by browser
//read https://github.com/jrburke/requirejs/issues/187
//if we can NOT find [native code] then it must NOT natively supported.
//in IE8, node.attachEvent does not have toString()
//Note the test for "[native code" with no closing brace, see:
//https://github.com/jrburke/requirejs/issues/273
!(node.attachEvent.toString && node.attachEvent.toString().indexOf('[native code') < 0)
&& !isOpera) {
//Probably IE. IE (at least 6-8) do not fire
//script onload right after executing the script, so
//we cannot tie the anonymous define call to a name.
//However, IE reports the script as being in 'interactive'
//readyState at the time of the define call.
useInteractive = true;
node.attachEvent('onreadystatechange', context.onScriptLoad);
//It would be great to add an error handler here to catch
//404s in IE9+. However, onreadystatechange will fire before
//the error handler, so that does not help. If addEventListener
//is used, then IE will fire error before load, but we cannot
//use that pathway given the connect.microsoft.com issue
//mentioned above about not doing the 'script execute,
//then fire the script load event listener before execute
//next script' that other browsers do.
//Best hope: IE10 fixes the issues,
//and then destroys all installs of IE 6-9.
//node.attachEvent('onerror', context.onScriptError);
} else {
node.addEventListener('load', context.onScriptLoad, false);
node.addEventListener('error', context.onScriptError, false);
}
从上面的注释可以明白对于IE6-8都是采用的onreadystatechange来监听加载成功事件,而对于404的事件是没有办法做出区分的,所以也就没能完成此功能,只是对于高级浏览器比如chrome之类的采用了onload监听成功事件,onerror监听失败事件。由于主要针对js文件的加载,所以也没有针对css文件的加载做出明确的实现。
三.YUI3
YUI3作为一款非常优秀的js框架,其框架的代码都经过了yahoo的使用场景考验,应该是非常完善和趋于完美的实现了,那我们同样来窥探一下他实现异步获取文件的模块代码吧:
// Inject the node.
if (isScript && ua.ie && (ua.ie < 9 || (document.documentMode && document.documentMode < 9))) {
// Script on IE < 9, and IE 9+ when in IE 8 or older modes, including quirks mode.
node.onreadystatechange = function () {
if (/loaded|complete/.test(node.readyState)) {
node.onreadystatechange = null;
onLoad();
}
};
} else if (!isScript && !env.cssLoad) {
// CSS on Firefox <9 or WebKit.
this._poll(req);
} else {
// Script or CSS on everything else. Using DOM 0 events because that
// evens the playing field with older IEs.
if (ua.ie >= 10) {
// We currently need to introduce a timeout for IE10, since it
// calls onerror/onload synchronously for 304s - messing up existing
// program flow.
// Remove this block if the following bug gets fixed by GA
// https://connect.microsoft.com/IE/feedback/details/763871/dynamically-loaded-scripts-with-304s-responses-interrupt-the-currently-executing-js-thread-onload
node.onerror = function() { setTimeout(onError, 0); };
node.onload = function() { setTimeout(onLoad, 0); };
} else {
node.onerror = onError;
node.onload = onLoad;
}
// If this browser doesn't fire an event when CSS fails to load,
// fail after a timeout to avoid blocking the transaction queue.
if (!env.cssFail && !isScript) {
cssTimeout = setTimeout(onError, req.timeout || 3000);
}
}
this.nodes.push(node);
insertBefore.parentNode.insertBefore(node, insertBefore);
从YUI3的实现来看完成了js和css文件的加载,基本上符合我们的要求。对于js文件,在IE6-8用onreadystatechange事件来监听,但没有办法监听到error事件所以放弃了;其他浏览器则通过onload和onerror来实现,基本上和上面LABjs和Requirejs类似。对于css文件,在IE6-8上面同样采用的是onreadystatechange来实现,并且同样没办法来实现error事件的监听;其他浏览器如果支持onload事件则采用此方法,如果不支持(比如firefox<7和一些低版本的webkit内核)则只能通过不断的轮询css节点来实现了。从注释当中可以看出,在IE10下面服务器缓存设置返回304的时候有一个bug,需要通过异步的方式来触发监听方法,具体可以再测试一下。YUI3中对于css加载的轮询方式如下:
if (isWebKit) {
// Look for a stylesheet matching the pending URL.
sheets = req.doc.styleSheets;
j = sheets.length;
nodeHref = req.node.href;
while (--j >= 0) {
if (sheets[j].href === nodeHref) {
pendingCSS.splice(i, 1);
i -= 1;
self._progress(null, req);
break;
}
}
} else {
// Many thanks to Zach Leatherman for calling my attention to
// the @import-based cross-domain technique used here, and to
// Oleg Slobodskoi for an earlier same-domain implementation.
//
// See Zach's blog for more details:
// http://www.zachleat.com/web/2010/07/29/load-css-dynamically/
try {
// We don't really need to store this value since we never
// use it again, but if we don't store it, Closure Compiler
// assumes the code is useless and removes it.
hasRules = !!req.node.sheet.cssRules;
// If we get here, the stylesheet has loaded.
pendingCSS.splice(i, 1);
i -= 1;
self._progress(null, req);
} catch (ex) {
// An exception means the stylesheet is still loading.
}
}
从上面轮询的方式来看,对于webkit内核的则通过检查style的sheet节点是否附加上了来测试,其他比如firefox和opera则通过检查sheet.cssRules是否生效来完成。但是始终都是没有办法解决404的问题。所以也就只能这样了。。。
四.jQuery
大名鼎鼎的jQuery在实现ajax封装了所有的异步加载功能的时候,为script加载专门分了文件的,具体可以看到如下实现:
script = jQuery("<script>").prop({
async: true,
charset: s.scriptCharset,
src: s.url
}).on(
"load error",
callback = function( evt ) {
script.remove();
callback = null;
if ( evt ) {
complete( evt.type === "error" ? 404 : 200, evt.type );
}
}
);
document.head.appendChild( script[ 0 ] );
从上面代码看基本上和上面类似,看来对于js而言没有什么太多的方法,所以基本上按照以上几种实现即可。jquery并没有对css文件加载做专门的处理,所以还无从参考。
五.Seajs
Seajs在阿里系还是有很大的使用范围的,并且目前推广的还不错,所以陆续有很多公司开始采用了。其也主要是推行模块化开发的方式,因此也会涉及到异步记载模块文件的方式,所以也涉及到了文件的异步加载。其request模块实现如下:
function addOnload(node, callback, isCSS) {
var missingOnload = isCSS && (isOldWebKit || !("onload" in node))
// for Old WebKit and Old Firefox
if (missingOnload) {
setTimeout(function() {
pollCss(node, callback)
}, 1) // Begin after node insertion
return
}
node.onload = node.onerror = node.onreadystatechange = function() {
if (READY_STATE_RE.test(node.readyState)) {
// Ensure only run once and handle memory leak in IE
node.onload = node.onerror = node.onreadystatechange = null
// Remove the script to reduce memory leak
if (!isCSS && !configData.debug) {
head.removeChild(node)
}
// Dereference the node
node = undefined
callback()
}
}
}
function pollCss(node, callback) {
var sheet = node.sheet
var isLoaded
// for WebKit < 536
if (isOldWebKit) {
if (sheet) {
isLoaded = true
}
}
// for Firefox < 9.0
else if (sheet) {
try {
if (sheet.cssRules) {
isLoaded = true
}
} catch (ex) {
// The value of `ex.name` is changed from "NS_ERROR_DOM_SECURITY_ERR"
// to "SecurityError" since Firefox 13.0. But Firefox is less than 9.0
// in here, So it is ok to just rely on "NS_ERROR_DOM_SECURITY_ERR"
if (ex.name === "NS_ERROR_DOM_SECURITY_ERR") {
isLoaded = true
}
}
}
setTimeout(function() {
if (isLoaded) {
// Place callback here to give time for style rendering
callback()
}
else {
pollCss(node, callback)
}
}, 20)
}
从seajs的实现来看,主要完成了js和css的异步加载,其主要实现还是和YUI3的get模块实现方式基本一致。并且实现方式还是简单粗暴的,具体细节还不如YUI3的实现精细,但是对于大多数场景还是够用了的。
另外从labjs和seajs上面可以注意一个细节,为了防止内存溢出,还是在js文件加载完毕之后会删除其对应的script节点。因为对于js而言已经执行,其内存中已经保存了相关的环境变量,css文件则不一样删除则会将对应的style样式一并清除。
—————分割线———
上面讨论了几种实现方式,看来js都比较好处理,大家也都实现的很简单,主要分IE6-8采用onreadystatechange事件,判断readystate状态来完成;其他浏览器则通过监听onload事件来完成,但都无法完全通过onerror事件来监听404状态。对于css文件则实现比较难一点,如果浏览器本身支持onload方法便好说,不支持则通过轮询sheet的cssRules是否生效或者对应的节点是否生成。难道就没有好一点的办法么?
通过google查询,可以通过new Image()的方式来加载css文件的地址,然后由于mime类型错误,所以会触发img的onerror事件从而来达到模拟css文件加载成功的事件,如果不支持此方法的再没办法的采用轮询的方式来完成。那到底什么时候采用此方式呢?为此,我做了一个测试页面,来测试各种浏览器的对于js和css文件的异步加载事件的支持情况,具体代码如下:
<!DOCTYPE html>
<html>
<head>
<title>test</title>
</head>
<body>
<div id="testresult" style="margin:50px;">-------------------------start----------------------<br/></div>
<script>
/**
*@fileoverview the loader plugin for asynchronous loading resources
*@author ginano
*@website
*@date 20130228
*/
(function(){
var headEl=document.getElementsByTagName("head")[0],
dom=document.getElementById('testresult');
var Loader={
/**
*加载js文件
* @param {Object} url
*/
importJS:function(url,str){
var head ,
script;
head = headEl;
script = document.createElement("script");
script.type = "text/javascript";
script.onreadystatechange=function(){
dom.innerHTML+='suppport:js-readystatechange-'+this.readyState+' event-----------------------'+str+'<br/>';
};
script.onload=function(){
dom.innerHTML+='suppport:js-load event-----------------------'+str+'<br/>';
};
script.onerror=function(){
dom.innerHTML+='suppport:js-error event-----------------------'+str+'<br/>';
};
script.src = url;
head.appendChild(script);
},
/**
*加载css文件
* @param {Object} url
*/
importCSS:function(url,str){
var head,
link,
img,
ua;
head = headEl;
link = document.createElement("link");
link.rel="stylesheet";
link.type = "text/css";
link.href=url;
link.onerror=function(){
dom.innerHTML+='<div style="color:green">suppport:css-error event-----------------------'+str+'<br/></div>';
};
link.onload=function(){
dom.innerHTML+='<div style="color:green">suppport:css-load event-----------------------'+str+'<br/></div>';
};
link.onreadystatechange=function(){
dom.innerHTML+='<div style="color:green">suppport:css-readystatechange-'+this.readyState+' event-----------------------'+str+'<br/></div>';
};
head.appendChild(link);
img=document.createElement('img');
img.onerror=function(){
dom.innerHTML+='<div style="color:green">suppport:css-img-error event-----------------------'+str+'<br/></div>';
};
img.src=url;
}
};
dom.innerHTML+='browser Info:'+window.navigator.userAgent+'<br/>';
//测试正常文件
Loader.importJS(
'http://yui.yahooapis.com/2.9.0/build/yahoo/yahoo-min.js',
'rightJS'
);
//测试404js文件
Loader.importJS(
'http://www.ginano.net/1.js',
'wrongJS'
);
//测试css文件
Loader.importCSS(
'http://yui.yahooapis.com/2.9.0/build/fonts/fonts.css',
'rightCSS'
);
//测试404css文件
Loader.importCSS(
'http://www.ginano.net/1.css',
'wrongCSS'
);
})();
</script>
</body>
</html>
由于需要各种浏览器测试结果,所以在http://browsershots.org/上面打开测试页面http://www.ginano.net/test-browser-load-js-css-event.html ,跑了半天每个浏览器都有一张如下所示的截屏。
该平台支持173种浏览器,通过将结果整理得到如下的数据:
事件支持情况
浏览器
onreadystatechange
onload
onerror
Img.onerror
5~8
JS
200
Loading,loaded,第二次为complete
–
–
–
404
Loading,loaded,第二次为complete
–
–
–
CSS
200
Loading,complete
ok
–
ok
404
Loading,complete
ok
–
ok
9.0-10
JS
200
Loading,loaded
ok
–
–
404
Loading,loaded
–
ok
–
CSS
200
Loading,complete
ok
–
ok
404
Loading,complete
ok
–
ok
Chrome
1~9
(webkit:530-534)
JS
200
–
ok
–
–
404
–
–
ok
–
CSS
200
–
–
–
–
404
–
–
–
–
10~19
(webkit:534.15-535.21)
JS
200
–
ok
–
–
404
–
–
ok
CSS
200
–
–
–
ok
404
–
–
–
ok
20~26
(webkit:536.11-537.11)
JS
200
–
Ok
–
–
404
–
–
ok
–
CSS
200
–
ok
–
ok
404
–
–
ok
ok
Firefox
1~8
JS
200
–
ok
–
–
404
–
–
ok
–
CSS
200
–
–
–
ok
404
–
–
–
ok
9~20
JS
200
–
Ok
–
–
404
–
–
Ok
–
CSS
200
–
Ok
–
ok
404
–
–
ok
ok
Opera
<=11.61
JS
200
Loaded
Ok
–
–
404
–
–
–
–
CSS
200
Undefined?
Ok
–
ok
404
–
–
–
ok
404
–
–
–
ok
备注:
在9.64版本,js两种情况都还会触发onreadystatechange-interactive;
在11.61版本中,js-404会触发onerror事件。
11.64~12.50
JS
200
–
OK
–
–
404
–
–
OK
–
CSS
200
–
OK
–
OK
404
–
–
–
OK
Safari
3.2.3
(webkit:525.28)
JS
200
–
ok
–
–
404
–
ok
–
–
CSS
200
–
–
–
–
404
–
–
–
–
5.0
(webkit:533-534)
JS
200
–
ok
–
–
404
–
–
ok
–
CSS
200
–
–
–
ok
404
–
–
–
ok
6.0
(webkit:536)
JS
200
–
ok
–
–
404
–
ok
CSS
200
–
ok
–
ok
404
–
–
ok
OK
从上面的结果总结规律如下:
1.js文件
1.1 IE8及以下版本,通过onreadystatechange事件监听,判断readystate状态是否为load或者complete从而触发成功事件。具体可查阅上表
1.2 其他浏览器直接通过onload事件即可完成加载成功事件的监听。
1.3 由于始终无法保证onerror事件的支持,只是对能够支持的加上即可
2.css文件
2.1 所有浏览器对onerror的支持都不完美,所以只是尽量处理
2.2 IE浏览器/firefox9.0级以上/opera/chrome浏览器20及以上/safari浏览器6.0以上都支持css的onload事件,因此通过监听onload即可。
2.3 chrome浏览器9.10到19.0/safari浏览器5.0到5.9/firefox浏览器8.9一下则通过img的onerror事件即可模拟出css文件的加载成功事件
2.4 其他浏览器,比如chrome浏览器9.0及以下则只能通过轮询css样式节点是否附加成功来判断了
备注:YUI3的注释当中提到了IE10的bug不知道修复与否,但是目前测试结果是ok的所以没有做单独处理。
鉴于上面的基本规律,我再问的文件加载器模块当中的实现代码如下:
/**
*@fileoverview the loader plugin for asynchronous loading resources
* there isn't method to resolve the problem of 404
*@author ginano
*@website
*@date 20130228
*/
define('modules/loader',[
'modules/class',
'modules/ua',
'modules/util'
],function(Class,UA,Util){
var LoadedList={},
headEl=document.getElementsByTagName("head")[0],
isFunction=function(f){
return f instanceof Function;
};
var Loader=new Class('modules/loader',{
/**
*加载js文件
* @param {Object} url
*/
static__importJS:function(url,callback){
var head ,
script,
//成功之后做的事情
wellDone=function(){
LoadedList[url]=true;
clear();
Util.log('load js file success:'+url);
callback();
},
clear=function(){
script.onload=script.onreadystatechange=script.onerror=null;
head.removeChild(script);
head=script=null;
};
if(LoadedList[url]){
isFunction(callback)&&callback();
return;
}
head = headEl;
script = document.createElement("script");
script.type = "text/javascript";
script.onerror=function(){
clear();
Util.log('load js file error:'+url);
};
if(isFunction(callback)){
//如果是IE6-IE8
if(UA.browser=='ie' && UA.version<9){
script.onreadystatechange=function(){
//当第一次访问的时候是loaded,第二次缓存访问是complete
if(/loaded|complete/.test(script.readyState)){
wellDone();
}
}
}else{
script.onload=function(){
wellDone();
}
}
//始终保证callback必须执行,所以需要定时器去完成,测试结果表明早期的大量的浏览器还不支持
//timer=setTimeout(function(){
// wellDone();
//},10000);
}
script.src = url;
head.appendChild(script);
},
/**
*加载css文件
* @param {Object} url
*/
static__importCSS:function(url,callback){
var head,
link,
img,
firefox,
opera,
chrome,
poll,
//成功之后做的事情
wellDone=function(){
LoadedList[url]=true;
clear();
Util.log('load css file success:'+url);
callback();
},
clear=function(){
timer=null;
link.onload=link.onerror=null;
head=null;
};
if(LoadedList[url]){
isFunction(callback)&&callback();
return;
}
head = headEl;
link = document.createElement("link");
link.rel="stylesheet";
link.type = "text/css";
link.href=url;
link.onerror=function(){
clear();
Util.log('load css file error:'+url);
};
if(isFunction(callback)){
//如果是IE系列,直接load事件
if(UA.browser=='ie'
|| (UA.browser=='firefox' && UA.version>8.9)
|| UA.browser=='opera'
|| (UA.browser=='chrome' && UA.version>19)
|| (UA.browser=='safari' && UA.version>5.9)
){
//IE和opera浏览器用img实现
link.onload=function(){
wellDone();
};
head.appendChild(link);
}else if(
(UA.browser=='chrome' && UA.version>9)
|| (UA.browser=='safari' && UA.version>4.9)
|| UA.browser=='firefox'
){
head.appendChild(link);
//如果是非IE系列
img=document.createElement('img');
img.onerror=function(){
img.onerror=null;
img=null;
wellDone();
};
img.src=url;
}else{//轮询实现
head.appendChild(link);
poll=function(){
if(link.sheet && link.sheet.cssRules){
wellDone();
}else{
setTimeout(poll,300);
}
};
poll();
}
}else{
head.appendChild(link);
}
},
/**
*异步加载所需的文件
* @param {Array} urls
* @param {Function} callback
* @param {Boolean} [option=true] isOrdered 是否需要按序加载,默认是需要按序加载
*/
static__asyncLoad:function(urls,callback,isOrdered){
var _self=this,
isOrder=!(isOrdered===false),
isAllDone=false,
now,
i,
urls= ('string'===typeof urls)?[urls]:urls;
len=(urls instanceof Array) && urls.length,
/**
*根据后缀判断是js还是css文件
* @param {Object} url
* @param {Object} done
*/
load=function(url, done){
if(/\.js(?:\?\S+|#\S+)?$/.test(url)){
_self.importJS(url,done);
}else{
_self.importCSS(url,done);
}
},
orderLoad=function(){
now=urls.shift();
if(now){
load(now,orderLoad);
}else{
callback && callback();
}
};
if(!len || len<1){
return;
}
//如果有顺序
if(isOrder){
orderLoad();
}else{
//如果没有顺序加载
for(i=0,now=0;i<len;i++){
load(urls[i],function(){
now+=1;
if(now==len){
callback && callback();
}
});
}
}
}
});
return Loader;
});
经过测试以上实现方式还是具有非常好的兼容性的,如果大家测试有什么bug可以尽管在评论中予以指正。