皮肤 颜色
板式
宽屏 窄屏
您的位置:合度 > 动态 > 技术选摘 > >

javascript异步动态加载js和css文件的实现方法、原理

发表日期:2015-06-29    文章编辑:本站编辑   来源:未知    浏览次数:
主要几个框架或者插件是如何实现异步加载事件响应的。
一.LABjs
这个项目位于github上面,其本意就是Loading And Blocking JavaScript,就是一个异步脚本加载器,相对于原始的粗暴script标签方式而言,提供了弹性的性能优化(做到js文件在浏览器中尽可能的并行加载,并且能够提供顺序执行的保证),还能在高级浏览器中实现先加载然后执行的功能。作为一个异步js加载器还是非常优秀的。其在异步加载的成功事件响应方面的实现如下:
// creates a script load listener
function create_script_load_listener(elem,registry_item,flag,onload) {
        elem.onload = elem.onreadystatechange = function() {
                if ((elem.readyState && elem.readyState != "complete" && elem.readyState != "loaded")
               || registry_item[flag]) return;
                        elem.onload = elem.onreadystatechange = null;
                        onload();
        };
}
从上面可见,基本上就是利用onload事件和onreadystatechange事件来完成的,最后一个registry_item[flag]就是对同源的文件可以通过AJAX来实现的。但是没有涉及到css文件的加载,也没提到js文件不存在的时候如何来检测。
二.RequireJS
RequireJS主要定位于a file and module loader for javascript,就是作为一种模块化开发过程中的模块加载器,由于模块是以文件形式存在,所以也就相当于一个文件加载器,但是实现了模块管理的功能。其主要也仍然是在处理js文件的加载,没有考虑css文件的加载。那我们看一下他主要的事件监听实现吧:
 //Set up load listener. Test attachEvent first because IE9 has
 //a subtle issue in its addEventListener and script onload firings
 //that do not match the behavior of all other browsers with
 //addEventListener support, which fire the onload event for a
 //script right after the script execution. See:
 //https://connect.microsoft.com/IE/feedback/details/648057/script-onload-event-is-not-fired-immediately-after-script-execution
 //UNFORTUNATELY Opera implements attachEvent but does not follow the script
 //script execution mode.
if (node.attachEvent &&
    //Check if node.attachEvent is artificially added by custom script or
    //natively supported by browser
    //read https://github.com/jrburke/requirejs/issues/187
    //if we can NOT find [native code] then it must NOT natively supported.
    //in IE8, node.attachEvent does not have toString()
     //Note the test for "[native code" with no closing brace, see:
     //https://github.com/jrburke/requirejs/issues/273
     !(node.attachEvent.toString && node.attachEvent.toString().indexOf('[native code') < 0)
     && !isOpera) {
      //Probably IE. IE (at least 6-8) do not fire
      //script onload right after executing the script, so
      //we cannot tie the anonymous define call to a name.
      //However, IE reports the script as being in 'interactive'
      //readyState at the time of the define call.
          useInteractive = true;
          node.attachEvent('onreadystatechange', context.onScriptLoad);
      //It would be great to add an error handler here to catch
      //404s in IE9+. However, onreadystatechange will fire before
      //the error handler, so that does not help. If addEventListener
      //is used, then IE will fire error before load, but we cannot
      //use that pathway given the connect.microsoft.com issue
      //mentioned above about not doing the 'script execute,
      //then fire the script load event listener before execute
       //next script' that other browsers do.
       //Best hope: IE10 fixes the issues,
      //and then destroys all installs of IE 6-9.
      //node.attachEvent('onerror', context.onScriptError);
} else {
      node.addEventListener('load', context.onScriptLoad, false);
      node.addEventListener('error', context.onScriptError, false);
}
从上面的注释可以明白对于IE6-8都是采用的onreadystatechange来监听加载成功事件,而对于404的事件是没有办法做出区分的,所以也就没能完成此功能,只是对于高级浏览器比如chrome之类的采用了onload监听成功事件,onerror监听失败事件。由于主要针对js文件的加载,所以也没有针对css文件的加载做出明确的实现。
三.YUI3
YUI3作为一款非常优秀的js框架,其框架的代码都经过了yahoo的使用场景考验,应该是非常完善和趋于完美的实现了,那我们同样来窥探一下他实现异步获取文件的模块代码吧:
// Inject the node.
if (isScript && ua.ie && (ua.ie < 9 || (document.documentMode && document.documentMode < 9))) {
        // Script on IE < 9, and IE 9+ when in IE 8 or older modes, including quirks mode.
        node.onreadystatechange = function () {
                if (/loaded|complete/.test(node.readyState)) {
                        node.onreadystatechange = null;
                        onLoad();
                }
        };
} else if (!isScript && !env.cssLoad) {
        // CSS on Firefox <9 or WebKit.
        this._poll(req);
} else {
        // Script or CSS on everything else. Using DOM 0 events because that
        // evens the playing field with older IEs.

        if (ua.ie >= 10) {

                // We currently need to introduce a timeout for IE10, since it
                // calls onerror/onload synchronously for 304s - messing up existing
                // program flow.

                // Remove this block if the following bug gets fixed by GA
                // https://connect.microsoft.com/IE/feedback/details/763871/dynamically-loaded-scripts-with-304s-responses-interrupt-the-currently-executing-js-thread-onload
                node.onerror = function() { setTimeout(onError, 0); };
                node.onload = function() { setTimeout(onLoad, 0); };
        } else {
                node.onerror = onError;
                node.onload = onLoad;
        }

        // If this browser doesn't fire an event when CSS fails to load,
        // fail after a timeout to avoid blocking the transaction queue.
        if (!env.cssFail && !isScript) {
                cssTimeout = setTimeout(onError, req.timeout || 3000);
        }
}

