CSS文件動態加載(續)—— 殘酷的真相


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里關於資源加載完成的描述,概括起來就是:

  1. CSS文件加載成功,在link節點上觸發load事件
  2. 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 the link 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 named error at the linkelement. 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文件加載完成——五種方案

拋開一切的埋怨與不滿,按照過往的經驗,如何判斷一個文件是否加載完成?一般有以下幾種方式:

  1. 監聽link.load
  2. 監聽link.addEventListener('load', loadHandler, false);
  3. 監聽link.onreadystatechange
  4. 監聽document.styleSheets的變化
  5. 通過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下的解決方案

  1. you create a style element, not a link
  2. add @import "URL"
  3. 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

http://www.zachleat.com/web/load-css-dynamically/


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM