zoukankan      html  css  js  c++  java
  • javascript【AMD模块加载器】浅析

    很久没有写博客了,一来是工作比较忙,二来主要是觉得没什么可写。当然,自己的懒惰也是不可推卸的责任。
    最近有点空余的时间,就看了一下AMD模块加载。关于它的定义和优缺点就不介绍了,园子里大把。相信大家也都知道。主要说一下加载器的原理以及在开发过程当中遇到的一些坑。当然由于加载器不是规范的一部分,所以实现方法也各不相同。我所用的方法也不是最优的,只是用来当作学习而已。

    【模块加载器原理】
    1.开始
    2.通过模块名解析出模块信息,以及计算出URL。
    3.通过创建SCRIPT的形式把模块加载到页面中。(当然也可以采用其它方法,如XHR。 IFRAME等等)
    4.判断被加载的脚本,如果发现它有依赖就去加载依赖模块。如果不依赖其它模块,就直接执行factory方法。
    5.等所有脚本都被加载完毕就执行加载完成之后的回调函数。
    【实现的过程】
    在弄懂了整个加载器的工作原理之后,就开始具体的编码过程。最开始,我使用了moduleCache,modules,moduleLoads这三个对象。分别记录加载中需要用到的信息。首先我把整个加载的信息存储到moduleCache中。其中的结构大致如下。

    moduleCache[uuid] = {
        uuid : uuid,    //随即生成的id
        deps : deps, //此次加载需需要加载的模块
       args : args, //回调函数需要的方法
       callback : callback  //此次加载完成后需要调用的回调函数      
    }

    在modules中储存具体的模块信息,也就是moduleCache[uuid]的deps中的模块的具体信息。结构很简单,主要是记录一下名字,和url和状态。

    modules[modname] = {
        name : name, //模块名字
        url : url, //模块的url
        state : state //模块的状态   
    }

    moduleLoads 中是存储的加载信息,也就是uuid的数组。 过程很顺利,很快就完成了开发。当然有一个前提,加载的模块是不能依赖其它模块的。 实现的大致原理就是。分析完毕模块信息,储存完上面的信息后。创建一个script来加载模块。当被加载的模块的define执行的时候,就通过模块名去modules中把模块的状态改为1,然后执行模块的factory方法。得到exports 放入到modules[modname].exports 中。 然后循环moduleLoads,得到uuid去moduleCache中的数据。然后判断deps的模块是否全部加载完毕。如果加载完毕就执行callback方法。 然后把uuid从moduleLoads中删除。

    然后开始实现加载的模块依赖别的模块,一开始的做法是当发现加载的模块存在依赖的时候,就从新调用require方法去加载模块需要的模块。 然而这样就造成了一个问题。就是等所有的被依赖的模块加载完毕之后,按照上面的流程执行完毕。无法告知需要依赖的那个模块它依赖的模块都被加载完毕了。可能这么说不是太直观,看一下现在变量里存储的信息就一目了然了。

    //加载hello模块,hello模块又依赖test模块。
    moduleCache = {
         cb100001 : {
            uuid : cb10001
            deps: {hello}
            args:[hello]
            callback : callback   
         } ,
         cb100002:{
             uuid:cb10002,
             deps:{test}
             args:[test]
             callback : callback
         }  
    }
    modules : {
         hello : {
            name:hello,
            url:url,
            state : 1,
            exports : {} 
        },
        test: {
            name:test
            url:url
            state:2
            exports:{}
        }
    }

    就像上面这样,test已经加载完毕。uuid 为cb10002的moduleCache的信息已经执行完毕。但是无法让hello模块加载完毕。 苦思冥想了许久,终于找到了一个解决方案。就是在申请一个变量,来存储模块的依赖信息。 结构如下

    var moduledeps = {
        'hello' : ['test']
        'test' : ['module1','module2']
    }

    这样一来就解决了两个问题,一个是循环依赖的问题,可以通过上面的结构被检测出来。二来就是可以在被依赖模块被加载完毕之后遍历上面的moduledeps来将需要依赖的那个模块状态改为加载完成。从而执行moduleCache中的回调。又经过一番编码之后,初级版本的模块加载器终于完成了。 实现了并行下载依赖模块,可以检测循环依赖。在各个浏览器下测试。似乎都没什么问题。然后当我去测试加载在不同目录的两个同名模块的时候,问题产生了。后加载的模块,覆盖了前面的同名模块的信息。 后来在群里经过一番讨论,决定用URL来做modules的key。这样就避免了覆盖的问题。 同时又优化了加载器的结构,将3个变量改为两个变量。 保留了modules与moduleCache,去掉了moduleLoads与moduledeps。

    结构如下。

    modules = {
         url1 : {
            name: hello,
            url : url1,
            state : 1
            exports : {}
         },
         url2 : {
            name: test,
            url : url2,
            state : 2
            exports : {}
         }
    }
    
    moduleCache : {
         cbi10001 : {
              state: 1,
              uuid : cbi10001,
              factory : callback,
              args : [url1],l
              deps :{url1:'lynx'}
         },
        url1 : {
              state: 1,
              uuid : url1,
              factory : callback,
              args : [url2],
              deps :{url2:'lynx'}
         }
    }

    这样通过url既可以获得模块的依赖,又能够获得模块。 所以就不用modoleLoads与moduleDeps了。然后在define中获得url又有一个坑就是在safari下无法获得正在被解析的script。获得正在被解析的script请参见正美大大的这篇文章。 不过在safari下又另外一个特性就是在脚本解析完成之后会立即调用脚本的onload事件,如此一来就找到了解决办法。 就是在脚本解析的时候,存入一个函数到某个数组中,然后在它的onload事件中取出这个函数。将node的url传入函数中就可以了,唯一的坏处就是比可以获得url要慢上一点点。想到办法之后便开始改代码,经过半天左右的编码终于完成了。 下面是全部源码。在各个浏览器中测试都通过。但由于个人能力有限,其中未被发现的bug定所难免,如果各位发现其中的bug或有什么不足的地方请告知。

    View Code
      1 View Code 
      2 
      3 (function(win, undefined){
      4     win = win || window;
      5     var doc = win.document || document,
      6         head = doc.head || doc.getElementsByTagName("head")[0];
      7         hasOwn = Object.prototype.hasOwnProperty,
      8         slice = Array.prototype.slice,
      9         basePath = (function(nodes){
     10             var node = nodes[nodes.length - 1],
     11             url = (node.hasAttribute ? node.src : node.getAttribute("src", 4)).replace(/[?#].*/, "");
     12             return url.slice(0, url.lastIndexOf('/') + 1);
     13         }(doc.getElementsByTagName('script')));
     14 
     15     function lynxcat(){
     16 
     17     }
     18     lynxcat.prototype = {
     19         constructor : lynxcat,
     20         init : function(){
     21 
     22         }
     23     }
     24     lynxcat.prototype.init.prototype = lynxcat.prototype;
     25 
     26     /**
     27      * mix
     28      * @param  {Object} target   目标对象
     29      * @param  {Object} source   源对象
     30      * @return {Object}          目标对象
     31      */
     32     lynxcat.mix = function(target, source){
     33         if( !target || !source ) return;
     34         var args = slice.call(arguments), i = 1, override = typeof args[args.length - 1] === "boolean" ? args.pop() : true, prop;
     35         while ((source = args[i++])) {
     36             for (prop in source) {
     37                 if (hasOwn.call(source, prop) && (override || !(prop in target))) {
     38                     target[prop] = source[prop];
     39                 }
     40             }
     41         }
     42         return target;
     43     };
     44 
     45     lynxcat.mix(lynxcat, {
     46         modules : {},
     47         moduleCache : {},
     48         loadings : [],
     49 
     50         /**
     51          * parse module
     52          * @param {String} id 模块名
     53          * @param {String} basePath 基础路径
     54          * @return {Array} 
     55          */
     56         parseModule : function(id, basePath){
     57             var url, result, ret, dir, paths, i, len, ext, modname, protocol = /^(\w+\d?:\/\/[\w\.-]+)(\/(.*))?/;
     58             if(result = protocol.exec(id)){
     59                 url = id;
     60                 paths = result[3] ? result[3].split('/') : [];
     61             }else{
     62                 result = protocol.exec(basePath);
     63                 url = result[1];
     64                 paths = result[3] ? result[3].split('/') : [];
     65                 modules = id.split('/');
     66                 paths.pop();
     67                 for(i = 0, len = modules.length; i < len; i++){
     68                     dir = modules[i];
     69                     if(dir == '..'){
     70                         paths.pop();
     71                     }else if(dir !== '.'){
     72                         paths.push(dir);
     73                     }
     74                 }
     75                 url = url + '/' + paths.join('/');
     76             }
     77             modname = paths[paths.length - 1];
     78             ext = modname.slice(modname.lastIndexOf('.'));
     79             if(ext != '.js'){
     80                 url = url + '.js';
     81             }else{
     82                 modname = modname.slice(0, modname.lastIndexOf('.'));
     83             }
     84             if(modname == ''){
     85                 modname = url;
     86             }
     87             return [modname, url]
     88         },
     89 
     90         /**
     91          * get uuid
     92          * @param {String} prefix
     93          * @return {String} uuid
     94          */
     95         guid : function(prefix){
     96             prefix = prefix || '';
     97             return prefix + (+new Date()) + String(Math.random()).slice(-8);
     98         },
     99 
    100         /**
    101          * error 
    102          * @param {String} str
    103          */
    104         error : function(str){
    105             throw new Error(str);
    106         }
    107     });
    108 
    109 
    110     //================================ 模块加载 ================================
    111     /**
    112      * 模块加载方法
    113      * @param {String|Array}   ids      需要加载的模块
    114      * @param {Function} callback 加载完成之后的回调
    115      * @param {String} parent 父路径
    116      */
    117     win.require = lynxcat.require = function(ids, callback, parent){
    118         ids = typeof ids === 'string' ? [ids] : ids;
    119         var i = 0, len = ids.length, flag = true, uuid = parent || lynxcat.guid('cb_'), path = parent || basePath,
    120             modules = lynxcat.modules, moduleCache = lynxcat.moduleCache, 
    121             args = [], deps = {}, id, result;
    122         for(; i < len; i++){
    123             id = ids[i];
    124             result = lynxcat.parseModule(id, path);
    125 
    126             if(!modules[result[1]]){
    127                 modules[result[1]] = {
    128                     name : result[0],
    129                     url : result[1],
    130                     state : 0,
    131                     exports : {}
    132                 }
    133                 flag = false;
    134             }else if(modules[result[1]].state != 2){
    135                 flag = false;
    136             }
    137             if(!deps[result[1]]){
    138                 if(checkCircularDeps(uuid, result[1])){
    139                     lynxcat.error('模块[url:'+ uuid +']与模块[url:'+ result[1] +']循环依赖');
    140                 }
    141                 deps[result[1]] = 'lynxcat';
    142                 args.push(result[1]);
    143             }
    144             lynxcat.loadJS(result[1]);
    145         }
    146 
    147         moduleCache[uuid] = {
    148             uuid : uuid,
    149             factory : callback,
    150             args : args,
    151             deps : deps,
    152             state : 1
    153         }
    154 
    155         if(flag){
    156             fireFactory(uuid);
    157             return checkLoadReady();
    158         }
    159     };
    160     require.amd = lynxcat.modules;
    161 
    162     /**
    163      * @param  {String} id           模块名
    164      * @param  {String|Array} [dependencies] 依赖列表
    165      * @param  {Function} factory      工厂方法
    166      */
    167     win.define = function(id, dependencies, factory){
    168         if((typeof id === 'array' || typeof id === 'string') && typeof dependencies === 'function'){
    169             factory = dependencies;
    170             dependencies = [];
    171         }else if (typeof id == 'function'){
    172             factory = id;
    173             dependencies = [];
    174         }
    175         id = lynxcat.getCurrentScript();
    176         if(!id){
    177             lynxcat.loadings.push(function(id){
    178                 require(dependencies, factory, id);
    179             });
    180         }else{
    181             require(dependencies, factory, id);
    182         }
    183     }
    184 
    185     /**
    186      * fire factory
    187      * @param  {String} uuid
    188      */
    189     function fireFactory(uuid){
    190         var moduleCache = lynxcat.moduleCache, modules = lynxcat.modules,
    191         data = moduleCache[uuid], deps = data.args, result,
    192         i = 0, len = deps.length, args = [];
    193         for(; i < len; i++){
    194             args.push(modules[deps[i]].exports)
    195         }
    196         result = data.factory.apply(null, args);
    197         if(modules[uuid]){
    198             modules[uuid].state = 2;
    199             modules[uuid].exports = result;
    200             delete moduleCache[uuid];
    201         }else{
    202             delete lynxcat.moduleCache;
    203         }
    204         return result;
    205     }
    206 
    207     /**
    208      * 检测是否全部加载完毕
    209      */
    210     function checkLoadReady(){
    211         var moduleCache = lynxcat.moduleCache, modules = lynxcat.modules,
    212             i, data, prop, deps, mod;
    213         loop: for (prop in moduleCache) {
    214             data = moduleCache[prop];
    215             deps = data.args;
    216             for(i = 0; mod = deps[i]; i++){
    217                 if(hasOwn.call(modules, mod) && modules[mod].state != 2){
    218                     continue loop;
    219                 }
    220             }
    221             if(data.state != 2){
    222                 fireFactory(prop);
    223                 checkLoadReady();
    224             }
    225         }
    226     }
    227 
    228     /**
    229      * 检测循环依赖
    230      * @param  {String} id         
    231      * @param  {Array} dependencie
    232      */
    233     function checkCircularDeps(id, dependencie){
    234         var moduleCache = lynxcat.moduleCache, depslist = moduleCache[dependencie] ? moduleCache[dependencie].deps : {}, prop;
    235         for(prop in depslist){
    236             if(hasOwn.call(depslist, prop) && prop === id){
    237                 return true;
    238             }
    239         }
    240         return false;
    241     }
    242 
    243     lynxcat.mix(lynxcat, {
    244         /**
    245          * 加载JS文件
    246          * @param  {String} url
    247          */
    248         loadJS : function(url){
    249             var node = doc.createElement("script");
    250             node[node.onreadystatechange ? 'onreadystatechange' : 'onload'] = function(){
    251                 if(!node.onreadystatechange || /loaded|complete/i.test(node.readyState)){
    252                     var fn = lynxcat.loadings.pop();
    253                     fn && fn.call(null, node.src);
    254                     node.onload = node.onreadystatechange = node.onerror = null;
    255                     head.removeChild(node);
    256                 }
    257             }
    258             node.onerror = function(){
    259                 lynxcat.error('模块[url:'+ node.src +']加载失败');
    260                 node.onload = node.onreadystatechange = node.onerror = null;
    261                 head.removeChild(node);
    262             }
    263             node.src = url;
    264             head.insertBefore(node, head.firstChild);
    265         },
    266 
    267         /**
    268          * get current script [此方法来自司徒正美的博客]
    269          * @return {String}
    270          */
    271         getCurrentScript : function(){
    272             //取得正在解析的script节点
    273             if (doc.currentScript) { //firefox 4+
    274                 return doc.currentScript.src;
    275             }
    276             // 参考 https://github.com/samyk/jiagra/blob/master/jiagra.js
    277             var stack;
    278             try {
    279                 a.b.c(); //强制报错,以便捕获e.stack
    280             } catch (e) { //safari的错误对象只有line,sourceId,sourceURL
    281                 stack = e.stack;
    282                 if (!stack && window.opera) {
    283                     //opera 9没有e.stack,但有e.Backtrace,但不能直接取得,需要对e对象转字符串进行抽取
    284                     stack = (String(e).match(/of linked script \S+/g) || []).join(" ");
    285                 }
    286             }
    287             if (stack) {
    288                 /**e.stack最后一行在所有支持的浏览器大致如下:
    289                  *chrome23:
    290                  * at http://113.93.50.63/data.js:4:1
    291                  *firefox17:
    292                  *@http://113.93.50.63/query.js:4
    293                  *opera12:http://www.oldapps.com/opera.php?system=Windows_XP
    294                  *@http://113.93.50.63/data.js:4
    295                  *IE10:
    296                  *  at Global code (http://113.93.50.63/data.js:4:1)
    297                  */
    298                 stack = stack.split(/[@ ]/g).pop(); //取得最后一行,最后一个空格或@之后的部分
    299                 stack = stack[0] === "(" ? stack.slice(1, -1) : stack;
    300                 return stack.replace(/(:\d+)?:\d+$/i, ""); //去掉行号与或许存在的出错字符起始位置
    301             }
    302             var nodes = head.getElementsByTagName("script"); //只在head标签中寻找
    303             for (var i = 0, node; node = nodes[i++]; ) {
    304                 if (node.readyState === "interactive") {
    305                     return node.src;
    306                 }
    307             }    
    308         }
    309     });
    310     win.lynxcat = lynxcat;
    311 }(window));

    使用方法

    //hello.js文件
    define('hello', function(){
        return {world : 'hello, world!'};
    });
    
    //主文件
    lynxcat.require('hello',function(hello){
        console.log(hello.world);  //hello, world!;
    });
    
    
    //hello.js 有依赖的情况
    //hello.js文件
    define('hello', 'test',function(test){
        return {world : 'hello, world!' + test};
    });
    
    //test.js文件
    define('test', function(){
        return 'this is test';
    });
    //主文件
    lynxcat.require('hello',function(hello){
        console.log(hello.world);  //hello, world!this is test;
    });
  • 相关阅读:
    IIS服务器支持.apk文件下载
    java序列化
    ECMAScript 5/6/7兼容性速查表
    jquery获得select选中索引
    javascript获取调用方法的父引用
    AsyncCTP &IdentityModel
    开源的Owin 的身份验证支持 和跨域支持
    为什么Application_BeginRequest会执行两次
    基于Redis的消息订阅/发布
    基于异步的MVC webAPI控制器
  • 原文地址:https://www.cnblogs.com/lynxcat/p/2950373.html
Copyright © 2011-2022 走看看