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。 但是重名模块就很难解决了。如果大家有更好的解决办法希望能告知一下。大家一起进步。 下面是最新的源码和使用的方法。
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{ width: 200px; height: 100px; background-color: red; } //hello.js 文件 define('hello', 'test',function(test){ return 'a' + test; }); //test.js 文件 define('test',function(test){ return 'b'; });