上一篇文章中我们为config添加了baseUrl和packages的支持,那么这篇文章中将会看到对shim与paths的支持。
要添加shim与paths,第一要务当然是了解他们的语义与用法。先来看shim,shim翻译成中文是“垫片”的意思。在AMD中主要用途是把不支持AMD的某些变量包装AMD模块。shim是一个哈希对象,key为包装后的模块Id,value是关于这个包装模块的一些配置,主要配置项如下:
举个例子:
这个配置的目的是想将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 });