this.nodes.push(node);
insertBefore.parentNode.insertBefore(node, insertBefore);
从YUI3的实现来看完成了js和css文件的加载,基本上符合我们的要求。对于js文件,在IE6-8用onreadystatechange事件来监听,但没有办法监听到error事件所以放弃了;其他浏览器则通过onload和onerror来实现,基本上和上面LABjs和Requirejs类似。对于css文件,在IE6-8上面同样采用的是onreadystatechange来实现,并且同样没办法来实现error事件的监听;其他浏览器如果支持onload事件则采用此方法,如果不支持(比如firefox<7和一些低版本的webkit内核)则只能通过不断的轮询css节点来实现了。从注释当中可以看出,在IE10下面服务器缓存设置返回304的时候有一个bug,需要通过异步的方式来触发监听方法,具体可以再测试一下。YUI3中对于css加载的轮询方式如下:
if (isWebKit) {
        // Look for a stylesheet matching the pending URL.
        sheets = req.doc.styleSheets;
        j = sheets.length;
        nodeHref = req.node.href;

        while (--j >= 0) {
                if (sheets[j].href === nodeHref) {
                        pendingCSS.splice(i, 1);
                        i -= 1;
                        self._progress(null, req);
                        break;
                }
        }
} else {
        // Many thanks to Zach Leatherman for calling my attention to
        // the @import-based cross-domain technique used here, and to
        // Oleg Slobodskoi for an earlier same-domain implementation.
        //
        // See Zach's blog for more details:
        // http://www.zachleat.com/web/2010/07/29/load-css-dynamically/
        try {
                // We don't really need to store this value since we never
                // use it again, but if we don't store it, Closure Compiler
                // assumes the code is useless and removes it.
                hasRules = !!req.node.sheet.cssRules;

                // If we get here, the stylesheet has loaded.
                pendingCSS.splice(i, 1);
                i -= 1;
                self._progress(null, req);
        } catch (ex) {
                // An exception means the stylesheet is still loading.
        }
}
从上面轮询的方式来看,对于webkit内核的则通过检查style的sheet节点是否附加上了来测试,其他比如firefox和opera则通过检查sheet.cssRules是否生效来完成。但是始终都是没有办法解决404的问题。所以也就只能这样了。。。
四.jQuery
大名鼎鼎的jQuery在实现ajax封装了所有的异步加载功能的时候,为script加载专门分了文件的,具体可以看到如下实现:
script = jQuery("<script>").prop({
        async: true,
        charset: s.scriptCharset,
        src: s.url
}).on(
        "load error",
        callback = function( evt ) {
                script.remove();
                callback = null;
                if ( evt ) {
                        complete( evt.type === "error" ? 404 : 200, evt.type );
                }
        }
);
document.head.appendChild( script[ 0 ] );
从上面代码看基本上和上面类似,看来对于js而言没有什么太多的方法,所以基本上按照以上几种实现即可。jquery并没有对css文件加载做专门的处理,所以还无从参考。
五.Seajs
Seajs在阿里系还是有很大的使用范围的,并且目前推广的还不错,所以陆续有很多公司开始采用了。其也主要是推行模块化开发的方式,因此也会涉及到异步记载模块文件的方式,所以也涉及到了文件的异步加载。其request模块实现如下:
function addOnload(node, callback, isCSS) {
  var missingOnload = isCSS && (isOldWebKit || !("onload" in node))

  // for Old WebKit and Old Firefox
  if (missingOnload) {
    setTimeout(function() {
      pollCss(node, callback)
    }, 1) // Begin after node insertion
    return
  }

  node.onload = node.onerror = node.onreadystatechange = function() {
    if (READY_STATE_RE.test(node.readyState)) {

      // Ensure only run once and handle memory leak in IE
      node.onload = node.onerror = node.onreadystatechange = null

      // Remove the script to reduce memory leak
      if (!isCSS && !configData.debug) {
        head.removeChild(node)
      }

      // Dereference the node
      node = undefined

      callback()
    }
  }
}

