note:本文主要參考了Stoyan Stefanov的文章《When is a stylesheet really loaded?》
在之前的文章《CSS文件動態加載》中,我們提到了在動態加載CSS文件的時候,如何檢測加載是否完成。注意,這里的加載完成包含了兩種情況:
1)加載成功 2)加載失敗
也就是說,這里並沒有將成功與失敗的情況區分開來。看到這里你可能疑惑了,就動態加載個CSS文件,洋洋灑灑寫了一兩百行代碼,連是否加載成功/失敗都沒能區分開來,這似乎有些不可理解。
美好的假象——如何判斷CSS加載完成
這里先不拋出結論,而是先思考一個問題:如何動態加載CSS文件?
很簡單,就下面幾行代碼:
var node = document.createElement('link'); node.rel = 'stylesheet'; node.href = 'style.css'; document.getElementsByTagName('head')[0].appendChild(node);
很好,那么接下來的問題是:怎么判斷CSS文件是否加載完成?
那還不簡單,幾行代碼就搞定的事情,前端的老朋友onload、onerror閃亮登場:
var node = document.createElement('link'); node.rel = 'stylesheet';
node.type = 'text/css'; node.href = 'style.css'; node.onload = function(){ alert('加載成功啦!'); }; node.onerror = function(){ alert('加載失敗啦!'); }; document.getElementsByTagName('head')[0].appendChild(node);
嗯,這么寫是沒錯。。。從理論上。。。看下HTML 5里關於資源加載完成的描述,概括起來就是:
- CSS文件加載成功,在link節點上觸發load事件
- CSS文件加載失敗,在link節點上觸發error事件
Once the attempts to obtain the resource and its critical subresources are complete, the user agent must, if the loads were successful, queue a task to fire a simple event named
load
at thelink
element, or, if the resource or one of its critical subresources failed to completely load for any reason (e.g. DNS error, HTTP 404 response, a connection being prematurely closed, unsupported Content-Type), queue a task to fire a simple event namederror
at thelink
element. Non-network errors in processing the resource or its subresources (e.g. CSS parse errors, PNG decoding errors) are not failures for the purposes of this paragraph.
看上去很美好的樣子。我們知道,這個世界從來都不完美,至少對於前端來說,這個世界跟完美這個詞沒半毛錢關系。JS中一直為人詬病的語法,瀏覽器糟糕的兼容性問題神馬的。將上面那段代碼放到IE(版本9及以下,10沒有測過)里面,將文件鏈接指向一個不存在的文件,比如在fiddler里將返回替換成404:
var node = document.createElement('link'); node.href = 'none_exist_file.css'; //其他屬性設置省略 node.onload = function(){ alert('加載成功啦!'); }; node.onerror = function(){ alert('加載失敗啦!'); }; document.getElementsByTagName('head')[0].appendChild(node);
於是你看到一句華麗麗的提示:
“加載成功啦!”
看到這里是不是對這個世界產生了深深的懷疑——我承認我當時把微軟開發IE瀏覽器的兄弟們全家都問候了一下。
好吧,這篇文章並不是關於IE的吐槽文,在CSS文件加載狀態的檢測這個問題上,IE的表現雖不完美,但相比之下還不算特別糟糕。
慢着!意思是——還有更糟糕的?是的,比如早期版本的firefox,連onload都不支持。
如何判斷CSS文件加載完成——五種方案
拋開一切的埋怨與不滿,按照過往的經驗,如何判斷一個文件是否加載完成?一般有以下幾種方式:
- 監聽link.load
- 監聽link.addEventListener('load', loadHandler, false);
- 監聽link.onreadystatechange
- 監聽document.styleSheets的變化
- 通過setTimeout定時檢查你預先創建好的標簽的樣式是否發生變化(該標簽賦予了在動態加載的CSS文件里才聲明的樣式)
示例代碼如下:
//方案一 link.onload = function(){ alert('CSS onload!'); }
//方案二 link.addEventListener('load', function(){ alert('addEventListener loaded !'); }, false);
//方案三 link.onreadystatechange = function(){ var readyState = this.readyState; if(readyState=='complete' || readyState=='loaded'){ alert('readystatechange loaded !'); } };
//方案四 var curCSSNum = document.styleSheets.length; var timer = setInterval(function(){ if(document.styleSheets.length>curCSSNum){ //注意:當你一次性加載很多文件的時候,需要判斷究竟是哪個文件加載完成了 alert('document.styleSheets loaded !'); clearInterval(timer); } }, 50);
var div = document.createElement('div'); div.className = 'pre_defined_class'; //加載的CSS文件里才有的樣式 var timer = setTimeout(function(){ //假設getStyle方法的作用:獲取標簽特性樣式的值 if(getStyle(div, 'display')=='none'){ alert('setTimeout check style loaded !'); return; } setTimeout(arguments.callee, 50); //繼續檢查 }, 50);
五種方案的實際測試結果
實際測試的結果如何呢?如下:
瀏覽器 | 檢查onload(onload/addEventListener) | link.onreadystatechange | 檢查document.styleSheets.length | 檢查特定標簽的樣式 |
IE | ok,但404等情況也會觸發onload | 可行,但404等情況下readyState 也為complete或loaded |
測試結果與網上說的不一致 需再加驗證 |
ok |
chrome | 1、老版本:not ok 2、新版本:ok(如24.0) |
not ok | ok(文件加載完成后才改變length) | ok |
firefox | 1、老版本:not ok(3.X) 2、新版本:ok(如16.0) |
not ok | not ok(節點插入時,length就改變) | ok |
safari | 1、老版本:not ok(?) 2、新版本:ok(如6.0) |
not ok | ok(文件加載完成后才改變length) | ok |
opera | ok | not ok | not ok(節點插入時,length就改變) | ok |
方案一、方案二本質上是一樣的;而如果可能的話,stoyan建議盡可能不用方案五,原因如下:
1)性能開銷(方案四也好不到哪去)
2)需添加額外無用樣式,需要對CSS文件有足夠的控制權(CSS文件可能並不是自己的團隊在維護)
那好,暫時將方案五排除在外(其實兼容性是最好的),從上表格可以知道,各瀏覽器分別可采用方案如下:
瀏覽器 | 可采用方案 |
IE | 方案一、方案二、方案三 |
chrome | 方案四 |
firefox | 無 |
safari | 方案四 |
opera | 方案一、二 |
firefox竟然。。。霎時間內心萬千只草泥馬在歡快地奔騰。。。對於firefox,stoyan大神也嘗試了其他方式,比如:
1、MozAfterPaint(這是神馬還沒查,總之失敗了,求指導~)
2、document.styleSheets[n].cssRules,只有當CSS文件加載下來的時候,document.styleSheets[n].cssRules才會發生變化;但是,由於ff 3.5的安全限制,如果CSS文件跨域的話,JS訪問document.styleSheets[n].cssRules會出錯
如何在老版本的firefox里判斷CSS是否加載完成
就在stoyan大神即將絕望之際,Zach Leatherman 童鞋發現了firefox下的解決方案:
- you create a
style
element, not alink
- add
@import "URL"
- poll for access to that style node's
cssRules
collection
這個方案利用了上面提到的第二點,同時解決了跨域的問題。代碼如下(代碼引用自原文):
var style = document.createElement('style'); style.textContent = '@import "' + url + '"'; var fi = setInterval(function() { try { style.sheet.cssRules; // <--- MAGIC: only populated when file is loaded CSSDone('listening to @import-ed cssRules'); clearInterval(fi); } catch (e){} }, 10); head.appendChild(style);
根據stoyan、Zach的思路, Ryan Grove 在LazyLoad里將實現,有興趣的可以看下 源代碼
Ryan Grove的代碼有些小問題,比如:
1、CSS文件的阻塞式加載,比如加載A.css、B.css,需要等A.css加載完了,才開始加載B.css
2、某些判斷語句的失誤,導致CSS文件記載成功的情況下,檢測失誤(見pollWebkit方法第一個while循環)
盡管如此,還是要感謝Ryan的勞動(撒花),LZ根據實際需要,將LazyLoad里js加載部分的代碼剔除,並上面提到的兩個比較明顯的bug fix了,修改后的源碼以及demo可參見《CSS文件動態加載》一文 :)
如何判斷CSS文件加載失敗
一直到這里,我們終於解決了如何檢測CSS文件是否加載完成的問題。 接下來又有一個嚴峻的問題擺在我們面前:如何判斷一個文件加載失敗?
不要忘了onerror童鞋!onerror的支持情況如何呢?—— 實際測試了下,情況並不樂觀,直接引用先輩的勞動結晶,原文鏈接如下:http://seajs.org/tests/research/load-js-css/test.html
css: Chrome / Safari: - WebKit >= 535.23 后支持 onload / onerror - 之前的版本無任何事件觸發 Firefox: - Firefox >= 9.0 后支持 onload / onerror - 之前的版本無任何事件觸發 Opera: - 會觸發 onload - 但 css 404 時,不會觸發 onerror IE6-8: - 下載成功和失敗時都會觸發 onload 和 onreadystatechange,無 onerror IE9: - 同 IE6-8 - onreadystatechange 會重復觸發 解決方案: - Old WebKit 和 Old Firefox 下,用 poll 方法:load-css.html - 其他瀏覽器用 onload / onerror 不足: - Opera 下如果 404,沒有任何事件觸發,有可能導致依賴該 css 的模塊一直處於等待狀態 - IE6-8 下區分不出 onerror - poll 探測難以區分出 onerror
可見,之前的方案,並不能完美解決“判斷CSS文件加載失敗”這個問題(相當令人沮喪,有主意的童鞋千萬要留言告訴我 TAT)
目前有兩種思路,其實並沒有完全解決問題:
1、超時失敗判定:設定t值,當加載時間超過t時,認定其加載失敗(簡單粗暴,目前采用方式)
2、判定加載完成后,通過上面的方案五(檢查樣式),判斷CSS文件是否加載失敗 —— 前提是沒有被認定為“超時失敗”
多方請教后,外部門的同事tom提供了一個不錯的的思路,該實現方案已經有線上項目作為實踐支撐:JSONP
CSS加載失敗判斷——不一樣的思路JSONP
假設有style.css(實際想要加載的文件)、style.js;style.js里是個回調方法CSSLoadedCallback,CSSLoadedCallback做兩件事情
1)打標記,標識style.js加載成功(即頁面拿到了style.css里的樣式字符串)
2)創建link標簽,並將CSSLoadedCallback里傳入的樣式字符串寫到link標簽里
style.js里的代碼大致如下:
//第一個參數style.css為實際想要加載的CSS的文件名
//第二個參數:style.css里的樣式
CSSLoadedCallback("style.css", ".hide{display:'none';} .title{font-size:14px;}");
於是,由原先的判斷CSS是否加載失敗,轉為判斷JS是否加載失敗;關於JS是否加載失敗,前輩的測試如下,原文鏈接請點擊這里:
關於IE6-8無法區分onerror,在這里並不是問題(可通過判斷變量是否存在實現),就是說JSONP是個靠譜的解決方案。
js:
Chrome / Firefox / Safari / Opera:
- 下載成功時觸發 onload, 下載失敗時觸發 onerror
- 下載成功包括 200, 302, 304 等,只要下載下來了就好
- 下載失敗指沒下載下來,比如 404
- Opera 老版本對 empty.js 這種空文件時不會觸發 onload,新版本已無問題
IE6-8:
- 下載成功和失敗時都會觸發 onreadystatechange, 無 onload / onerror
- 成功和失敗的含義同上
IE9:
- 有 onload / onerror,同時也有 onreadystatechange
解決方案:
- 在 Firefox、Chrome、Safari、Opera、IE9 下,用 onload + onerror
- 在 IE6-8 下,用 onreadystatechange
不足:
- IE6-8 下區分不出 onerror
小結:
1、可檢測CSS文件是否加載成功(通過多種手段判斷文件加載完成的情況下,結合檢查標簽樣式的方法)
2、可大致檢測CSS文件是否加載失敗(前提是判斷CSS已經加載完成,在chrome、opera老版本里無法准確判斷)
3、通過JSONP方式可准確判斷文件是否加載成功、失敗
寫在后面:
本文參考了多篇外站技術博客的文章,如有引用外站內容,但未聲明的情況,敬請指處!
文中示例如有錯漏,請指出;如覺得文章對您有用,可點擊“推薦” :)
參考鏈接:
http://www.phpied.com/when-is-a-stylesheet-really-loaded/
https://github.com/seajs/seajs/blob/master/src/util-request.js
https://github.com/rgrove/lazyload/commit/6caf58525532ee8046c78a1b026f066bad46d32d