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);
View Code

變量 seed 保存加載過的模塊和一些配置信息。對象 Module 用來描述一個模塊,Module.STATUS 描述一個模塊的狀態信息,define.js 加載完畢之后調用 Module.init 來初始化baseUrl 和主模塊。當主模塊調用require方法后,程序就會去加載相關的依賴模塊。

 

有一個需要注意的地方是 動態創建的script,在腳本加載完畢之后,會立即執行返回的代碼。對於AMD模塊,其加載完畢之后會執行define方法,如果該模塊為匿名模塊(沒有指定id),我們需要在onload回調中來處理該模塊。在開始加載模塊的時候,我們不會知道其依賴和工廠方法等信息,需要在這個模塊加載完畢執行define方法才能獲得。

4. 參考

Requirejs

Seajs

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM