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或有什么不足的地方请告知。
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; });