zoukankan      html  css  js  c++  java
  • javascript【AMD模块加载器】浅析V3(添加CSS加载功能,重构内部流程)

    由于今天正美大大的回复,然后前篇文章的评论里就出现了好多人站出来指责我抄袭,吓的我小心肝都扑通扑通的跳。

    虽然这次的模块加载器是参照了正美大大的来写,但是至少我是一行一行代码自己写出来的。然后一个浏览器一个浏览器测试的,就连流程图都重画了好几次。

    虽然大体上跟正美的差不多,但是细节上还是有很多不同的。看到哪些回复我也不想说啥。 抄没抄,会不会。明眼人一眼就能看出来,犯不着解释太多。

    废话不多说,下面介绍这一版本的改进。额外增加了一个配置项控制并发的数量。因为浏览器的有效并发数是有限的。所以如果你一次性加载10个模块,就有可能阻塞掉其它的资源加载。

    现在内部默认最大并发是4个。将原来的moduleCache变量删除,将所有加在信息都整合到modules中,并标记初始加载函数,在所有模块加载结束后删除初始加在函数。  

    所有css加载,不计入模块加载中。而且加载css也不会在factory的参数中出现。也就是说如果你这样写也没关系。

    require(['hello','test.css','test'], function(hello,test){
       console.log(hello,test); 
    });

    不过现在还有一个问题就是加载css检测加载完毕的问题。 由于浏览器对link标签的onload事件支持各不一样,加之就算为之做了兼容也是锦衣夜行。 因为根本不需要知道css的加载情况。

    主要的改动就这些,还有一些细节性的改动。去掉了deps属性,检测循环依赖的方法改为使用args而不是之前的deps。将之前的loadJS方法改变为loadSource。 将require方法和load拆分开来。

    使用文档碎片来将节点批量插入到页面中,尽量减少修改dom树,减少浏览器重排。

    这一版中依然使用了正美大大博客中的哪个获得当前被解析的script的url的方法,不知道园子里的各位朋友有没有更好的办法。在最初的时候我是用模块名称来做唯一的,这样就不用获取正在解析的script的url。 但是重名模块就很难解决了。如果大家有更好的解决办法希望能告知一下。大家一起进步。 下面是最新的源码和使用的方法。

    View Code
      1 ;(function(win, undefined){
      2     win = win || window;
      3     var doc = win.document || document,
      4         head = doc.head || doc.getElementsByTagName("head")[0],
      5         fragment = document.createDocumentFragment(),
      6         hasOwn = Object.prototype.hasOwnProperty,
      7         slice = Array.prototype.slice,
      8         configure = {total : 4},
      9         basePath = (function(nodes){
     10             var node, url;
     11             if(!configure.baseUrl){
     12                 node = nodes[nodes.length - 1];
     13                 url = (node.hasAttribute ? node.src : node.getAttribute("src", 4)).replace(/[?#].*/, "");
     14             }else{
     15                 url = configure.baseUrl;
     16             }
     17             return url.slice(0, url.lastIndexOf('/') + 1);
     18         }(doc.getElementsByTagName('script'))),
     19         _lynx = win.lynx;
     20 
     21     /**
     22      * 框架入口
     23      */
     24     function lynx(exp, context){
     25         return new lynx.prototype.init(exp, context);
     26     }
     27     
     28     lynx.prototype = {
     29         constructor : lynx,
     30 
     31         /**
     32          * 初始化
     33          * @param {All} expr 
     34          * @param {All} context
     35          * @return {Object}
     36          * 
     37          */
     38         init : function(expr, context){
     39             if(typeof expr === 'function'){
     40                 require('ready', expr);
     41             }
     42             //TODO
     43         }
     44     }
     45     lynx.fn = lynx.prototype.init.prototype = lynx.prototype;
     46 
     47     /**
     48      * 继承方法
     49      */
     50     lynx.fn.extend = lynx.extend = function(){
     51         var args = slice.call(arguments), deep = typeof args[args.length - 1] == 'bollean' ? args.pop() : false;
     52         
     53         if(args.length == 1){
     54             args[1] = args[0];
     55             args[0] = this;
     56             args.length = 2;
     57         }
     58 
     59         var target = args[0], i = 1, len = args.length, source, prop;
     60         
     61         for(; i < len; i++){
     62             source = args[i];
     63             for(prop in source){
     64                 if(hasOwn.call(source, prop)){
     65                     if(typeof source[prop] == 'object'){
     66                         target[prop] = {};
     67                         this.extend(target[prop],source[prop]);
     68                     }else{
     69                         if(target[prop] === undefined){
     70                             target[prop] = source[prop];
     71                         }else{
     72                             deep && (target[prop] = source[prop]);
     73                         }
     74                     }
     75                 }
     76             }
     77         } 
     78     };
     79 
     80     /**
     81      * mix
     82      * @param  {Object} target   目标对象
     83      * @param  {Object} source   源对象
     84      * @return {Object}          目标对象
     85      */
     86     lynx.mix = function(target, source){
     87         if( !target || !source ) return;
     88         var args = slice.call(arguments), i = 1, override = typeof args[args.length - 1] === "boolean" ? args.pop() : true, prop;
     89         while ((source = args[i++])) {
     90             for (prop in source) {
     91                 if (hasOwn.call(source, prop) && (override || !(prop in target))) {
     92                     target[prop] = source[prop];
     93                 }
     94             }
     95         }
     96         return target;
     97     };
     98 
     99     lynx.mix(lynx, {
    100         modules : {               //保存加载模块
    101             ready : {
    102                 state : 1,
    103                 type : 1,
    104                 args : [],
    105                 exports : lynx
    106             }
    107         },
    108         urls : [],
    109         loading : 0,
    110         stacks : [],             //getCurrentScript取不到值的时候用来存储当前script onload的回调函数数组
    111 
    112         /**
    113          * get uuid
    114          * @param {String} prefix
    115          * @return {String} uuid
    116          */
    117         guid : function(prefix){
    118             prefix = prefix || '';
    119             return prefix + (+new Date()) + String(Math.random()).slice(-8);
    120         },
    121 
    122         /**
    123          * noop 空白函数
    124          */
    125         noop : function(){
    126 
    127         },
    128 
    129         /**
    130          * error 
    131          * @param {String} str
    132          */
    133         error : function(str){
    134             throw new Error(str);
    135         },
    136 
    137         /**
    138          * @return {Object} lynx
    139          */
    140         noConflict : function(deep) {
    141             if ( window.lynx === lynx ) {
    142                 window.lynx = _lynx;
    143             }
    144             return lynx;
    145         }
    146     });
    147 
    148 
    149     //================================ 模块加载 ================================
    150     /**
    151      * 模块加载方法
    152      * @param {String|Array}   ids      需要加载的模块
    153      * @param {Function} callback 加载完成之后的回调
    154      */
    155     win.require = lynx.require = function(ids, callback){
    156         ids = typeof ids === 'string' ? [ids] : ids;
    157         var modules = lynx.modules, urls = lynx.urls, uuid = lynx.guid('cb_'), data;
    158         data = parseModules(ids, basePath);
    159         modules[uuid] = {
    160             name : 'initialize',
    161             type : 2,
    162             state : 1,
    163             args : data.args,
    164             factory : callback
    165         };
    166         urls = urls.concat(data.urls);
    167         lynx.load(urls);
    168     };
    169 
    170     /**
    171      * @param  {String} id           模块名
    172      * @param  {String|Array} [dependencies] 依赖列表
    173      * @param  {Function} factory      工厂方法
    174      */
    175     win.define = function(id, dependencies, factory){
    176         if(typeof dependencies === 'function'){
    177             factory = dependencies;
    178             if(typeof id === 'array'){
    179                 dependencies = id;
    180             }else if(typeof id === 'string'){
    181                 dependencies = [];
    182             }
    183         }else if (typeof id == 'function'){
    184             factory = id;
    185             dependencies = [];
    186         }
    187         id = lynx.getCurrentScript();
    188 
    189         dependencies = typeof dependencies === 'string' ? [dependencies] : dependencies;
    190 
    191         var handle = function(id, dependencies, factory){
    192             var modules = lynx.modules, urls = lynx.urls;
    193             modules[id].factory = factory;
    194             modules[id].state = 2;
    195             if(!dependencies.length){
    196                 fireFactory(id);
    197             }else{
    198                 var data = parseModules(dependencies, id, true);
    199                 urls = urls.concat(data.urls);
    200                 lynx.load(urls);
    201             }
    202         }
    203         if(!id){
    204             lynx.stacks.push(function(dependencies, factory){
    205                 return function(id){
    206                     handle(id, dependencies, factory);
    207                     id = null; dependencies = null; factory = null;
    208                 }
    209             }(dependencies, factory));
    210         }else{
    211             handle(id, dependencies, factory);
    212         }
    213     }
    214 
    215     require.amd = define.amd = lynx.modules;
    216 
    217     /**
    218      * 解析加载模块信息
    219      * @param {Array} list
    220      * @param {String} path 
    221      * @param {boolean} flag 
    222      * @return {Object}
    223      */
    224     function parseModules(list, basePath, flag){
    225         var modules = lynx.modules, urls = [], args = [], uniqurl = {}, id, result;
    226         while(id = list.shift()){
    227             if(modules[id]){ 
    228                 args.push(id);
    229                 continue;
    230             }
    231             result = parseModule(id, basePath);
    232             modules[basePath] && modules[basePath].args.push(result[1]);
    233             flag && checkCircularDeps(result[1], basePath) && lynx.error('模块[url:'+ basePath +']与模块[url:'+ result[1] +']循环依赖');
    234             modules[result[1]] = {
    235                 type : result[2] === 'js' ? 1 : 2,
    236                 name : result[0],
    237                 state : 0,
    238                 exports : {},
    239                 args : [],
    240                 factory : lynx.noop
    241             };
    242             (result[2] === 'js') && args.push(result[1]);
    243             if(!uniqurl[result[1]]){
    244                 uniqurl[result[1]] = true;
    245                 urls.push(result[1]);
    246             }
    247         }
    248 
    249         return {
    250             args : args,
    251             urls : urls
    252         }
    253     }
    254 
    255     /**
    256      * parse module
    257      * @param {String} id 模块名
    258      * @param {String} basePath 基础路径
    259      * @return {Array} 
    260      */
    261     function parseModule(id, basePath){
    262         var url, result, ret, dir, paths, i, len, type, modname, protocol = /^(\w+\d?:\/\/[\w\.-]+)(\/(.*))?/;
    263         if(result = protocol.exec(id)){
    264             url = id;
    265             paths = result[3] ? result[3].split('/') : [];
    266         }else{
    267             result = protocol.exec(basePath);
    268             url = result[1];
    269             paths = result[3] ? result[3].split('/') : [];
    270             modules = id.split('/');
    271             paths.pop();
    272             for(i = 0, len = modules.length; i < len; i++){
    273                 dir = modules[i];
    274                 if(dir == '..'){
    275                     paths.pop();
    276                 }else if(dir !== '.'){
    277                     paths.push(dir);
    278                 }
    279             }
    280             url = url + '/' + paths.join('/');
    281         }
    282         modname = paths[paths.length - 1];
    283         type = modname.slice(modname.lastIndexOf('.') + 1);
    284         if(type !== 'js' && type !== 'css'){
    285             type = 'js';
    286             url += '.js';
    287         }
    288         return [modname, url, type];
    289     }
    290 
    291     /**
    292      * fire factory
    293      * @param  {String} uuid
    294      */
    295     function fireFactory(uuid){
    296         var modules = lynx.modules,
    297         data = modules[uuid], deps = data.args,
    298         i = 0, len = deps.length, args = [];
    299         for(; i < len; i++){
    300             args.push(modules[deps[i]].exports)
    301         }
    302         data.exports = data.factory.apply(null, args);
    303         data.state = 3;
    304         delete data.factory;
    305         delete data.args;
    306         if(data.type == 2 && data.name == 'initialize'){
    307             delete modules[uuid];
    308         }
    309         checkLoadReady();
    310     }
    311 
    312     /**
    313      * 检测是否全部加载完毕
    314      */
    315     function checkLoadReady(){
    316         var modules = lynx.modules, flag = true, data, prop, deps, mod, i , len;
    317         for (prop in modules) {
    318             data = modules[prop];
    319             if(data.type == 1 && data.state != 2){    //如果还没执行到模块的define方法
    320                 continue;
    321             }
    322             deps = data.args;
    323             for(i = 0, len = deps.length; mod = deps[i], i < len ; i++){
    324                 if(hasOwn.call(modules, mod) && modules[mod].state != 3){
    325                     flag = false;
    326                     break;
    327                 }
    328             }
    329             if(data.state != 3 && flag){
    330                 fireFactory(prop);
    331             }
    332         }
    333     }
    334 
    335     /**
    336      * 检测循环依赖
    337      * @param  {String} id         
    338      * @param  {Array} dependencie
    339      */
    340     function checkCircularDeps(id, dependencie){
    341         var modules = lynx.modules, depslist = modules[id] ? modules[id].args : [];
    342         return ~depslist.join(' ').indexOf(dependencie);
    343     }
    344 
    345     /**
    346      * create
    347      * @param {String} type CSS|JS
    348      * @param {String} url
    349      * @param {Function} callback
    350      */
    351     function loadSource(type, url, callback){
    352         var ndoe, modules = lynx.modules;
    353         if(type == 'JS'){
    354             var node = doc.createElement("script");
    355             node[node.onreadystatechange ? 'onreadystatechange' : 'onload'] = function(){
    356                 if(!node.onreadystatechange || /loaded|complete/i.test(node.readyState)){
    357                     callback();
    358                     node.onload = node.onreadystatechange = node.onerror = null;
    359                     var fn = lynx.stacks.pop();
    360                     fn && fn.call(null, node.src);
    361                     head.removeChild(node);
    362                 }
    363             }
    364             node.src = url;
    365             modules[url].state = 1;
    366             lynx.loading++;
    367         }else if(type == 'CSS'){
    368             var node = doc.createElement("link");
    369             node.rel = 'stylesheet';
    370             node.href = url;
    371             delete modules[url];
    372         }
    373         node.onerror = function(){
    374             lynx.error('模块[url:'+ node.src +']加载失败');
    375             node.onload = node.onreadystatechange = node.onerror = null;
    376             lynx.loading--;
    377             head.removeChild(node);
    378         }
    379         return node;
    380 
    381     };
    382 
    383     lynx.mix(lynx, {
    384         load : function(urls){
    385             var loading , total = configure.total,modules = lynx.modules, url, node = fragment, type;
    386             while((loading = lynx.loading) < total && (url = urls.shift())){
    387                 type = url.slice(url.lastIndexOf('.') + 1).toUpperCase();
    388                 node.appendChild(loadSource(type, url, function(){
    389                     lynx.loading--;
    390                     var urls = lynx.urls;
    391                     urls.length && lynx.load(urls); 
    392                 }));
    393             }
    394             head.insertBefore(node, head.firstChild);
    395         },
    396 
    397         /**
    398          * 加载JS文件
    399          * @param {String} url
    400          * @param {Function} callback 
    401          */
    402         loadJS : function(url, callback){
    403             var node = loadSource('JS', url, callback)
    404             head.insertBefore(node, head.firstChild);
    405         },
    406 
    407         /**
    408          * 加载CSS文件
    409          * @param {String} url
    410          * @param {Function} callback
    411          */
    412         loadCSS : function(url, callback){
    413             var node = loadSource('CSS', url, callback);
    414             head.insertBefore(node, head.firstChild);
    415         },
    416 
    417         /**
    418          * get current script [此方法来自司徒正美的博客]
    419          * @return {String}
    420          */
    421         getCurrentScript : function(){
    422             //取得正在解析的script节点
    423             if (doc.currentScript) { //firefox 4+
    424                 return doc.currentScript.src;
    425             }
    426             // 参考 https://github.com/samyk/jiagra/blob/master/jiagra.js
    427             var stack;
    428             try {
    429                 a.b.c(); //强制报错,以便捕获e.stack
    430             } catch (e) { //safari的错误对象只有line,sourceId,sourceURL
    431                 stack = e.stack; 
    432                 if (!stack && window.opera) {
    433                     //opera 9没有e.stack,但有e.Backtrace,但不能直接取得,需要对e对象转字符串进行抽取
    434                     stack = (String(e).match(/of linked script \S+/g) || []).join(" ");
    435                 }
    436             }
    437             if (stack) {
    438                 /**e.stack最后一行在所有支持的浏览器大致如下:
    439                  *chrome23:
    440                  * at http://113.93.50.63/data.js:4:1
    441                  *firefox17:
    442                  *@http://113.93.50.63/query.js:4
    443                  *opera12:http://www.oldapps.com/opera.php?system=Windows_XP
    444                  *@http://113.93.50.63/data.js:4
    445                  *IE10:
    446                  *  at Global code (http://113.93.50.63/data.js:4:1)
    447                  */
    448                 stack = stack.split(/[@ ]/g).pop(); //取得最后一行,最后一个空格或@之后的部分
    449                 stack = stack[0] === "(" ? stack.slice(1, -1) : stack;
    450                 return stack.replace(/(:\d+)?:\d+$/i, ""); //去掉行号与或许存在的出错字符起始位置
    451             }
    452             var nodes = head.getElementsByTagName("script"); //只在head标签中寻找
    453             for (var i = 0, node; node = nodes[i++]; ) {
    454                 if (node.readyState === "interactive") {
    455                     return node.src;
    456                 }
    457             }    
    458         },
    459 
    460         /**
    461          * 配置模块信息
    462          * @param  {Object} option
    463          */
    464         config : function(option){
    465             lynx.mix(configure, option);
    466         },
    467 
    468 
    469     //============================== DOM Ready =============================
    470         
    471         /**
    472          * dom ready
    473          * @param {Function} callback
    474          */
    475         ready : function (){                              
    476             var isReady = false;
    477             var readyList = [];
    478             var ready = function(fn){
    479                 if(isReady){
    480                     fn();
    481                 }else{
    482                     readyList.push(fn);
    483                 }
    484             };
    485 
    486             var fireReady = function(){
    487                 for(var i = 0,len = readyList.length; i < len; i++){
    488                     readyList[i]();
    489                 }
    490                 readyList = [];
    491                 lynx.modules.ready.state = 3;
    492                 checkLoadReady();
    493             };
    494 
    495             var bindReady = function(){
    496                 if(isReady){
    497                     return;
    498                 }
    499                 isReady=true;
    500                 fireReady();
    501                 if(doc.removeEventListener){
    502                     doc.removeEventListener("DOMContentLoaded",bindReady,false);
    503                 }else if(doc.attachEvent){
    504                     doc.detachEvent("onreadystatechange", bindReady);
    505                 }               
    506             };
    507 
    508             if( doc.readyState === "complete" ) {
    509                 bindReady();
    510             }else if(doc.addEventListener){
    511                 doc.addEventListener("DOMContentLoaded", bindReady, false);
    512             }else if(doc.attachEvent){
    513                 doc.attachEvent("onreadystatechange", function(){
    514                     if((/loaded|complete/).test(doc.readyState)){
    515                         bindReady();
    516                     }                       
    517                 });
    518                 (function(){
    519                     if(isReady){
    520                         return;
    521                     }
    522                     var node = new Image();
    523                     var timer = setInterval(function(){
    524                         try{
    525                             isReady || node.doScroll('left');
    526                             node = null;
    527                         }catch(e){
    528                             return;
    529                         }
    530                         clearInterval(timer);
    531                         bindReady();
    532                     }, 16);
    533                 }());
    534             }
    535             return ready;
    536         }()
    537     });
    538     
    539     win.lynx = lynx;
    540 }(window));
    //使用方法
    //index.html 页面
    <html>
        <head>
            <title>测试</title>
            <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
            <script type="text/javascript" src="lynx.js"></script>
            <script type="text/javascript">
                lynx.require(['test.css','hello'], function(hello){
                    alert(hello);
                })
            </script>
        </head>
        <body>
            <div class="test"></div>
        </body>
    </html>
    
    //test.css 文件
    .test{
         200px;
        height: 100px;
        background-color: red;
    }
    
    //hello.js 文件
    define('hello', 'test',function(test){
        return 'a' + test;
    });
    
    //test.js 文件
    define('test',function(test){
        return 'b';
    });
  • 相关阅读:
    带锚点URL同一页面滚动效果的实现
    思路先行
    transliteration -- 2个功能
    html5 section article
    fields('t')
    使用Bootstrap
    JavaScript Switch
    菜单
    写一个博客页面
    自动适应
  • 原文地址:https://www.cnblogs.com/lynxcat/p/2954943.html
Copyright © 2011-2022 走看看