function pollCss(node, callback) {
  var sheet = node.sheet
  var isLoaded

  // for WebKit < 536
  if (isOldWebKit) {
    if (sheet) {
      isLoaded = true
    }
  }
  // for Firefox < 9.0
  else if (sheet) {
    try {
      if (sheet.cssRules) {
        isLoaded = true
      }
    } catch (ex) {
      // The value of `ex.name` is changed from "NS_ERROR_DOM_SECURITY_ERR"
      // to "SecurityError" since Firefox 13.0. But Firefox is less than 9.0
      // in here, So it is ok to just rely on "NS_ERROR_DOM_SECURITY_ERR"
      if (ex.name === "NS_ERROR_DOM_SECURITY_ERR") {
        isLoaded = true
      }
    }
  }

  setTimeout(function() {
    if (isLoaded) {
      // Place callback here to give time for style rendering
      callback()
    }
    else {
      pollCss(node, callback)
    }
  }, 20)
}
从seajs的实现来看,主要完成了js和css的异步加载,其主要实现还是和YUI3的get模块实现方式基本一致。并且实现方式还是简单粗暴的,具体细节还不如YUI3的实现精细,但是对于大多数场景还是够用了的。
另外从labjs和seajs上面可以注意一个细节,为了防止内存溢出,还是在js文件加载完毕之后会删除其对应的script节点。因为对于js而言已经执行,其内存中已经保存了相关的环境变量,css文件则不一样删除则会将对应的style样式一并清除。
—————分割线———
上面讨论了几种实现方式,看来js都比较好处理,大家也都实现的很简单,主要分IE6-8采用onreadystatechange事件,判断readystate状态来完成;其他浏览器则通过监听onload事件来完成,但都无法完全通过onerror事件来监听404状态。对于css文件则实现比较难一点,如果浏览器本身支持onload方法便好说,不支持则通过轮询sheet的cssRules是否生效或者对应的节点是否生成。难道就没有好一点的办法么?
通过google查询,可以通过new Image()的方式来加载css文件的地址,然后由于mime类型错误,所以会触发img的onerror事件从而来达到模拟css文件加载成功的事件,如果不支持此方法的再没办法的采用轮询的方式来完成。那到底什么时候采用此方式呢?为此,我做了一个测试页面,来测试各种浏览器的对于js和css文件的异步加载事件的支持情况,具体代码如下:
<!DOCTYPE html>
<html>
<head>
<title>test</title>
</head>
<body>
<div id="testresult" style="margin:50px;">-------------------------start----------------------<br/></div>

        <script>
