CSS檔案動態載入(續)—— 殘酷的真相

來源:互聯網
上載者:User

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/

相關文章

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.