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/

posted @ 2013-03-03 01:02 程序猿小卡 阅读(...) 评论(...) 编辑 收藏