/**
 *@fileoverview the loader plugin for asynchronous loading resources
 *@author ginano
 *@website
 *@date 20130228
 */
(function(){
   var headEl=document.getElementsByTagName("head")[0],
       dom=document.getElementById('testresult');

   var Loader={
       /**
       *加载js文件
       * @param {Object} url
       */
      importJS:function(url,str){
            var head ,
                script;

            head = headEl;
            script = document.createElement("script");
            script.type = "text/javascript";

           script.onreadystatechange=function(){
                 dom.innerHTML+='suppport:js-readystatechange-'+this.readyState+' event-----------------------'+str+'<br/>';
            };

           script.onload=function(){
                dom.innerHTML+='suppport:js-load event-----------------------'+str+'<br/>';
            };

            script.onerror=function(){
                     dom.innerHTML+='suppport:js-error event-----------------------'+str+'<br/>';
            };

            script.src = url;
            head.appendChild(script);
      },
      /**
       *加载css文件
       * @param {Object} url
       */
      importCSS:function(url,str){
            var head,
                link,
                img,
                ua;

            head = headEl;
            link = document.createElement("link");
            link.rel="stylesheet";
            link.type = "text/css";
            link.href=url;

                   link.onerror=function(){
               dom.innerHTML+='<div style="color:green">suppport:css-error event-----------------------'+str+'<br/></div>';
           };
           link.onload=function(){
                dom.innerHTML+='<div style="color:green">suppport:css-load event-----------------------'+str+'<br/></div>';
            };
                    link.onreadystatechange=function(){
                 dom.innerHTML+='<div style="color:green">suppport:css-readystatechange-'+this.readyState+' event-----------------------'+str+'<br/></div>';
           };
            head.appendChild(link);
                    img=document.createElement('img');
            img.onerror=function(){
                dom.innerHTML+='<div style="color:green">suppport:css-img-error event-----------------------'+str+'<br/></div>';
            };
            img.src=url;

      }
   };
   dom.innerHTML+='browser Info:'+window.navigator.userAgent+'<br/>';
   //测试正常文件
   Loader.importJS(
       'http://yui.yahooapis.com/2.9.0/build/yahoo/yahoo-min.js',
        'rightJS'
   );
   //测试404js文件
    Loader.importJS(
       'http://www.ginano.net/1.js',
        'wrongJS'
   );
  //测试css文件
   Loader.importCSS(
       'http://yui.yahooapis.com/2.9.0/build/fonts/fonts.css',
        'rightCSS'
   );
//测试404css文件
   Loader.importCSS(
       'http://www.ginano.net/1.css',
        'wrongCSS'
   );

})();
</script>
</body>
</html>
由于需要各种浏览器测试结果,所以在http://browsershots.org/上面打开测试页面http://www.ginano.net/test-browser-load-js-css-event.html ,跑了半天每个浏览器都有一张如下所示的截屏。

 
该平台支持173种浏览器,通过将结果整理得到如下的数据:



事件支持情况 
浏览器
onreadystatechange
onload
onerror
Img.onerror



5~8
JS
200
Loading,loaded,第二次为complete





404
Loading,loaded,第二次为complete





CSS
200
Loading,complete
ok

ok


404
Loading,complete
ok

ok


9.0-10
JS
200
Loading,loaded
ok




404
Loading,loaded

ok



CSS
200
Loading,complete
ok

ok


404
Loading,complete
ok

ok


Chrome
1~9 
(webkit:530-534)
JS
200

ok




404


ok



CSS
200






404






10~19 
(webkit:534.15-535.21)
JS
200

ok




404


ok



CSS
200



ok


404



ok


20~26 
(webkit:536.11-537.11)
JS
200

Ok




404


ok



CSS
200

ok

ok


404


ok
ok


Firefox
1~8
JS
200

ok




404


ok



CSS
200



ok


404



ok


9~20
JS
200

Ok




404


Ok



CSS
200

Ok

ok


404


ok
ok


Opera
<=11.61
JS
200
Loaded
Ok




404






CSS
200
Undefined?
Ok

