在之前的文章《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方式可准确判断文件是否加载成功、失败
写在后面:
本文参考了多篇外站技术博客的文章,如有引用外站内容,但未声明的情况,敬请指处!
文中示例如有错漏,请指出;如觉得文章对您有用,可点击“推荐” :)