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;
});

 

posted @ 2013-03-08 17:56  猫猫大侠  阅读(2263)  评论(1编辑  收藏  举报