借助HTML5 Web Worker 解决资源预加载 的一些顽疾.

有时.为了访问下一个页面时,可以更快.我们可能在当前页面 onload之后,利用空闲 去预加载下一个页面,以及相关资源.
为了不影响当前页面.我们必须保证.这些预加载的html css js 资源不被解析或执行.
 
先看看一些其他可选择的方式: 
 new Image().src 方式: 缺点 只能完整的预加载 mimeType 是 image/* 的资源. 其他资源只会被加载一个包的数据.就会被终止下载.
 
 link rel="stylesheet"  缺点 ie css表达式 css样式将被渲染. 非 ie 无法获知onload onerror
 link rel="stylesheet" disabled="disabled" ...不渲染css 预加载. 但FF 可能无效.除非额外配置其他属性 .
 link rel="stylesheet" media="cache etc..."   不渲染css  缺点  非ie 无onload onerror ,ie 会触发一次 draw 渲染页面
 
 link rel="alternate stylesheet"   不渲染css  缺点  非ie 无onload onerror ,ie 会触发一次 draw 渲染页面
 link rel="prefetch"  缺点 gecko 浏览器支持 ff1.5+ .  动态添加 link节点无效.  优点自动在window.onload以后才去加载资源. 
 其他如 font-face  background-image 都有无法onload onerror . 不停添加CSS Rule  无法及时回收资源的问题
document.createElement('object').data = url 方式. 缺点 :FF 加载 html 时导致解析html 并执行html中脚本资源
object.type ="application/x-shockwave-flash" 方式 . 缺点:一但资源为flash ....

 

yahoo的加载.js .css 且不解析不渲染的解决方法如下:
<script type="text/cache" src="url"></script> (text/这里是随意的.我们是要故意让浏览器不认识.)
如此浏览器可以预先加载任何资源 而不会对资源进行解析和执行.比如某个.js. 
但是 Firefox 并不支持这样做.  如果Firefox不认识type.那么就不会发起一个http请求.
yahoo的办法是用object Element 来加载 ,Firefox3.0+的浏览器允许你这么做. 但是如果我们需要加载一个text/html资源.那么object标签加载它会导致浏览器解析这个html,并执行里面的脚本. 这个是我们不能容忍的.  想象一下.我们当前页面跑的好好的. 突然因为预加载某个页面 而导致播放音乐.或执行某页的脚本.  那么对于Firefox 我们需要借助 Link Element (rel = "stylesheet")来做. 它可以保证加载 HTML不解析. 但是Link Element 在Firefox中.没有onload ,onerror . 一但我们需要在Firefox解析加载完当前资源后.我们是需要自动把 动态添加的 Link 标签从DOM上移除. script标签也是如此.
而chrome8+,Safari5+开始就不再支持 type不认识的script 资源加载了.
Opera浏览器对于script 标签无能为力的地方同Firefox 的Link Element 一样.一单不认识type .那么onload onerror都无效.
为了代码更健壮. 我想到了使用 HTML5 Web Worker 的 importScripts()方法. 
显然 Worker 为我们提供了一个相对完美的沙箱机制.  (并不是完全完美.原因最后说.)
可惜的是.Worker+script方案 仍然无法解决 Opera9.6- Firefox3.0- 的浏览器.
如果你需要解决这两个浏览器.的早期版本.那么需要借助一些反面教材的经验.  对于opera的早期版本.你可需要借助轮询来解决.
因为 虽然opera 对于不认识 type的script标签. onloda onerror都失效. 但是.实际上. scriptElement.readyState 是变化的.
我们可以自己轮询 来查看其 readyState的值.(就好像ie那样做).  来判断资源是否加载完毕. 
当然.我最希望听到的就是.我们的项目部需要考虑 opera 9.6 FF3.0 以及之前的那些老古董们..
好.最后给出两组伪代码. 当然其实只是代码依托的一些基础对象不存在,而导致无法执行: 
 
//反面教材. 
//script Element + link Element(FireFox加载HTML) + object ELement (FireFox  JS CSS ...)
//需要在恰当时间 调用dispose()方法用来 移除过多的节点.(受Firefox 和 Opera拖累)
void function (win,doc,head,ns){
     var _class = '__PRELOAD_CLEARTEMPELEMENTS__' , _tag = 'script';
     if(ns.preLoad){
          return;
     }
     var _preLoad = ns.singlePreLoad = !!doc.getBoxObjectFor || win.mozInnerScreenX != null ? (_tag = '*') && function(sURL,isHTML){
               //FF3.0+开始支持 object加载其他数据类型. 如果是html则会解析该html
               var el;
               if(isHTML){
                    el = doc.createElement('link');
                    el.type = 'text/css';
                    el.rel = 'stylesheet';
                    el.href = sURL;
               }else{
                    el = doc.createElement('object');
                    el.data = sURL;
               }
               el.className = _class;
               head.appendChild(el);
          }: function(sURL){//!FF
                    var el = doc.createElement('script');
                    el.type = 'text/C';
                    el.className = _class;
                    el.src = sURL;
                    head.appendChild(el);          
          };
     ns.preLoad = function(aURL){
          typeof aURL == 'string' && (aURL = [aURL]);
          for(var i = 0 , l = aURL.length ; i < l ; i++)_preLoad(aURL[i]);
     };
     ns.preLoad.dispose = function (){
          var list = ns.DOM.$C('__PRELOAD_CLEARTEMPELEMENTS__',_tag,head);
          for (var i = list.length ; i-- ;){
               head.removeChild(list[i]);
          }
          list = null;
     };
          
}(window,document,document.getElementsByTagName('head')[0],Visit.Util);

 

//worker + script  方案
引入worker.js
onmessage = function(e){
    var a = e.data.toString().split(',');
          for(var i = 0 , l = a.length ;i++){
               try{ 
                    //避免因加载失败导致脚本不在继续执行下去. 否则 一个imirtScripts.apply(null,a); 就解决问题了.
                    //现在,还导致失去了并行加载的优势.但是也是在是木有办法.
                    importScripts(a[i]);
               }catch(e){}
          }
    postMessage('Loaded');
};



void function (win,doc,head,ns){
     var _dom = ns.DOM,
          _workerURL = 'js/worker.js',//worker.js Path
          _emptyFn = function(){};
     
     if(ns.preLoad){
          return;
     }
     if(win.Worker){//FF3.5+ Opera10+  Safari4+ Chrome3+ (需要 worker.js支持)
          ns.preLoad = function(aURL){
               var _w = new win.Worker(_workerURL);
               _w.onerror = _emptyFn;
               _w.onmessage = function(e){
                    e.data == 'VISITLOADED' && (_w.terminate(),1) && (_w = null);//
               };
               _w.postMessage(aURL);
          };
     }else if(!doc.getBoxObjectFor || !win.opera){//IE6+ Safari3- Chrome2-
          ns.preLoad = function(aURL){
               for(var i = 0,l = aURL.length ; i < l ; i++){
                    _dom.loadJS(aURL[i],null,null,'text/C');
               }
          };
          
     }else{//FF3.0-  Opera9.6-
          ns.preLoad = _emptyFn;
     }
          
}(window,document,document.getElementsByTagName('head')[0],Visit.Util);

 

最后说说为什么说Woker 的沙箱不够好.

 

1.importScripts方法加载的资源如果是个脚本.那么实际上它是会执行的. 所以一旦资源拥有者了解这一切,那么可以给你一个执行 postMessage('loaded')语句的脚本.

显然主页面  : e.data == 'loaded' && (_w.terminate(),1) && (_w = null); 这里就会被执行.那么结果就不准确了. 虽然此漏洞可以通过 一个随机key验证方式绕过. 但考虑到 2 和 3 的问题. 

仅仅修复。此漏洞 意义并不大. 而且 importScripts 的语句还要放到一个独立的闭包中..额外损耗. 还没意义.得不偿失.

 

2. 加载的资源执行死循环的脚本.那么 除了FF 其他浏览器的 postMessage('loaded') 可能就无法被执行到.

 

3. 另外应该注意.opera 浏览器 worker 内部调用 self.close()后 带来的问题...这个问题我们无法绕过去..很纠结. 不过好在 仅仅是使我们无法确定资源是否加载完毕而已...

损失可以忽略. 只要没有安全问题.还是可以接受的.如果可能的话 加入一个 超时机制 来弥补这种问题所带来的风险应该是ok的...毕竟我们最终的目的是为了预加载资源.

 

 

4. 一但importScripts 引入的脚本 执行类似的脚本 while(!window) postMessage('hellow'); 那么就是个悲剧 主页面进程相当的卡. 

类似的还有 importScripts()递归调用.

 

防堵这两个问题也是有办法的. 大家可以自己想想看(提示黑名单制度+超时制度.)

参考伪代码 :

 _w.onmessage = function(e){
    win.clearTimeout(_timer);
    _w.terminate();
    _w = _w.onmessage = null;
    e.data != key && _preLoad.addBlackList && _preLoad.addBlackList((aURL.shift(),aURL),e.data);
};

 

 

posted @ 2010-12-09 23:35 Franky 阅读(...) 评论(...) 编辑 收藏