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作为模块的返回值。

  举个例子:

  AMD加载器实现笔记(三)_第1张图片

  这个配置的目的是想将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   });

 

你可能感兴趣的:(AMD加载器实现笔记(三))