继续这一系列的内容,到目前为止除了AMD规范中config的map、config参数外,我们已经全部支持其他属性了。这一篇文章中,我们来为增加对map的支持。同样问题,想要增加map的支持首先要知道map的语义。
主要用于解决在两个不同模块集中使用一个模块的不同版本,并且保证两个模块集的交互没有冲突。
假设磁盘有如下文件:
当‘some/newmodule‘请求‘foo‘模块时,它将从foo1.2.js总得到‘foo1.2‘模块;当‘some/oldmodule‘请求‘foo‘模块时它将从foo1.0中得到‘foo1.0‘模块。
在map属性中可以使用任何的module ID前缀,并且mapping对象可以匹配任何别的module ID前缀。
如果出现通配符‘*’,表示任何模块使用这个匹配配置。通配符匹配对象中的模块ID前缀可以被覆盖。
通过上文的解释,可以明白,如果在‘some/newmodule‘中依赖的foo实际是上依赖的foo1.2。转化成代码逻辑应当是这样的:如果在‘some/module’模块中发现依赖foo模块那就将foo替换成foo1.2。但是在什么地方实现替换好呢?因为模块的定义从define开始,同时只有在define中才能获得模块的绝对路径,所以我们把替换的处理放在define中。那么问题来了,我们的模块大部分都是匿名模块,模块自己如何知道自己的模块Id?所以一定要有一个标记去告诉define函数当前模块的Id,我们知道每一个模块都是一个JavaScript文件,每一个模块都有一个对应的script元素,所以最好的做法是没每一个script都加一个自定义特性,来标记当前元素的模块Id。
所以在loadJs中要为script加自定义特性:
function loadJS(url, mId) { var script = document.createElement(‘script‘); script.setAttribute(‘data-moduleId‘, mId); //为script元素保留原始模块Id script.type = "text/javascript"; //判断模块是否在paths中定义了路径 script.src = (url in global.require.parsedConfig.paths ? global.require.parsedConfig.paths[url] : url) + ‘.js‘; script.onload = function() { var module = modules[url]; if (module && isReady(module) && loadings.indexOf(url) > -1) { callFactory(module); } checkDeps(); }; var head = document.getElementsByTagName(‘head‘)[0]; head.appendChild(script); };
在define函数中,通过文件的绝对路径,找出对应的script元素,拿到模块Id,判断如果在map中,则进行替换:
global.define = function(id, deps, callback) { //加上moduleId的支持 if (typeof id !== "string" && arguments.length === 2) { callback = deps; deps = id; id = ""; } var id = id || getCurrentScript(); var script = document.querySelector(‘script[src="‘ + id + ‘"]‘); if (script || id in require.parsedConfig.shim) { var mId = script ? script.getAttribute(‘data-moduleId‘) : id; var maping = getMapSetting(mId); if (maping) { deps = deps.map(function(dep) { return maping[dep] || dep; }); } } if (modules[id]) { console.error(‘multiple define module: ‘ + id); } require(deps, callback, id); };
function getMapSetting(mId) { if (mId in require.parsedConfig.map) { return require.parsedConfig[mId]; } else if (‘*‘ in require.parsedConfig.map) { return require.parsedConfig.map[‘*‘]; } else { return null; } };
目前为止,我们的加载器已经支持了map属性,完整代码如下:
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 var oriDeps = deps.slice();//保留原始dep的模块Id 13 14 // dep为非绝对路径形式,而modules的key仍然需要绝对路径 15 deps = deps.map(function(dep) { 16 if (modules[dep]) { //jquery 17 return dep; 18 } else if (dep in global.require.parsedConfig.paths) { 19 return dep; 20 } 21 var rel = ""; 22 if (/^Bodhi/.test(id)) { 23 rel = global.require.parsedConfig.baseUrl; 24 } else { 25 var parts = parent.split(‘/‘); 26 parts.pop(); 27 rel = parts.join(‘/‘); 28 } 29 return getModuleUrl(dep, rel); 30 }); 31 32 var module = { 33 id: id, 34 deps: deps, 35 factory: callback, 36 state: 1, 37 result: null 38 }; 39 modules[id] = module; 40 41 if (checkCircleRef(id, id)) { 42 return; 43 } 44 45 deps.forEach(function(dep, i) { 46 if (modules[dep] && modules[dep].state === 2) { 47 cn++ 48 args.push(modules[dep].result); 49 } else if (!(modules[dep] && modules[dep].state === 1) && loadedJs.indexOf(dep) === -1) { 50 loadJS(dep, oriDeps[i]); 51 loadedJs.push(dep); 52 } 53 }); 54 if (cn === dn) { 55 callFactory(module); 56 } else { 57 loadings.push(id); 58 checkDeps(); 59 } 60 }; 61 62 global.require.config = function(config) { 63 this.parsedConfig = {}; 64 if (config.baseUrl) { 65 var currentUrl = getCurrentScript(); 66 var parts = currentUrl.split(‘/‘); 67 parts.pop(); 68 var currentDir = parts.join(‘/‘); 69 this.parsedConfig.baseUrl = getRoute(currentDir, config.baseUrl); 70 } 71 var burl = this.parsedConfig.baseUrl; 72 // 得到baseUrl后,location相对baseUrl定位 73 this.parsedConfig.packages = []; 74 if (config.packages) { 75 for (var i = 0, len = config.packages.length; i < len; i++) { 76 var pck = config.packages[i]; 77 var cp = { 78 name: pck.name, 79 location: getRoute(burl, pck.location) 80 } 81 this.parsedConfig.packages.push(cp); 82 } 83 } 84 85 86 this.parsedConfig.paths = {}; 87 if (config.paths) { 88 for (var p in config.paths) { 89 this.parsedConfig.paths[p] = /^http(s)?/.test(config.paths[p]) ? config.paths[p] : getRoute(burl, config.paths[p]); 90 } 91 } 92 93 this.parsedConfig.map = {}; 94 if (config.map) { 95 this.parsedConfig.map = config.map; 96 } 97 98 this.parsedConfig.shim = {}; 99 //shim 要放在最后处理 100 if (config.shim) { 101 this.parsedConfig.shim = config.shim; 102 for (var p in config.shim) { 103 var item = config.shim[p]; 104 define(p, item.deps, function() { 105 var exports; 106 if (item.init) { 107 exports = item.init.apply(item, arguments); 108 } 109 110 return exports ? exports : item.exports; 111 }); 112 } 113 } 114 115 console.log(this.parsedConfig); 116 } 117 118 global.define = function(id, deps, callback) { 119 //加上moduleId的支持 120 if (typeof id !== "string" && arguments.length === 2) { 121 callback = deps; 122 deps = id; 123 id = ""; 124 } 125 var id = id || getCurrentScript(); 126 127 var script = document.querySelector(‘script[src="‘ + id + ‘"]‘); 128 if (script || id in require.parsedConfig.shim) { 129 var mId = script ? script.getAttribute(‘data-moduleId‘) : id; 130 var maping = getMapSetting(mId); 131 132 if (maping) { 133 deps = deps.map(function(dep) { 134 return maping[dep] || dep; 135 }); 136 } 137 } 138 if (modules[id]) { 139 console.error(‘multiple define module: ‘ + id); 140 } 141 142 require(deps, callback, id); 143 }; 144 145 global.define.amd = {};//AMD规范 146 147 function getMapSetting(mId) { 148 if (mId in require.parsedConfig.map) { 149 return require.parsedConfig[mId]; 150 } else if (‘*‘ in require.parsedConfig.map) { 151 return require.parsedConfig.map[‘*‘]; 152 } else { 153 return null; 154 } 155 }; 156 157 function checkCircleRef(start, target){ 158 var m = modules[start]; 159 if (!m) { 160 return false; 161 } 162 var depModules = m.deps.map(function(dep) { 163 return modules[dep] || null; 164 }); 165 166 167 return depModules.some(function(m) { 168 if (!m) { 169 return false; 170 } 171 return m.deps.some(function(dep) { 172 var equal = dep === target; 173 if (equal) { 174 console.error("circle reference: ", target, m.id); 175 } 176 177 return equal; 178 }); 179 }) ? true : depModules.some(function(m) { 180 if (!m) { 181 return false; 182 } 183 return m.deps.some(function(dep) { 184 return checkCircleRef(dep, target); 185 }); 186 }); 187 188 //return hasCr ? true: 189 }; 190 191 function getRoute(base, target) { 192 var bts = base.replace(/\/$/, "").split(‘/‘); //base dir 193 var tts = target.split(‘/‘); //target parts 194 while (isDefined(tts[0])) { 195 if (tts[0] === ‘.‘) { 196 return bts.join(‘/‘) + ‘/‘ + tts.slice(1).join(‘/‘); 197 } else if (tts[0] === ‘..‘) { 198 bts.pop(); 199 tts.shift(); 200 } else { 201 return bts.join(‘/‘) + ‘/‘ + tts.join(‘/‘); 202 } 203 } 204 }; 205 206 function isDefined(v) { 207 return v !== null && v !== undefined; 208 }; 209 210 function getModuleUrl(moduleId, relative) { 211 function getPackage(nm) { 212 for (var i = 0, len = require.parsedConfig.packages.length; i < len; i++) { 213 var pck = require.parsedConfig.packages[i]; 214 if (nm === pck.name) { 215 return pck; 216 } 217 } 218 return false; 219 } 220 var mts = moduleId.split(‘/‘); 221 var pck = getPackage(mts[0]); 222 if (pck) { 223 mts.shift(); 224 return getRoute(pck.location, mts.join(‘/‘)); 225 } else if (mts[0] === ‘.‘ || mts[0] === ‘..‘) { 226 return getRoute(relative, moduleId); 227 } else { 228 return getRoute(require.parsedConfig.baseUrl, moduleId); 229 } 230 }; 231 232 function loadJS(url, mId) { 233 var script = document.createElement(‘script‘); 234 script.setAttribute(‘data-moduleId‘, mId); //为script元素保留原始模块Id 235 script.type = "text/javascript"; 236 //判断模块是否在paths中定义了路径 237 script.src = (url in global.require.parsedConfig.paths ? global.require.parsedConfig.paths[url] : url) + ‘.js‘; 238 script.onload = function() { 239 var module = modules[url]; 240 if (module && isReady(module) && loadings.indexOf(url) > -1) { 241 callFactory(module); 242 } 243 checkDeps(); 244 }; 245 var head = document.getElementsByTagName(‘head‘)[0]; 246 head.appendChild(script); 247 }; 248 249 function checkDeps() { 250 for (var p in modules) { 251 var module = modules[p]; 252 if (isReady(module) && loadings.indexOf(module.id) > -1) { 253 callFactory(module); 254 checkDeps(); // 如果成功,在执行一次,防止有些模块就差这次模块没有成功 255 } 256 } 257 }; 258 259 function isReady(m) { 260 var deps = m.deps; 261 var allReady = deps.every(function(dep) { 262 return modules[dep] && isReady(modules[dep]) && modules[dep].state === 2; 263 }) 264 if (deps.length === 0 || allReady) { 265 return true; 266 } 267 }; 268 269 function callFactory(m) { 270 var args = []; 271 for (var i = 0, len = m.deps.length; i < len; i++) { 272 args.push(modules[m.deps[i]].result); 273 } 274 m.result = m.factory.apply(window, args); 275 m.state = 2; 276 277 var idx = loadings.indexOf(m.id); 278 if (idx > -1) { 279 loadings.splice(idx, 1); 280 } 281 }; 282 283 function getCurrentScript(base) { 284 // 参考 https://github.com/samyk/jiagra/blob/master/jiagra.js 285 var stack; 286 try { 287 a.b.c(); //强制报错,以便捕获e.stack 288 } catch (e) { //safari的错误对象只有line,sourceId,sourceURL 289 stack = e.stack; 290 if (!stack && window.opera) { 291 //opera 9没有e.stack,但有e.Backtrace,但不能直接取得,需要对e对象转字符串进行抽取 292 stack = (String(e).match(/of linked script \S+/g) || []).join(" "); 293 } 294 } 295 if (stack) { 296 /**e.stack最后一行在所有支持的浏览器大致如下: 297 *chrome23: 298 * at http://113.93.50.63/data.js:4:1 299 *firefox17: 300 *@http://113.93.50.63/query.js:4 301 *opera12:http://www.oldapps.com/opera.php?system=Windows_XP 302 *@http://113.93.50.63/data.js:4 303 *IE10: 304 * at Global code (http://113.93.50.63/data.js:4:1) 305 * //firefox4+ 可以用document.currentScript 306 */ 307 stack = stack.split(/[@ ]/g).pop(); //取得最后一行,最后一个空格或@之后的部分 308 stack = stack[0] === "(" ? stack.slice(1, -1) : stack.replace(/\s/, ""); //去掉换行符 309 return stack.replace(/(:\d+)?:\d+$/i, "").replace(/\.js$/, ""); //去掉行号与或许存在的出错字符起始位置 310 } 311 var nodes = (base ? document : head).getElementsByTagName("script"); //只在head标签中寻找 312 for (var i = nodes.length, node; node = nodes[--i]; ) { 313 if ((base || node.className === moduleClass) && node.readyState === "interactive") { 314 return node.className = node.src; 315 } 316 } 317 }; 318 })(window)
下面我们看一个demo:
使用我们的加载器来加载jquery,同时禁用jquery的全局模式$:
window.something = "Bodhi"; require.config({ baseUrl: "./", packages: [{ name: "more", location: "./more" }, { name: "mass", location: "../" }, { name: "wab", location: "../../../" }], shim: { "something": { "deps": [‘jquery‘], exports: ‘something‘, init: function(jq, ol) { console.log(jq); console.log($); return something + " in shim"; } } }, map: { ‘*‘: { ‘jquery‘: ‘jquery-private‘ }, ‘jquery-private‘: { ‘jquery‘: ‘jquery‘ } }, paths: { ‘jquery‘: "../../Bodhi/src/roots/jquery" } }); require([ ‘bbb‘, //‘aaa.bbb.ccc‘, //‘ccc‘, //‘ddd‘, //‘fff‘, ‘something‘ ], function(aaabbbccc){ console.log(‘simple loader‘); console.log(arguments); });
jquery-private代码如下:
define([‘jquery‘], function(jquery) { return jquery.noConflict(true); });
如果某一模块依赖jquery,那么将会加载jquery-private。而在jquery中,因为配置了map和paths,所以jquery-private中的jquery根据paths找到jquery文件,并加载。同时在jquery-private中将禁用了全局模式之后的jquery对象返回给something模块。通过这个配置所有的模块在引用jquery时,实际上是引用了jquery-private模块。