JavaScript AMD 模块加载器原理与实现
关于前端模块化,玉伯在其博文 前端模块化开发的价值 中有论述,有兴趣的同学可以去阅读一下。
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. 参考