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;

});

 

你可能感兴趣的:(JavaScript)