ok


404



ok


404



ok


备注: 
在9.64版本,js两种情况都还会触发onreadystatechange-interactive;
在11.61版本中,js-404会触发onerror事件。
 


11.64~12.50
JS
200

OK




404


OK



CSS
200

OK

OK


404



OK


Safari
3.2.3 
(webkit:525.28)
JS
200

ok




404

ok




CSS
200






404






5.0 
(webkit:533-534)
JS
200

ok




404


ok



CSS
200



ok


404



ok


6.0 
(webkit:536)
JS
200

ok




404


ok



CSS
200

ok

ok


404


ok
OK



从上面的结果总结规律如下:
1.js文件
1.1 IE8及以下版本,通过onreadystatechange事件监听,判断readystate状态是否为load或者complete从而触发成功事件。具体可查阅上表
1.2 其他浏览器直接通过onload事件即可完成加载成功事件的监听。
1.3 由于始终无法保证onerror事件的支持,只是对能够支持的加上即可
2.css文件
2.1 所有浏览器对onerror的支持都不完美,所以只是尽量处理
2.2 IE浏览器/firefox9.0级以上/opera/chrome浏览器20及以上/safari浏览器6.0以上都支持css的onload事件,因此通过监听onload即可。
2.3 chrome浏览器9.10到19.0/safari浏览器5.0到5.9/firefox浏览器8.9一下则通过img的onerror事件即可模拟出css文件的加载成功事件
2.4 其他浏览器,比如chrome浏览器9.0及以下则只能通过轮询css样式节点是否附加成功来判断了
备注:YUI3的注释当中提到了IE10的bug不知道修复与否,但是目前测试结果是ok的所以没有做单独处理。
鉴于上面的基本规律,我再问的文件加载器模块当中的实现代码如下:
/**
 *@fileoverview the loader plugin for asynchronous loading resources
 * there isn't method to resolve the problem of 404
 *@author ginano
 *@website
 *@date 20130228
 */
