javascript異步動(dòng)態(tài)加載js和css文件的實(shí)現方法、原理
發(fā)表日期:2015-06-29 文章編輯:本站編輯 來(lái)源:未知 瀏覽次數:
主要幾個(gè)框架或者插件是如何實(shí)現異步加載事件響應的。
一.LABjs
這個(gè)項目位于github上面,其本意就是Loading And Blocking JavaScript,就是一個(gè)異步腳本加載器,相對于原始的粗暴script標簽方式而言,提供了彈性的性能優(yōu)化(做到j(luò )s文件在瀏覽器中盡可能的并行加載,并且能夠提供順序執行的保證),還能在高級瀏覽器中實(shí)現先加載然后執行的功能。作為一個(gè)異步j(luò )s加載器還是非常優(yōu)秀的。其在異步加載的成功事件響應方面的實(shí)現如下:
// 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();
};
}
從上面可見(jiàn),基本上就是利用onload事件和onreadystatechange事件來(lái)完成的,最后一個(gè)registry_item[flag]就是對同源的文件可以通過(guò)AJAX來(lái)實(shí)現的。但是沒(méi)有涉及到css文件的加載,也沒(méi)提到j(luò )s文件不存在的時(shí)候如何來(lái)檢測。
二.RequireJS
RequireJS主要定位于a file and module loader for javascript,就是作為一種模塊化開(kāi)發(fā)過(guò)程中的模塊加載器,由于模塊是以文件形式存在,所以也就相當于一個(gè)文件加載器,但是實(shí)現了模塊管理的功能。其主要也仍然是在處理js文件的加載,沒(méi)有考慮css文件的加載。那我們看一下他主要的事件監聽(tīng)實(shí)現吧:
//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來(lái)監聽(tīng)加載成功事件,而對于404的事件是沒(méi)有辦法做出區分的,所以也就沒(méi)能完成此功能,只是對于高級瀏覽器比如chrome之類(lèi)的采用了onload監聽(tīng)成功事件,onerror監聽(tīng)失敗事件。由于主要針對js文件的加載,所以也沒(méi)有針對css文件的加載做出明確的實(shí)現。
三.YUI3
YUI3作為一款非常優(yōu)秀的js框架,其框架的代碼都經(jīng)過(guò)了yahoo的使用場(chǎng)景考驗,應該是非常完善和趨于完美的實(shí)現了,那我們同樣來(lái)窺探一下他實(shí)現異步獲取文件的模塊代碼吧:
// 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的實(shí)現來(lái)看完成了js和css文件的加載,基本上符合我們的要求。對于js文件,在IE6-8用onreadystatechange事件來(lái)監聽(tīng),但沒(méi)有辦法監聽(tīng)到error事件所以放棄了;其他瀏覽器則通過(guò)onload和onerror來(lái)實(shí)現,基本上和上面LABjs和Requirejs類(lèi)似。對于css文件,在IE6-8上面同樣采用的是onreadystatechange來(lái)實(shí)現,并且同樣沒(méi)辦法來(lái)實(shí)現error事件的監聽(tīng);其他瀏覽器如果支持onload事件則采用此方法,如果不支持(比如firefox<7和一些低版本的webkit內核)則只能通過(guò)不斷的輪詢(xún)css節點(diǎn)來(lái)實(shí)現了。從注釋當中可以看出,在IE10下面服務(wù)器緩存設置返回304的時(shí)候有一個(gè)bug,需要通過(guò)異步的方式來(lái)觸發(fā)監聽(tīng)方法,具體可以再測試一下。YUI3中對于css加載的輪詢(xún)方式如下:
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.
}
}
從上面輪詢(xún)的方式來(lái)看,對于webkit內核的則通過(guò)檢查style的sheet節點(diǎn)是否附加上了來(lái)測試,其他比如firefox和opera則通過(guò)檢查sheet.cssRules是否生效來(lái)完成。但是始終都是沒(méi)有辦法解決404的問(wèn)題。所以也就只能這樣了。。。
四.jQuery
大名鼎鼎的jQuery在實(shí)現ajax封裝了所有的異步加載功能的時(shí)候,為script加載專(zhuān)門(mén)分了文件的,具體可以看到如下實(shí)現:
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 ] );
從上面代碼看基本上和上面類(lèi)似,看來(lái)對于js而言沒(méi)有什么太多的方法,所以基本上按照以上幾種實(shí)現即可。jquery并沒(méi)有對css文件加載做專(zhuān)門(mén)的處理,所以還無(wú)從參考。
五.Seajs
Seajs在阿里系還是有很大的使用范圍的,并且目前推廣的還不錯,所以陸續有很多公司開(kāi)始采用了。其也主要是推行模塊化開(kāi)發(fā)的方式,因此也會(huì )涉及到異步記載模塊文件的方式,所以也涉及到了文件的異步加載。其request模塊實(shí)現如下:
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的實(shí)現來(lái)看,主要完成了js和css的異步加載,其主要實(shí)現還是和YUI3的get模塊實(shí)現方式基本一致。并且實(shí)現方式還是簡(jiǎn)單粗暴的,具體細節還不如YUI3的實(shí)現精細,但是對于大多數場(chǎng)景還是夠用了的。
另外從labjs和seajs上面可以注意一個(gè)細節,為了防止內存溢出,還是在js文件加載完畢之后會(huì )刪除其對應的script節點(diǎn)。因為對于js而言已經(jīng)執行,其內存中已經(jīng)保存了相關(guān)的環(huán)境變量,css文件則不一樣刪除則會(huì )將對應的style樣式一并清除。
—————分割線(xiàn)———
上面討論了幾種實(shí)現方式,看來(lái)js都比較好處理,大家也都實(shí)現的很簡(jiǎn)單,主要分IE6-8采用onreadystatechange事件,判斷readystate狀態(tài)來(lái)完成;其他瀏覽器則通過(guò)監聽(tīng)onload事件來(lái)完成,但都無(wú)法完全通過(guò)onerror事件來(lái)監聽(tīng)404狀態(tài)。對于css文件則實(shí)現比較難一點(diǎn),如果瀏覽器本身支持onload方法便好說(shuō),不支持則通過(guò)輪詢(xún)sheet的cssRules是否生效或者對應的節點(diǎn)是否生成。難道就沒(méi)有好一點(diǎn)的辦法么?
通過(guò)google查詢(xún),可以通過(guò)new Image()的方式來(lái)加載css文件的地址,然后由于mime類(lèi)型錯誤,所以會(huì )觸發(fā)img的onerror事件從而來(lái)達到模擬css文件加載成功的事件,如果不支持此方法的再沒(méi)辦法的采用輪詢(xún)的方式來(lái)完成。那到底什么時(shí)候采用此方式呢?為此,我做了一個(gè)測試頁(yè)面,來(lái)測試各種瀏覽器的對于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/上面打開(kāi)測試頁(yè)面http://www.ginano.net/test-browser-load-js-css-event.html ,跑了半天每個(gè)瀏覽器都有一張如下所示的截屏。
該平臺支持173種瀏覽器,通過(guò)將結果整理得到如下的數據:
事件支持情況
瀏覽器
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兩種情況都還會(huì )觸發(fā)onreadystatechange-interactive;
在11.61版本中,js-404會(huì )觸發(fā)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及以下版本,通過(guò)onreadystatechange事件監聽(tīng),判斷readystate狀態(tài)是否為load或者complete從而觸發(fā)成功事件。具體可查閱上表
1.2 其他瀏覽器直接通過(guò)onload事件即可完成加載成功事件的監聽(tīng)。
1.3 由于始終無(wú)法保證onerror事件的支持,只是對能夠支持的加上即可
2.css文件
2.1 所有瀏覽器對onerror的支持都不完美,所以只是盡量處理
2.2 IE瀏覽器/firefox9.0級以上/opera/chrome瀏覽器20及以上/safari瀏覽器6.0以上都支持css的onload事件,因此通過(guò)監聽(tīng)onload即可。
2.3 chrome瀏覽器9.10到19.0/safari瀏覽器5.0到5.9/firefox瀏覽器8.9一下則通過(guò)img的onerror事件即可模擬出css文件的加載成功事件
2.4 其他瀏覽器,比如chrome瀏覽器9.0及以下則只能通過(guò)輪詢(xún)css樣式節點(diǎn)是否附加成功來(lái)判斷了
備注:YUI3的注釋當中提到了IE10的bug不知道修復與否,但是目前測試結果是ok的所以沒(méi)有做單獨處理。
鑒于上面的基本規律,我再問(wèn)的文件加載器模塊當中的實(shí)現代碼如下:
/**
*@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(){
//當第一次訪(fǎng)問(wèn)的時(shí)候是loaded,第二次緩存訪(fǎng)問(wèn)是complete
if(/loaded|complete/.test(script.readyState)){
wellDone();
}
}
}else{
script.onload=function(){
wellDone();
}
}
//始終保證callback必須執行,所以需要定時(shí)器去完成,測試結果表明早期的大量的瀏覽器還不支持
//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實(shí)現
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{//輪詢(xún)實(shí)現
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{
//如果沒(méi)有順序加載
for(i=0,now=0;i<len;i++){
load(urls[i],function(){
now+=1;
if(now==len){
callback && callback();
}
});
}
}
}
});
return Loader;
});
經(jīng)過(guò)測試以上實(shí)現方式還是具有非常好的兼容性的,如果大家測試有什么bug可以盡管在評論中予以指正。
一.LABjs
這個(gè)項目位于github上面,其本意就是Loading And Blocking JavaScript,就是一個(gè)異步腳本加載器,相對于原始的粗暴script標簽方式而言,提供了彈性的性能優(yōu)化(做到j(luò )s文件在瀏覽器中盡可能的并行加載,并且能夠提供順序執行的保證),還能在高級瀏覽器中實(shí)現先加載然后執行的功能。作為一個(gè)異步j(luò )s加載器還是非常優(yōu)秀的。其在異步加載的成功事件響應方面的實(shí)現如下:
// 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();
};
}
從上面可見(jiàn),基本上就是利用onload事件和onreadystatechange事件來(lái)完成的,最后一個(gè)registry_item[flag]就是對同源的文件可以通過(guò)AJAX來(lái)實(shí)現的。但是沒(méi)有涉及到css文件的加載,也沒(méi)提到j(luò )s文件不存在的時(shí)候如何來(lái)檢測。
二.RequireJS
RequireJS主要定位于a file and module loader for javascript,就是作為一種模塊化開(kāi)發(fā)過(guò)程中的模塊加載器,由于模塊是以文件形式存在,所以也就相當于一個(gè)文件加載器,但是實(shí)現了模塊管理的功能。其主要也仍然是在處理js文件的加載,沒(méi)有考慮css文件的加載。那我們看一下他主要的事件監聽(tīng)實(shí)現吧:
//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來(lái)監聽(tīng)加載成功事件,而對于404的事件是沒(méi)有辦法做出區分的,所以也就沒(méi)能完成此功能,只是對于高級瀏覽器比如chrome之類(lèi)的采用了onload監聽(tīng)成功事件,onerror監聽(tīng)失敗事件。由于主要針對js文件的加載,所以也沒(méi)有針對css文件的加載做出明確的實(shí)現。
三.YUI3
YUI3作為一款非常優(yōu)秀的js框架,其框架的代碼都經(jīng)過(guò)了yahoo的使用場(chǎng)景考驗,應該是非常完善和趨于完美的實(shí)現了,那我們同樣來(lái)窺探一下他實(shí)現異步獲取文件的模塊代碼吧:
// 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的實(shí)現來(lái)看完成了js和css文件的加載,基本上符合我們的要求。對于js文件,在IE6-8用onreadystatechange事件來(lái)監聽(tīng),但沒(méi)有辦法監聽(tīng)到error事件所以放棄了;其他瀏覽器則通過(guò)onload和onerror來(lái)實(shí)現,基本上和上面LABjs和Requirejs類(lèi)似。對于css文件,在IE6-8上面同樣采用的是onreadystatechange來(lái)實(shí)現,并且同樣沒(méi)辦法來(lái)實(shí)現error事件的監聽(tīng);其他瀏覽器如果支持onload事件則采用此方法,如果不支持(比如firefox<7和一些低版本的webkit內核)則只能通過(guò)不斷的輪詢(xún)css節點(diǎn)來(lái)實(shí)現了。從注釋當中可以看出,在IE10下面服務(wù)器緩存設置返回304的時(shí)候有一個(gè)bug,需要通過(guò)異步的方式來(lái)觸發(fā)監聽(tīng)方法,具體可以再測試一下。YUI3中對于css加載的輪詢(xún)方式如下:
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.
}
}
從上面輪詢(xún)的方式來(lái)看,對于webkit內核的則通過(guò)檢查style的sheet節點(diǎn)是否附加上了來(lái)測試,其他比如firefox和opera則通過(guò)檢查sheet.cssRules是否生效來(lái)完成。但是始終都是沒(méi)有辦法解決404的問(wèn)題。所以也就只能這樣了。。。
四.jQuery
大名鼎鼎的jQuery在實(shí)現ajax封裝了所有的異步加載功能的時(shí)候,為script加載專(zhuān)門(mén)分了文件的,具體可以看到如下實(shí)現:
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 ] );
從上面代碼看基本上和上面類(lèi)似,看來(lái)對于js而言沒(méi)有什么太多的方法,所以基本上按照以上幾種實(shí)現即可。jquery并沒(méi)有對css文件加載做專(zhuān)門(mén)的處理,所以還無(wú)從參考。
五.Seajs
Seajs在阿里系還是有很大的使用范圍的,并且目前推廣的還不錯,所以陸續有很多公司開(kāi)始采用了。其也主要是推行模塊化開(kāi)發(fā)的方式,因此也會(huì )涉及到異步記載模塊文件的方式,所以也涉及到了文件的異步加載。其request模塊實(shí)現如下:
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的實(shí)現來(lái)看,主要完成了js和css的異步加載,其主要實(shí)現還是和YUI3的get模塊實(shí)現方式基本一致。并且實(shí)現方式還是簡(jiǎn)單粗暴的,具體細節還不如YUI3的實(shí)現精細,但是對于大多數場(chǎng)景還是夠用了的。
另外從labjs和seajs上面可以注意一個(gè)細節,為了防止內存溢出,還是在js文件加載完畢之后會(huì )刪除其對應的script節點(diǎn)。因為對于js而言已經(jīng)執行,其內存中已經(jīng)保存了相關(guān)的環(huán)境變量,css文件則不一樣刪除則會(huì )將對應的style樣式一并清除。
—————分割線(xiàn)———
上面討論了幾種實(shí)現方式,看來(lái)js都比較好處理,大家也都實(shí)現的很簡(jiǎn)單,主要分IE6-8采用onreadystatechange事件,判斷readystate狀態(tài)來(lái)完成;其他瀏覽器則通過(guò)監聽(tīng)onload事件來(lái)完成,但都無(wú)法完全通過(guò)onerror事件來(lái)監聽(tīng)404狀態(tài)。對于css文件則實(shí)現比較難一點(diǎn),如果瀏覽器本身支持onload方法便好說(shuō),不支持則通過(guò)輪詢(xún)sheet的cssRules是否生效或者對應的節點(diǎn)是否生成。難道就沒(méi)有好一點(diǎn)的辦法么?
通過(guò)google查詢(xún),可以通過(guò)new Image()的方式來(lái)加載css文件的地址,然后由于mime類(lèi)型錯誤,所以會(huì )觸發(fā)img的onerror事件從而來(lái)達到模擬css文件加載成功的事件,如果不支持此方法的再沒(méi)辦法的采用輪詢(xún)的方式來(lái)完成。那到底什么時(shí)候采用此方式呢?為此,我做了一個(gè)測試頁(yè)面,來(lái)測試各種瀏覽器的對于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/上面打開(kāi)測試頁(yè)面http://www.ginano.net/test-browser-load-js-css-event.html ,跑了半天每個(gè)瀏覽器都有一張如下所示的截屏。
該平臺支持173種瀏覽器,通過(guò)將結果整理得到如下的數據:
事件支持情況
瀏覽器
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兩種情況都還會(huì )觸發(fā)onreadystatechange-interactive;
在11.61版本中,js-404會(huì )觸發(fā)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及以下版本,通過(guò)onreadystatechange事件監聽(tīng),判斷readystate狀態(tài)是否為load或者complete從而觸發(fā)成功事件。具體可查閱上表
1.2 其他瀏覽器直接通過(guò)onload事件即可完成加載成功事件的監聽(tīng)。
1.3 由于始終無(wú)法保證onerror事件的支持,只是對能夠支持的加上即可
2.css文件
2.1 所有瀏覽器對onerror的支持都不完美,所以只是盡量處理
2.2 IE瀏覽器/firefox9.0級以上/opera/chrome瀏覽器20及以上/safari瀏覽器6.0以上都支持css的onload事件,因此通過(guò)監聽(tīng)onload即可。
2.3 chrome瀏覽器9.10到19.0/safari瀏覽器5.0到5.9/firefox瀏覽器8.9一下則通過(guò)img的onerror事件即可模擬出css文件的加載成功事件
2.4 其他瀏覽器,比如chrome瀏覽器9.0及以下則只能通過(guò)輪詢(xún)css樣式節點(diǎn)是否附加成功來(lái)判斷了
備注:YUI3的注釋當中提到了IE10的bug不知道修復與否,但是目前測試結果是ok的所以沒(méi)有做單獨處理。
鑒于上面的基本規律,我再問(wèn)的文件加載器模塊當中的實(shí)現代碼如下:
/**
*@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(){
//當第一次訪(fǎng)問(wèn)的時(shí)候是loaded,第二次緩存訪(fǎng)問(wèn)是complete
if(/loaded|complete/.test(script.readyState)){
wellDone();
}
}
}else{
script.onload=function(){
wellDone();
}
}
//始終保證callback必須執行,所以需要定時(shí)器去完成,測試結果表明早期的大量的瀏覽器還不支持
//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實(shí)現
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{//輪詢(xún)實(shí)現
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{
//如果沒(méi)有順序加載
for(i=0,now=0;i<len;i++){
load(urls[i],function(){
now+=1;
if(now==len){
callback && callback();
}
});
}
}
}
});
return Loader;
});
經(jīng)過(guò)測試以上實(shí)現方式還是具有非常好的兼容性的,如果大家測試有什么bug可以盡管在評論中予以指正。