關於前端模塊化,玉伯在其博文 前端模塊化開發的價值 中有論述,有興趣的同學可以去閱讀一下。
1. 模塊加載器
模塊加載器目前比較流行的有 Requirejs 和 Seajs。前者遵循 AMD規范,后者遵循 CMD規范。前者的規范產出比較適合於瀏覽器異步環境的習慣,后者的規范產出對於寫過 nodejs 的同學來說是比較爽的。關於兩者的比較,有興趣的同學請參看玉伯在知乎的回答 AMD和CMD的區別有哪些。本文希望能按照 AMD 規范來簡單實現自己的一個模塊加載器,以此來搞清楚模塊加載器的工作原理。
2. AMD規范與接口定義
在實現之前,我們需要擬定實現的API,然后才能進行下一步的編碼。出於學習的目的,並沒有完全實現 AMD規范 中定義的內容,簡單實現的API如下:
1 // 定義模塊 2 define(id?, dependencies?, factory); 3 4 // 調用模塊 5 require(dependencies?, factory); 6 7 // 模塊加載器配置 8 require.config({ 9 paths: {}, 10 shim: { 11 'xx': { 12 deps: [], 13 exports: '' 14 } 15 } 16 17 }); 18 19 // 模塊加載器標識 20 define.amd = {};
假如我們有以下的開發目錄:
1 scripts 2 |-- a.js 3 |-- b.js 4 |-- c.js 5 |-- d.js 6 |-- main.js 7 define.js 8 index.html
除了 define.js 為需要實現的內容,各個文件的大概內容為:
1 // a.js 2 define(['b'], function(b) { 3 4 return { 5 say: function() { 6 return 'a call: ' + b; 7 } 8 }; 9 10 }); 11 12 13 // b.js 14 define(function() { 15 return 'this is b'; 16 }); 17 18 19 // c.js 20 (function(global) { 21 global.NotAmd = function() { 22 return 'c, not amd module'; 23 } 24 })(window); 25 26 27 // d.js 28 define(['b'], function(b) { 29 30 return { 31 say: function() { 32 return 'd call: ' + b; 33 } 34 }; 35 36 }); 37 38 39 // main.js 40 require.config({ 41 paths: { 42 'notAmd': './c' 43 }, 44 shim: { 45 'notAmd': { 46 exports: 'NotAmd' 47 } 48 } 49 }); 50 51 require(['a', 'notAmd', 'd'], function(a, notAmd, d) { 52 console.log(a.say()); // should be: a call: this is b 53 console.log(notAmd()); // should be: c, not amd module 54 console.log(d.say()); // should be: d call: this is b 55 }); 56 57 58 // index.html 59 <script src="vendors/define.js" data-main="scripts/main"></script>
上面的代碼完全兼容於 Requirejs,將 define.js 換成 Requirejs,上面的代碼就能成功跑起來。這里我們需要實現 define.js 來達到同樣的效果。
3. 實現
一個文件對於一個模塊。先看一下模塊加載器的主要執行流程:
整個流程其實就是加載主模塊(data-main指定的模塊,里面有require調用),然后加載require的依賴模塊,當所有的模塊及其依賴模塊都已加載完畢,執行require調用中的factory方法。
在實現過程中需要考慮到的點有:
1. 構造一個對象,用以保存模塊的標識、依賴、工廠方法等信息。
2. 非AMD模塊的支持。非AMD模塊不會調用define方法來定義自己,如果不支持非AMD模塊,那么該模塊在加載完畢之后流程會中斷,其exports的結果也不對。
3. 采用url來作為模塊標識,由於url的唯一性,不同目錄同id的模塊就不會相互覆蓋。
4. 循環依賴。可分為兩種依賴方式:
1 // 弱依賴:不在factory中直接執行依賴模塊的方法 2 // a.js 3 define(['b'], function(b) { 4 return { 5 say: function() { 6 b.say(); 7 } 8 } 9 }); 10 11 // b.js 12 define(['a'], function(a) { 13 return { 14 say: function(a) { 15 a.say(); 16 } 17 } 18 }); 19 20 // 強依賴:直接在factory中執行依賴模塊的方法 21 // a.js 22 define(['b'], function(b) { 23 b.say(); 24 25 return { 26 say: function() { 27 return 'this is a'; 28 } 29 } 30 }); 31 32 // b.js 33 define(['a'], function(a) { 34 a.say(); 35 36 return { 37 say: function() { 38 return 'this is b'; 39 } 40 } 41 });
對於弱依賴,程序的解決方式是首先傳遞undefined作為其中一個依賴模塊的exports結果,當該依賴模塊的factory成功執行后,其就能返回正確的exports值。對於強依賴,程序會異常。但是如果確實在應用中發生了強依賴,我們可以用另外一種方式去解決,那就是模塊加載器會傳遞該模塊的exports參數給factory,factory直接將方法掛載在exports上。其實這也相當於將其轉換為了弱依賴。不過大部分情況下,程序里面發生了循環依賴,往往是我們的設計出現了問題。
好了,下面是 define.js 實現的代碼:

1 /*jslint regexp: true, nomen: true, sloppy: true */ 2 /*global window, navigator, document, setTimeout, opera */ 3 (function(global, undefined) { 4 var document = global.document, 5 head = document.head || document.getElementsByTagName('head')[0] || document.documentElement, 6 baseElement = document.getElementsByTagName('base')[0], 7 noop = function(){}, 8 currentlyAddingScript, interactiveScript, anonymousMeta, 9 dirnameReg = /[^?#]*\//, 10 dotReg = /\/\.\//g, 11 doubleDotReg = /\/[^/]+\/\.\.\//, 12 multiSlashReg = /([^:/])\/+\//g, 13 ignorePartReg = /[?#].*$/, 14 suffixReg = /\.js$/, 15 16 seed = { 17 // 緩存模塊 18 modules: {}, 19 config: { 20 baseUrl: '', 21 charset: '', 22 paths: {}, 23 shim: {}, 24 urlArgs: '' 25 } 26 }; 27 28 /* utils */ 29 function isType(type) { 30 return function(obj) { 31 return {}.toString.call(obj) === '[object ' + type + ']'; 32 } 33 } 34 35 var isFunction = isType('Function'); 36 var isString = isType('String'); 37 var isArray = isType('Array'); 38 39 40 function hasProp(obj, prop) { 41 return Object.prototype.hasOwnProperty.call(obj, prop); 42 } 43 44 /** 45 * 遍歷數組,回調返回 true 時終止遍歷 46 */ 47 function each(arr, callback) { 48 var i, len; 49 50 if (isArray(arr)) { 51 for (i = 0, len = arr.length; i < len; i++) { 52 if (callback(arr[i], i, arr)) { 53 break; 54 } 55 } 56 } 57 } 58 59 /** 60 * 反向遍歷數組,回調返回 true 時終止遍歷 61 */ 62 function eachReverse(arr, callback) { 63 var i; 64 65 if (isArray(arr)) { 66 for (i = arr.length - 1; i >= 0; i--) { 67 if (callback(arr[i], i, arr)) { 68 break; 69 } 70 } 71 } 72 } 73 74 /** 75 * 遍歷對象,回調返回 true 時終止遍歷 76 */ 77 function eachProp(obj, callback) { 78 var prop; 79 for (prop in obj) { 80 if (hasProp(obj, prop)) { 81 if (callback(obj[prop], prop)) { 82 break; 83 } 84 } 85 } 86 } 87 88 /** 89 * 判斷是否為一個空白對象 90 */ 91 function isPlainObject(obj) { 92 var isPlain = true; 93 94 eachProp(obj, function() { 95 isPlain = false; 96 return true; 97 }); 98 99 return isPlain; 100 } 101 102 /** 103 * 復制源對象的屬性到目標對象中 104 */ 105 function mixin(target, source) { 106 if (source) { 107 eachProp(source, function(value, prop) { 108 target[prop] = value; 109 }); 110 } 111 return target; 112 } 113 114 function makeError(name, msg) { 115 throw new Error(name + ":" + msg); 116 } 117 118 /** 119 * 獲取全局變量值。允許格式:a.b.c 120 */ 121 function getGlobal(value) { 122 if (!value) { 123 return value; 124 } 125 var g = global; 126 each(value.split('.'), function(part) { 127 g = g[part]; 128 }); 129 return g; 130 } 131 132 133 /* path */ 134 /** 135 * 獲取path對應的目錄部分 136 * 137 * a/b/c.js?foo=1#d/e --> a/b/ 138 */ 139 function dirname(path) { 140 var m = path.match(dirnameReg); 141 142 return m ? m[0] : "./"; 143 } 144 145 /** 146 * 規范化path 147 * 148 * http://test.com/a//./b/../c --> "http://test.com/a/c" 149 */ 150 function realpath(path) { 151 // /a/b/./c/./d --> /a/b/c/d 152 path = path.replace(dotReg, "/"); 153 154 // a//b/c --> a/b/c 155 // a///b////c --> a/b/c 156 path = path.replace(multiSlashReg, "$1/"); 157 158 // a/b/c/../../d --> a/b/../d --> a/d 159 while (path.match(doubleDotReg)) { 160 path = path.replace(doubleDotReg, "/"); 161 } 162 163 return path; 164 } 165 166 /** 167 * 將模塊id解析為對應的url 168 * 169 * rules: 170 * baseUrl: http://gcfeng.github.io/blog/js 171 * host: http://gcfeng.github.io/blog 172 * 173 * http://gcfeng.github.io/blog/js/test.js --> http://gcfeng.github.io/blog/js/test.js 174 * test --> http://gcfeng.github.io/blog/js/test.js 175 * ../test.js --> http://gcfeng.github.io/blog/test.js 176 * /test.js --> http://gcfeng.github.io/blog/test.js 177 * test?foo#bar --> http://gcfeng.github.io/blog/test.js 178 * 179 * @param {String} id 模塊id 180 * @param {String} baseUrl 模塊url對應的基地址 181 */ 182 function id2Url(id, baseUrl) { 183 var config = seed.config; 184 185 id = config.paths[id] || id; 186 187 // main///test?foo#bar --> main/test?foo#bar 188 id = realpath(id); 189 190 // main/test?foo#bar --> main/test 191 id = id.replace(ignorePartReg, ""); 192 193 id = suffixReg.test(id) ? id : (id + '.js'); 194 195 id = realpath(dirname(baseUrl) + id); 196 197 id = id + (config.urlArgs || ""); 198 199 return id; 200 } 201 202 203 function getScripts() { 204 return document.getElementsByTagName('script'); 205 } 206 207 /** 208 * 獲取當前正在運行的腳本 209 */ 210 function getCurrentScript() { 211 if (currentlyAddingScript) { 212 return currentlyAddingScript; 213 } 214 215 if (interactiveScript && interactiveScript.readyState === 'interactive') { 216 return interactiveScript; 217 } 218 219 if (document.currentScript) { 220 return interactiveScript = document.currentScript; 221 } 222 223 eachReverse(getScripts(), function (script) { 224 if (script.readyState === 'interactive') { 225 return (interactiveScript = script); 226 } 227 }); 228 return interactiveScript; 229 } 230 231 /** 232 * 請求JavaScript文件 233 */ 234 function loadScript(url, callback) { 235 var config = seed.config, 236 node = document.createElement('script'), 237 supportOnload = 'onload' in node; 238 239 node.charset = config.charset || 'utf-8'; 240 node.setAttribute('data-module', url); 241 242 // 綁定事件 243 if (supportOnload) { 244 node.onload = function() { 245 onload(); 246 }; 247 node.onerror = function() { 248 onload(true); 249 } 250 } else { 251 node.onreadystatechange = function() { 252 if (/loaded|complete/.test(node.readyState)) { 253 onload(); 254 } 255 } 256 } 257 258 node.async = true; 259 node.src = url; 260 261 // 在IE6-8瀏覽器中,某些緩存會導致結點一旦插入就立即執行腳本 262 currentlyAddingScript = node; 263 264 // ref: #185 & http://dev.jquery.com/ticket/2709 265 baseElement ? head.insertBefore(node, baseElement) : head.appendChild(node); 266 267 currentlyAddingScript = null; 268 269 270 function onload(error) { 271 // 保證執行一次 272 node.onload = node.onerror = node.onreadystatechange = null; 273 // 刪除腳本節點 274 head.removeChild(node); 275 node = null; 276 callback(error); 277 } 278 } 279 280 281 282 // 記錄模塊的狀態信息 283 Module.STATUS = { 284 // 初始狀態,此時模塊剛剛新建 285 INITIAL: 0, 286 // 加載module.url指定資源 287 FETCH: 1, 288 // 保存module的依賴信息 289 SAVE: 2, 290 // 解析module的依賴內容 291 LOAD: 3, 292 // 執行模塊,exports還不可用 293 EXECUTING: 4, 294 // 模塊執行完畢,exports可用 295 EXECUTED: 5, 296 // 出錯:請求或者執行出錯 297 ERROR: 6 298 }; 299 300 function Module(url, deps) { 301 this.url = url; 302 this.deps = deps || []; // 依賴模塊列表 303 this.dependencies = []; // 依賴模塊實例列表 304 this.refs = []; // 引用模塊列表,用於模塊加載完成之后通知其引用模塊 305 this.exports = {}; 306 this.status = Module.STATUS.INITIAL; 307 308 /* 309 this.id 310 this.factory 311 */ 312 } 313 314 Module.prototype = { 315 constructor: Module, 316 317 load: function() { 318 var mod = this, 319 STATUS = Module.STATUS, 320 args = []; 321 322 if (mod.status >= STATUS.LOAD) { 323 return mod; 324 } 325 mod.status = STATUS.LOAD; 326 327 mod.resolve(); 328 mod.pass(); 329 mod.checkCircular(); 330 331 each(mod.dependencies, function(dep) { 332 if (dep.status < STATUS.FETCH) { 333 dep.fetch(); 334 } else if (dep.status === STATUS.SAVE) { 335 dep.load(); 336 } else if (dep.status >= STATUS.EXECUTED) { 337 args.push(dep.exports); 338 } 339 }); 340 341 mod.status = STATUS.EXECUTING; 342 343 // 依賴模塊加載完成 344 if (args.length === mod.dependencies.length) { 345 args.push(mod.exports); 346 mod.makeExports(args); 347 mod.status = STATUS.EXECUTED; 348 mod.fireFactory(); 349 } 350 }, 351 352 /** 353 * 初始化依賴模塊 354 */ 355 resolve: function() { 356 var mod = this; 357 358 each(mod.deps, function(id) { 359 var m, url; 360 361 url = id2Url(id, seed.config.baseUrl); 362 m = Module.get(url); 363 m.id = id; 364 mod.dependencies.push(m); 365 }); 366 }, 367 368 /** 369 * 傳遞模塊給依賴模塊,用於依賴模塊加載完成之后通知引用模塊 370 */ 371 pass: function() { 372 var mod = this; 373 374 each(mod.dependencies, function(dep) { 375 var repeat = false; 376 377 each(dep.refs, function(ref) { 378 if (ref === mod.url) { 379 repeat = true; 380 return true; 381 } 382 }); 383 384 if (!repeat) { 385 dep.refs.push(mod.url); 386 } 387 }); 388 }, 389 390 /** 391 * 解析循環依賴 392 */ 393 checkCircular: function() { 394 var mod = this, 395 STATUS = Module.STATUS, 396 isCircular = false, 397 args = []; 398 399 each(mod.dependencies, function(dep) { 400 isCircular = false; 401 // 檢測是否存在循環依賴 402 if (dep.status === STATUS.EXECUTING) { 403 each(dep.dependencies, function(m) { 404 if (m.url === mod.url) { 405 // 存在循環依賴 406 return isCircular = true; 407 } 408 }); 409 410 // 嘗試解決循環依賴 411 if (isCircular) { 412 each(dep.dependencies, function(m) { 413 if (m.url !== mod.url && m.status >= STATUS.EXECUTED) { 414 args.push(m.exports); 415 } else if (m.url === mod.url) { 416 args.push(undefined); 417 } 418 }); 419 420 if (args.length === dep.dependencies.length) { 421 // 將exports作為最后一個參數傳遞 422 args.push(dep.exports); 423 try { 424 dep.exports = isFunction(dep.factory) ? dep.factory.apply(global, args) : dep.factory; 425 dep.status = STATUS.EXECUTED; 426 } catch (e) { 427 dep.exports = undefined; 428 dep.status = STATUS.ERROR; 429 makeError("Can't fix circular dependency", mod.url + " --> " + dep.url); 430 } 431 } 432 } 433 } 434 }); 435 }, 436 437 makeExports: function(args) { 438 var mod = this, 439 result; 440 441 result = isFunction(mod.factory) ? mod.factory.apply(global, args) : mod.factory; 442 mod.exports = isPlainObject(mod.exports) ? result : mod.exports; 443 }, 444 445 /** 446 * 模塊執行完畢,觸發引用模塊回調 447 */ 448 fireFactory: function() { 449 var mod = this, 450 STATUS = Module.STATUS; 451 452 each(mod.refs, function(ref) { 453 var args = []; 454 ref = Module.get(ref); 455 456 each(ref.dependencies, function(m) { 457 if (m.status >= STATUS.EXECUTED) { 458 args.push(m.exports); 459 } 460 }); 461 462 if (args.length === ref.dependencies.length) { 463 args.push(ref.exports); 464 ref.makeExports(args); 465 ref.status = STATUS.EXECUTED; 466 ref.fireFactory(); 467 } else { 468 ref.load(); 469 } 470 }); 471 }, 472 473 /** 474 * 發送請求加載資源 475 */ 476 fetch: function() { 477 var mod = this, 478 STATUS = Module.STATUS; 479 480 if (mod.status >= STATUS.FETCH) { 481 return mod; 482 } 483 mod.status = STATUS.FETCH; 484 485 loadScript(mod.url, function(error) { 486 mod.onload(error); 487 }); 488 }, 489 490 onload: function(error) { 491 var mod = this, 492 config = seed.config, 493 STATUS = Module.STATUS, 494 shim, shimDeps; 495 496 if (error) { 497 mod.exports = undefined; 498 mod.status = STATUS.ERROR; 499 mod.fireFactory(); 500 return mod; 501 } 502 503 // 非AMD模塊 504 shim = config.shim[mod.id]; 505 if (shim) { 506 shimDeps = shim.deps || []; 507 mod.save(shimDeps); 508 mod.factory = function() { 509 return getGlobal(shim.exports); 510 }; 511 mod.load(); 512 } 513 514 // 匿名模塊 515 if (anonymousMeta) { 516 mod.factory = anonymousMeta.factory; 517 mod.save(anonymousMeta.deps); 518 mod.load(); 519 anonymousMeta = null; 520 } 521 }, 522 523 save: function(deps) { 524 var mod = this, 525 STATUS = Module.STATUS; 526 527 if (mod.status >= STATUS.SAVE) { 528 return mod; 529 } 530 mod.status = STATUS.SAVE; 531 532 each(deps, function(d) { 533 var repeat = false; 534 each(mod.dependencies, function(d2) { 535 if (d === d2.id) { 536 return repeat = true; 537 } 538 }); 539 540 if (!repeat) { 541 mod.deps.push(d); 542 } 543 }); 544 } 545 }; 546 547 548 /** 549 * 初始化模塊加載 550 */ 551 Module.init = function() { 552 var script, scripts, initMod, url; 553 554 if (document.currentScript) { 555 script = document.currentScript; 556 } else { 557 // 正常情況下,在頁面加載時,當前js文件的script標簽始終是最后一個 558 scripts = getScripts(); 559 script = scripts[scripts.length - 1]; 560 } 561 initMod = script.getAttribute("data-main"); 562 // see http://msdn.microsoft.com/en-us/library/ms536429(VS.85).aspx 563 url = script.hasAttribute ? script.src : script.getAttribute("src", 4); 564 565 // 如果seed是通過script標簽內嵌到頁面,baseUrl為當前頁面的路徑 566 seed.config.baseUrl = dirname(initMod || url); 567 568 // 加載主模塊 569 if (initMod) { 570 Module.use(initMod.split(","), noop, Module.guid()); 571 } 572 573 scripts = script = null; 574 }; 575 576 /** 577 * 生成一個唯一id 578 */ 579 Module.guid = function() { 580 return "seed_" + (+new Date()) + (Math.random() + '').slice( -8 ); 581 }; 582 583 /** 584 * 獲取一個模塊,如果不存在則新建 585 * 586 * @param url 587 * @param deps 588 */ 589 Module.get = function(url, deps) { 590 return seed.modules[url] || (seed.modules[url] = new Module(url, deps)); 591 }; 592 593 /** 594 * 加載模塊 595 * 596 * @param {Array} ids 依賴模塊的id列表 597 * @param {Function} callback 模塊加載完成之后的回調函數 598 * @param {String} id 模塊id 599 */ 600 Module.use = function(ids, callback, id) { 601 var config = seed.config, 602 mod, url; 603 604 ids = isString(ids) ? [ids] : ids; 605 url = id2Url(id, config.baseUrl); 606 mod = Module.get(url, ids); 607 mod.id = id; 608 mod.factory = callback; 609 610 mod.load(); 611 }; 612 613 // 頁面已經存在AMD加載器或者seed已經加載 614 if (global.define) { 615 return; 616 } 617 618 define = function(id, deps, factory) { 619 var currentScript, mod; 620 621 // define(factory) 622 if (isFunction(id)) { 623 factory = id; 624 deps = []; 625 id = undefined; 626 627 } 628 629 // define(deps, factory) 630 else if (isArray(id)) { 631 factory = deps; 632 deps = id; 633 id = undefined; 634 } 635 636 if (!id && (currentScript = getCurrentScript())) { 637 id = currentScript.getAttribute("data-module"); 638 } 639 640 if (id) { 641 mod = Module.get(id); 642 mod.factory = factory; 643 mod.save(deps); 644 mod.load(); 645 } else { 646 anonymousMeta = { 647 deps: deps, 648 factory: factory 649 }; 650 } 651 }; 652 653 define.amd = {}; 654 655 require = function(ids, callback) { 656 // require("test", callback) 657 if (isString(ids)) { 658 makeError("Invalid", "ids can't be string"); 659 } 660 661 // require(callback) 662 if (isFunction(ids)) { 663 callback = ids; 664 ids = []; 665 } 666 667 Module.use(ids, callback, Module.guid()); 668 }; 669 670 require.config = function(config) { 671 mixin(seed.config, config); 672 }; 673 674 675 // 初始化 676 Module.init(); 677 })(window);
變量 seed 保存加載過的模塊和一些配置信息。對象 Module 用來描述一個模塊,Module.STATUS 描述一個模塊的狀態信息,define.js 加載完畢之后調用 Module.init 來初始化baseUrl 和主模塊。當主模塊調用require方法后,程序就會去加載相關的依賴模塊。
有一個需要注意的地方是 動態創建的script,在腳本加載完畢之后,會立即執行返回的代碼。對於AMD模塊,其加載完畢之后會執行define方法,如果該模塊為匿名模塊(沒有指定id),我們需要在onload回調中來處理該模塊。在開始加載模塊的時候,我們不會知道其依賴和工廠方法等信息,需要在這個模塊加載完畢執行define方法才能獲得。
4. 參考