define('modules/loader',[
        'modules/class',
        'modules/ua',
        'modules/util'
    ],function(Class,UA,Util){
   var LoadedList={},
       headEl=document.getElementsByTagName("head")[0],
       isFunction=function(f){
            return f instanceof Function;
       };

   var Loader=new Class('modules/loader',{
       /**
       *加载js文件
       * @param {Object} url
       */
      static__importJS:function(url,callback){
            var head ,
                script,
                //成功之后做的事情
                wellDone=function(){
                    LoadedList[url]=true;
                    clear();
                    Util.log('load js file success:'+url);
                    callback();
                },
                clear=function(){
                   script.onload=script.onreadystatechange=script.onerror=null;
                   head.removeChild(script);
                   head=script=null;
                };

            if(LoadedList[url]){
                isFunction(callback)&&callback();
                return;
            }
            head = headEl;
            script = document.createElement("script");
            script.type = "text/javascript";

           script.onerror=function(){
               clear();
               Util.log('load js file error:'+url);
           };

            if(isFunction(callback)){
                //如果是IE6-IE8
                if(UA.browser=='ie' && UA.version<9){
                    script.onreadystatechange=function(){
                        //当第一次访问的时候是loaded,第二次缓存访问是complete
                        if(/loaded|complete/.test(script.readyState)){
                            wellDone();
                        }
                    }
                }else{
                    script.onload=function(){
                       wellDone();
                    }
                }
                //始终保证callback必须执行,所以需要定时器去完成,测试结果表明早期的大量的浏览器还不支持
                //timer=setTimeout(function(){
                // wellDone();
                //},10000);
            }

            script.src = url;
            head.appendChild(script);
      },
      /**
       *加载css文件
       * @param {Object} url
       */
      static__importCSS:function(url,callback){
            var head,
                link,
                img,
                firefox,
                opera,
                chrome,
                poll,
                //成功之后做的事情
                wellDone=function(){
                    LoadedList[url]=true;
                    clear();
                    Util.log('load css file success:'+url);
                    callback();
                },
                clear=function(){
                    timer=null;
                    link.onload=link.onerror=null;
                    head=null;
                };
            if(LoadedList[url]){
                isFunction(callback)&&callback();
                return;
            }
            head = headEl;
            link = document.createElement("link");
            link.rel="stylesheet";
            link.type = "text/css";
            link.href=url;

            link.onerror=function(){
               clear();
               Util.log('load css file error:'+url);
            };
            if(isFunction(callback)){
                //如果是IE系列,直接load事件
                if(UA.browser=='ie'
                    || (UA.browser=='firefox' && UA.version>8.9)
                    || UA.browser=='opera'
                    || (UA.browser=='chrome' && UA.version>19)
                    || (UA.browser=='safari' && UA.version>5.9)

                ){

                   //IE和opera浏览器用img实现
                    link.onload=function(){
                        wellDone();
                    };
                    head.appendChild(link);

                }else if(
                   (UA.browser=='chrome' && UA.version>9)
                   || (UA.browser=='safari' && UA.version>4.9)
                   || UA.browser=='firefox'
                ){

                    head.appendChild(link);
                    //如果是非IE系列
                    img=document.createElement('img');
                    img.onerror=function(){
                        img.onerror=null;
                        img=null;
                        wellDone();
                    };
                    img.src=url;

                }else{//轮询实现
                    head.appendChild(link);
                    poll=function(){
                        if(link.sheet && link.sheet.cssRules){
                            wellDone();
                        }else{
                            setTimeout(poll,300);
                        }
                    };
                    poll();
                }
            }else{
                head.appendChild(link);
            }
      },
      /**
       *异步加载所需的文件
       * @param {Array} urls
       * @param {Function} callback
       * @param {Boolean} [option=true] isOrdered 是否需要按序加载,默认是需要按序加载
       */
      static__asyncLoad:function(urls,callback,isOrdered){
          var _self=this,
              isOrder=!(isOrdered===false),
              isAllDone=false,
              now,
              i,
              urls= ('string'===typeof urls)?[urls]:urls;
              len=(urls instanceof Array) && urls.length,
              /**
               *根据后缀判断是js还是css文件
               * @param {Object} url
               * @param {Object} done
               */
              load=function(url, done){
                  if(/\.js(?:\?\S+|#\S+)?$/.test(url)){
                      _self.importJS(url,done);
                  }else{
                      _self.importCSS(url,done);
                  }
              },
              orderLoad=function(){
                  now=urls.shift();
                  if(now){
                     load(now,orderLoad);
                  }else{
                     callback && callback();
                  }
              };
          if(!len || len<1){
              return;
          }
          //如果有顺序
          if(isOrder){
              orderLoad();
          }else{
             //如果没有顺序加载
             for(i=0,now=0;i<len;i++){
                 load(urls[i],function(){
                     now+=1;
                     if(now==len){
                        callback && callback();
                     }
                 });
             }
          }
      }
   });
   return Loader;
});
经过测试以上实现方式还是具有非常好的兼容性的,如果大家测试有什么bug可以尽管在评论中予以指正。

相关文章推荐

  
Socket 和 WebSocket 有哪些区别和联系? WebSocket 和 HTML5 是什么关系? 必须在浏览器中才能使用...
  
其实对于我们一般理解的计算机内存,它算是CPU与计算机打交道最频繁的区域,所有数据都是...
  
简介 响应式Web设计 是一种创建Web应用程序的新方法。一旦采用 响应式Web设计 创建出应用程序...
  
用div做成表格的形式,把标签中间的空格都去掉就可以了...
  
看下面的代码,其中连接池采用的c3p0,配置文件省略 import java.sql.Connection; import org.springframe...
  
主要几个框架或者插件是如何实现异步加载事件响应的。 一.LABjs 这个项目位于github上面,其本...
  
html5shiv让IE6-IE8支持HTML5标签 越来越多的站点开始使用 HTML5 标签。但是目前的情况是还有很多人...
  
缓存 是实际工作中非常常用的一种提高性能的方法, 我们会在许多场景下来使用缓存。 本文通...
  
为了防止恶意用户发布恶意内容,我们的安全分析浏览器都在虚拟机上运行。这使我们能够确...