首页 > 代码库 > JavaScript AMD 模块加载器原理与实现

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     scripts2         |-- a.js3         |-- b.js4         |-- c.js5         |-- d.js6         |-- main.js7     define.js8     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.js14 define(function() {15     return ‘this is b‘;16 });    17 18 19 // c.js20 (function(global) {21     global.NotAmd = function() {22         return ‘c, not amd module‘;23     }24 })(window);25 26 27 // d.js28 define([‘b‘], function(b) {29     30     return {31         say: function() {32             return ‘d call: ‘ + b;33         }34     };35         36 });37 38 39 // main.js40 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 b53     console.log(notAmd());       // should be: c, not amd module54     console.log(d.say());           // should be: d call: this is b55 });56 57 58 // index.html59 <script src="http://www.mamicode.com/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.js12 define([‘a‘], function(a) {13     return {14         say: function(a) {15             a.say();16         }17     }18 });19 20 // 强依赖:直接在factory中执行依赖模块的方法21 // a.js22 define([‘b‘], function(b) {23     b.say();24              25     return {26          say: function() {27              return ‘this is a‘;28          }29      }30 });31 32 // b.js33 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.c120      */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      * 规范化path147      *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/d152         path = path.replace(dotReg, "/");153 154         // a//b/c --> a/b/c155         // a///b////c --> a/b/c156         path = path.replace(multiSlashReg, "$1/");157 158         // a/b/c/../../d --> a/b/../d --> a/d159         while (path.match(doubleDotReg)) {160             path = path.replace(doubleDotReg, "/");161         }162 163         return path;164     }165 166     /**167      * 将模块id解析为对应的url168      *169      * rules:170      * baseUrl: http://gcfeng.github.io/blog/js171      * host: http://gcfeng.github.io/blog172      *173      * http://gcfeng.github.io/blog/js/test.js  -->  http://gcfeng.github.io/blog/js/test.js174      *                                    test  -->  http://gcfeng.github.io/blog/js/test.js175      *                              ../test.js  -->  http://gcfeng.github.io/blog/test.js176      *                                /test.js  -->  http://gcfeng.github.io/blog/test.js177      *                            test?foo#bar  -->  http://gcfeng.github.io/blog/test.js178      *179      * @param {String} id 模块id180      * @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#bar188         id = realpath(id);189 190         // main/test?foo#bar  -->  main/test191         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                 onl oad();246             };247             node.onerror = function() {248                 onl oad(true);249             }250         } else {251             node.onreadystatechange = function() {252                 if (/loaded|complete/.test(node.readyState)) {253                     onl oad();254                 }255             }256         }257 258         node.async = true;259         node.src =http://www.mamicode.com/ url;260 261         // 在IE6-8浏览器中,某些缓存会导致结点一旦插入就立即执行脚本262         currentlyAddingScript = node;263 264         // ref: #185 & http://dev.jquery.com/ticket/2709265         baseElement ? head.insertBefore(node, baseElement) : head.appendChild(node);266 267         currentlyAddingScript = null;268 269 270         function onl oad(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: 6298     };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.id310          this.factory311          */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         onl oad: 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).aspx563         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      * 生成一个唯一id578      */579     Module.guid = function() {580         return "seed_" + (+new Date()) + (Math.random() + ‘‘).slice( -8 );581     };582 583     /**584      * 获取一个模块,如果不存在则新建585      *586      * @param url587      * @param deps588      */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 模块id599      */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: factory649             };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);
View Code

变量 seed 保存加载过的模块和一些配置信息。对象 Module 用来描述一个模块,Module.STATUS 描述一个模块的状态信息,define.js 加载完毕之后调用 Module.init 来初始化baseUrl 和主模块。当主模块调用require方法后,程序就会去加载相关的依赖模块。

 

有一个需要注意的地方是 动态创建的script,在脚本加载完毕之后,会立即执行返回的代码。对于AMD模块,其加载完毕之后会执行define方法,如果该模块为匿名模块(没有指定id),我们需要在onload回调中来处理该模块。在开始加载模块的时候,我们不会知道其依赖和工厂方法等信息,需要在这个模块加载完毕执行define方法才能获得。

4. 参考

Requirejs

Seajs

 

本文同步自 gcfeng blog

 

JavaScript AMD 模块加载器原理与实现