AMD加載器實現筆記(三)


  上一篇文章中我們為config添加了baseUrl和packages的支持,那么這篇文章中將會看到對shim與paths的支持。

  要添加shim與paths,第一要務當然是了解他們的語義與用法。先來看shim,shim翻譯成中文是“墊片”的意思。在AMD中主要用途是把不支持AMD的某些變量包裝AMD模塊。shim是一個哈希對象,key為包裝后的模塊Id,value是關於這個包裝模塊的一些配置,主要配置項如下:

  • deps:定義模塊需要的依賴項的moduleId數組
  • exports:模塊輸出值
  • init:如果它的返回值不是undefined,則返回值作為‘some/thind’模塊的返回值,否則以exports作為模塊的返回值。

  舉個例子:

  

  這個配置的目的是想將window.some.thing這個全局變量包裝成id為‘some/thing’的模塊。模塊的依賴項Id為'a'、'b',輸出值為some.thing但因為定義了init函數,所以最后模塊的輸出值變成了some.thing + 'another'。

  因為shim是要將全局變量包裝成模塊,所以直接用shim中的配置項來定義模塊即可。上例中的配置最終將轉化為:

1 define('some/thing', ['a', 'b'], function(a, b) {
2    var initResult = shimItem.init(a, b);
3    return initResult === undefined ? shimItem.exports : initResult;  
4 });

  但前面我的加載器中,define只支持匿名模塊,現在我們讓它來支持顯示定義模塊Id:

global.define = function(id, deps, callback) {
        //加上moduleId的支持
        if (typeof id !== "string" && arguments.length === 2) {
            callback = deps;
            deps = id;
            id = "";
        }
        var id = id || getCurrentScript();
        if (modules[id]) {
            console.error('multiple define module: ' + id);
        }
        
        require(deps, callback, id);
    };

  完成后我們在require.config函數中加入對shim的支持。

//shim 要放在最后處理
        if (config.shim) {
            for (var p in config.shim) {
                var item = config.shim[p];
                define(p, item.deps, function() {
                    var exports;
                    if (item.init) {
                        exports = item.init.apply(item, arguments);
                    }
                    
                    return exports ? exports : item.exports;
                });
            }
        }

  於此同時,require中也要改一下,現在已經不能一股腦的將dep都轉化為絕對路徑了。

 1 // dep為非絕對路徑形式,而modules的key仍然需要絕對路徑
 2         deps = deps.map(function(dep) {
 3             if (modules[dep]) { //jquery 
 4                 return dep;
 5             } else if (dep in global.require.parsedConfig.paths) {
 6                 return dep;
 7             }
 8             var rel = "";
 9             if (/^Bodhi/.test(id)) {
10                 rel = global.require.parsedConfig.baseUrl;
11             } else {
12                 var parts = parent.split('/');
13                 parts.pop();
14                 rel = parts.join('/');
15             }
16             return getModuleUrl(dep, rel);
17         });

  到此,shim已經完美支持了。

 

  下面輪到了paths,paths也是一個對象,格式為{模塊Id:模塊所在路徑}。當其他模塊引用該模塊時,該模塊的加載地址使用paths中所配置的地址,這個地址可以是絕對的,也可以是相對路徑(相對於baseUrl)。首先要在require.config函數中解析path路徑

1 this.parsedConfig.paths = {};
2         if (config.paths) {
3             for (var p in config.paths) {
4                 this.parsedConfig.paths[p] = /^http(s)?/.test(config.paths[p]) ? config.paths[p] : getRoute(burl, config.paths[p]);
5             }
6         }

  然后當依賴模塊Id在paths中有配置時,那就需要從paths中拿到加載地址,所以需要修改loadJs中代碼

 1 function loadJS(url) {
 2         var script = document.createElement('script');
 3         script.type = "text/javascript";
 4         //判斷模塊是否在paths中定義了路徑
 5         script.src = (url in global.require.parsedConfig.paths ? global.require.parsedConfig.paths[url] : url) + '.js';
 6         script.onload = function() {
 7             var module = modules[url];
 8             if (module && isReady(module) && loadings.indexOf(url) > -1) {
 9                 callFactory(module);
10             }
11             checkDeps();
12         };
13         var head = document.getElementsByTagName('head')[0];
14         head.appendChild(script);
15     };

  同時如果dep在paths中有配置,也不能將dep轉化為絕對路徑(require函數)

 

  這里面需要注意的是,模塊的Id必須與路徑所在文件中定義的模塊的id保持一致。以jquery為例:

 1 require.config({
 2     baseUrl: "./",
 3     packages: [{
 4         name: "more",
 5         location: "./more"
 6     }, {
 7         name: "mass",
 8         location: "../"
 9     }, {
10         name: "wab",
11         location: "../../../"
12     }],
13     paths: {
14         'jquery': "../../Bodhi/src/roots/jquery"
15     }
16   });

  當在paths中配置了jquery模塊時,那么路徑對應的文件一定要定義jquery模塊,如jquery源碼中用define定義jquery模塊:

1 if ( typeof define === "function" && define.amd ) {
2     define( "jquery", [], function() {
3         return jQuery;
4     });
5 }

  

  本文內容結束,目前為止loader加載器的源碼為:

  1 (function(global){
  2     global = global || window;
  3     modules = {};
  4     loadings = [];
  5     loadedJs = [];
  6     //module: id, state, factory, result, deps;
  7     global.require = function(deps, callback, parent){
  8         var id = parent || "Bodhi" + Date.now();
  9         var cn = 0, dn = deps.length;
 10         var args = [];
 11         
 12          // dep為非絕對路徑形式,而modules的key仍然需要絕對路徑
 13         deps = deps.map(function(dep) {
 14             if (modules[dep]) { //jquery 
 15                 return dep;
 16             } else if (dep in global.require.parsedConfig.paths) {
 17                 return dep;
 18             }
 19             var rel = "";
 20             if (/^Bodhi/.test(id)) {
 21                 rel = global.require.parsedConfig.baseUrl;
 22             } else {
 23                 var parts = parent.split('/');
 24                 parts.pop();
 25                 rel = parts.join('/');
 26             }
 27             return getModuleUrl(dep, rel);
 28         });
 29         
 30         var module = {
 31             id: id,
 32             deps: deps,
 33             factory: callback,
 34             state: 1,
 35             result: null
 36         };
 37         modules[id] = module;
 38         
 39         deps.forEach(function(dep) {
 40             if (modules[dep] && modules[dep].state === 2) {
 41                 cn++
 42                 args.push(modules[dep].result);
 43             } else if (!(modules[dep] && modules[dep].state === 1) && loadedJs.indexOf(dep) === -1) {
 44                 loadJS(dep);
 45                 loadedJs.push(dep);
 46             }
 47         });
 48         if (cn === dn) {
 49             callFactory(module);
 50         } else {
 51             loadings.push(id);
 52             checkDeps();
 53         }
 54     };
 55     
 56     global.require.config = function(config) {
 57         this.parsedConfig = {};
 58         if (config.baseUrl) {
 59             var currentUrl = getCurrentScript();
 60             var parts = currentUrl.split('/');
 61             parts.pop();
 62             var currentDir = parts.join('/');
 63             this.parsedConfig.baseUrl = getRoute(currentDir, config.baseUrl);
 64         }
 65         var burl = this.parsedConfig.baseUrl;
 66         // 得到baseUrl后,location相對baseUrl定位
 67         this.parsedConfig.packages = [];
 68         if (config.packages) {
 69             for (var i = 0, len = config.packages.length; i < len; i++) {
 70                 var pck = config.packages[i];
 71                 var cp = {
 72                     name: pck.name,
 73                     location: getRoute(burl, pck.location)
 74                 }
 75                 this.parsedConfig.packages.push(cp);
 76             }
 77         }
 78         
 79         
 80         this.parsedConfig.paths = {};
 81         if (config.paths) {
 82             for (var p in config.paths) {
 83                 this.parsedConfig.paths[p] = /^http(s)?/.test(config.paths[p]) ? config.paths[p] : getRoute(burl, config.paths[p]);
 84             }
 85         }
 86         //shim 要放在最后處理
 87         if (config.shim) {
 88             for (var p in config.shim) {
 89                 var item = config.shim[p];
 90                 define(p, item.deps, function() {
 91                     var exports;
 92                     if (item.init) {
 93                         exports = item.init.apply(item, arguments);
 94                     }
 95                     
 96                     return exports ? exports : item.exports;
 97                 });
 98             }
 99         }
100         
101         console.log(this.parsedConfig);
102     }
103     
104     global.define = function(id, deps, callback) {
105         //加上moduleId的支持
106         if (typeof id !== "string" && arguments.length === 2) {
107             callback = deps;
108             deps = id;
109             id = "";
110         }
111         var id = id || getCurrentScript();
112         if (modules[id]) {
113             console.error('multiple define module: ' + id);
114         }
115         
116         require(deps, callback, id);
117     };
118     
119     global.define.amd = {};//AMD規范
120     
121     function getRoute(base, target) {
122         var bts = base.replace(/\/$/, "").split('/');  //base dir
123         var tts = target.split('/'); //target parts
124         while (isDefined(tts[0])) {
125             if (tts[0] === '.') {
126                 return bts.join('/') + '/' + tts.slice(1).join('/');
127             } else if (tts[0] === '..') {
128                 bts.pop();
129                 tts.shift();
130             } else {
131                 return bts.join('/') + '/' + tts.join('/');
132             }
133         }
134     };
135     
136     function isDefined(v) {
137         return v !== null && v !== undefined;
138     }
139     
140     function getModuleUrl(moduleId, relative) {
141         function getPackage(nm) {
142             for (var i = 0, len = require.parsedConfig.packages.length; i < len; i++) {
143                 var pck = require.parsedConfig.packages[i];
144                 if (nm === pck.name) {
145                     return pck;
146                 }
147             }
148             return false;
149         }
150         var mts = moduleId.split('/');
151         var pck = getPackage(mts[0]);
152         if (pck) {
153             mts.shift();
154             return getRoute(pck.location, mts.join('/'));
155         } else if (mts[0] === '.' || mts[0] === '..') {
156             return getRoute(relative, moduleId);
157         } else {
158             return getRoute(require.parsedConfig.baseUrl, moduleId);
159         }
160     }
161     
162     function loadJS(url) {
163         var script = document.createElement('script');
164         script.type = "text/javascript";
165         //判斷模塊是否在paths中定義了路徑
166         script.src = (url in global.require.parsedConfig.paths ? global.require.parsedConfig.paths[url] : url) + '.js';
167         script.onload = function() {
168             var module = modules[url];
169             if (module && isReady(module) && loadings.indexOf(url) > -1) {
170                 callFactory(module);
171             }
172             checkDeps();
173         };
174         var head = document.getElementsByTagName('head')[0];
175         head.appendChild(script);
176     };
177     
178     function checkDeps() {
179         for (var p in modules) {
180             var module = modules[p];
181             if (isReady(module) && loadings.indexOf(module.id) > -1) {
182                 callFactory(module);
183                 checkDeps(); // 如果成功,在執行一次,防止有些模塊就差這次模塊沒有成功
184             }
185         }
186     };
187     
188     function isReady(m) {
189         var deps = m.deps;
190         var allReady = deps.every(function(dep) {
191             return modules[dep] && isReady(modules[dep]) && modules[dep].state === 2;
192         })
193         if (deps.length === 0 || allReady) {
194             return true;
195         }
196     };
197     
198     function callFactory(m) {
199         var args = [];
200         for (var i = 0, len = m.deps.length; i < len; i++) {
201             args.push(modules[m.deps[i]].result);
202         }
203         m.result = m.factory.apply(window, args);
204         m.state = 2;
205         
206         var idx = loadings.indexOf(m.id);
207         if (idx > -1) {
208             loadings.splice(idx, 1);
209         }
210     };
211     
212     function getCurrentScript(base) {
213         // 參考 https://github.com/samyk/jiagra/blob/master/jiagra.js
214         var stack;
215         try {
216             a.b.c(); //強制報錯,以便捕獲e.stack
217         } catch (e) { //safari的錯誤對象只有line,sourceId,sourceURL
218             stack = e.stack;
219             if (!stack && window.opera) {
220                 //opera 9沒有e.stack,但有e.Backtrace,但不能直接取得,需要對e對象轉字符串進行抽取
221                 stack = (String(e).match(/of linked script \S+/g) || []).join(" ");
222             }
223         }
224         if (stack) {
225             /**e.stack最后一行在所有支持的瀏覽器大致如下:
226              *chrome23:
227              * at http://113.93.50.63/data.js:4:1
228              *firefox17:
229              *@http://113.93.50.63/query.js:4
230              *opera12:http://www.oldapps.com/opera.php?system=Windows_XP
231              *@http://113.93.50.63/data.js:4
232              *IE10:
233              *  at Global code (http://113.93.50.63/data.js:4:1)
234              *  //firefox4+ 可以用document.currentScript
235              */
236             stack = stack.split(/[@ ]/g).pop(); //取得最后一行,最后一個空格或@之后的部分
237             stack = stack[0] === "(" ? stack.slice(1, -1) : stack.replace(/\s/, ""); //去掉換行符
238             return stack.replace(/(:\d+)?:\d+$/i, "").replace(/\.js$/, ""); //去掉行號與或許存在的出錯字符起始位置
239         }
240         var nodes = (base ? document : head).getElementsByTagName("script"); //只在head標簽中尋找
241         for (var i = nodes.length, node; node = nodes[--i]; ) {
242             if ((base || node.className === moduleClass) && node.readyState === "interactive") {
243                 return node.className = node.src;
244             }
245         }
246     };
247 })(window)

  

  測試demo:

 1 window.something = "Bodhi";
 2   require.config({
 3     baseUrl: "./",
 4     packages: [{
 5         name: "more",
 6         location: "./more"
 7     }, {
 8         name: "mass",
 9         location: "../"
10     }, {
11         name: "wab",
12         location: "../../../"
13     }],
14     shim: {
15         "something": {
16             "deps": ['jquery'],
17             exports: 'something',
18             init: function(jq, ol) {
19                 console.log(jq);
20                 return something + " in shim";
21             }
22         }
23     },
24     paths: {
25         'jquery': "../../Bodhi/src/roots/jquery"
26     }
27   });
28   require([
29   'bbb',
30   'aaa.bbb.ccc',
31   'ccc',
32   'ddd',
33   'fff',
34   'something'
35   ], function(aaabbbccc){
36     console.log('simple loader');
37     console.log(arguments);
38   });

 


免責聲明!

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



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