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 里关于资源加载完成的描述,概括起来就是:
- 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 时,不会触发 onerrorIE6-8:
- 下载成功和失败时都会触发 onload 和 onreadystatechange,无 onerrorIE9:
- 同 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