前几篇文章对AMD规范中的config属性几乎全部支持了,这一节主要是进一步完善。到目前为止我们的加载器还无法处理环形依赖的问题,这一节就是解决环形依赖。
所谓环形依赖,指的是模块A的所有依赖项的依赖中有没有依赖A模块本身的模块。如果有那就说明存在环形依赖。所以检验的方式是利用递归,检查一个模块的依赖的依赖项中有没有依赖A模块,以及依赖项的依赖项的依赖项中有没有A模块,核心代码如下:
function checkCircleRef(start, target){ var m = modules[start]; if (!m) { return false; } var depModules = m.deps.map(function(dep) { return modules[dep] || null; }); return depModules.some(function(m) {//检查依赖项的依赖项 if (!m) { return false; } return m.deps.some(function(dep) { var equal = dep === target; if (equal) { console.error("circle reference: ", target, m.id); } return equal; }); }) ? true : depModules.some(function(m) {//检查依赖项的依赖项的依赖项 if (!m) { return false; } return m.deps.some(function(dep) { return checkCircleRef(dep, target); }); }); };
剩下的问题是我们把检查放到哪里去?我们的模块最先在require中注册,所以最佳的位置是放在require函数中去检查:
//require 函数 //。。。。。。。 var module = { id: id, deps: deps, factory: callback, state: 1, result: null }; modules[id] = module; if (checkCircleRef(id, id)) { hasCircleReferece = true; return; } // ...................... //....................... //......................
下一个要面临的问题就是:如果存在依赖项如何处理?对此,我的做法是如果存在环形依赖,结束整个加载过程。我们在加载器内部使用一个哨兵变量,一旦存在环形依赖,停止所有工作。如:loadJs中:
script.onload = function() { if (hasCircleReferece) { return; } var module = modules[url]; if (module && isReady(module) && loadings.indexOf(url) > -1) { callFactory(module); } checkDeps(); };
如define函数中:
if (modules[id]) { console.error('multiple define module: ' + id); } if (!hasCircleReferece) { require(deps, callback, id); }
测试:
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); });
bbb中:
define(["aaa", "ccc"],function(a, c){ console.log("已加载bbb模块", 7) return { aaa: a, ccc: c.ccc, bbb: "bbb" } })
aaa中:
define(['bbb'], function(){ console.log("已加载aaa模块", 7) return "aaa" });
测试结果:
circle reference: simpleAMDLoader/aaa simpleAMDLoader/bbb
目前为止整个加载器代码如下:
1 (function(global){ 2 global = global || window; 3 var modules = {}; 4 var loadings = []; 5 var loadedJs = []; 6 var hasCircleReferece = false; 7 //module: id, state, factory, result, deps; 8 global.require = function(deps, callback, parent){ 9 var id = parent || "Bodhi" + Date.now(); 10 var cn = 0, dn = deps.length; 11 var args = []; 12 13 var oriDeps = deps.slice();//保留原始dep的模块Id 14 15 // dep为非绝对路径形式,而modules的key仍然需要绝对路径 16 deps = deps.map(function(dep) { 17 if (modules[dep]) { //jquery 18 return dep; 19 } else if (dep in global.require.parsedConfig.paths) { 20 return dep; 21 } 22 var rel = ""; 23 if (/^Bodhi/.test(id)) { 24 rel = global.require.parsedConfig.baseUrl; 25 } else { 26 var parts = parent.split('/'); 27 parts.pop(); 28 rel = parts.join('/'); 29 } 30 return getModuleUrl(dep, rel); 31 }); 32 33 var module = { 34 id: id, 35 deps: deps, 36 factory: callback, 37 state: 1, 38 result: null 39 }; 40 modules[id] = module; 41 42 if (checkCircleRef(id, id)) { 43 hasCircleReferece = true; 44 return; 45 } 46 //checkCircleRef(id, id) 47 48 deps.forEach(function(dep, i) { 49 if (modules[dep] && modules[dep].state === 2) { 50 cn++ 51 args.push(modules[dep].result); 52 } else if (!(modules[dep] && modules[dep].state === 1) && loadedJs.indexOf(dep) === -1) { 53 loadJS(dep, oriDeps[i]); 54 loadedJs.push(dep); 55 } 56 }); 57 if (cn === dn) { 58 callFactory(module); 59 } else { 60 loadings.push(id); 61 checkDeps(); 62 } 63 }; 64 65 global.require.config = function(config) { 66 this.parsedConfig = {}; 67 if (config.baseUrl) { 68 var currentUrl = getCurrentScript(); 69 var parts = currentUrl.split('/'); 70 parts.pop(); 71 var currentDir = parts.join('/'); 72 this.parsedConfig.baseUrl = getRoute(currentDir, config.baseUrl); 73 } 74 var burl = this.parsedConfig.baseUrl; 75 // 得到baseUrl后,location相对baseUrl定位 76 this.parsedConfig.packages = []; 77 if (config.packages) { 78 for (var i = 0, len = config.packages.length; i < len; i++) { 79 var pck = config.packages[i]; 80 var cp = { 81 name: pck.name, 82 location: getRoute(burl, pck.location) 83 } 84 this.parsedConfig.packages.push(cp); 85 } 86 } 87 88 89 this.parsedConfig.paths = {}; 90 if (config.paths) { 91 for (var p in config.paths) { 92 this.parsedConfig.paths[p] = /^http(s)?/.test(config.paths[p]) ? config.paths[p] : getRoute(burl, config.paths[p]); 93 } 94 } 95 96 this.parsedConfig.map = {}; 97 if (config.map) { 98 this.parsedConfig.map = config.map; 99 } 100 101 this.parsedConfig.shim = {}; 102 //shim 要放在最后处理 103 if (config.shim) { 104 this.parsedConfig.shim = config.shim; 105 for (var p in config.shim) { 106 var item = config.shim[p]; 107 define(p, item.deps, function() { 108 var exports; 109 if (item.init) { 110 exports = item.init.apply(item, arguments); 111 } 112 113 return exports ? exports : item.exports; 114 }); 115 } 116 } 117 118 console.log(this.parsedConfig); 119 } 120 121 global.define = function(id, deps, callback) { 122 //加上moduleId的支持 123 if (typeof id !== "string" && arguments.length === 2) { 124 callback = deps; 125 deps = id; 126 id = ""; 127 } 128 var id = id || getCurrentScript(); 129 130 var mId = getModuleId(id); 131 if (mId || id in require.parsedConfig.shim) { 132 mId = mId ? mId : id; 133 var maping = getMapSetting(mId); 134 135 if (maping) { 136 deps = deps.map(function(dep) { 137 return maping[dep] || dep; 138 }); 139 } 140 } 141 if (modules[id]) { 142 console.error('multiple define module: ' + id); 143 } 144 145 if (!hasCircleReferece) { 146 require(deps, callback, id); 147 } 148 }; 149 150 global.define.amd = {};//AMD规范 151 152 function getModuleId(url) { 153 var script = document.querySelector('script[src="' + url + '"]'); 154 if (script) { 155 return script.getAttribute('data-moduleId'); 156 } else { 157 return null; 158 } 159 }; 160 161 function getMapSetting(mId) { 162 if (mId in require.parsedConfig.map) { 163 return require.parsedConfig[mId]; 164 } else if ('*' in require.parsedConfig.map) { 165 return require.parsedConfig.map['*']; 166 } else { 167 return null; 168 } 169 }; 170 171 function checkCircleRef(start, target){ 172 var m = modules[start]; 173 if (!m) { 174 return false; 175 } 176 var depModules = m.deps.map(function(dep) { 177 return modules[dep] || null; 178 }); 179 180 181 return depModules.some(function(m) { 182 if (!m) { 183 return false; 184 } 185 return m.deps.some(function(dep) { 186 var equal = dep === target; 187 if (equal) { 188 console.error("circle reference: ", target, m.id); 189 } 190 191 return equal; 192 }); 193 }) ? true : depModules.some(function(m) { 194 if (!m) { 195 return false; 196 } 197 return m.deps.some(function(dep) { 198 return checkCircleRef(dep, target); 199 }); 200 }); 201 }; 202 203 function getRoute(base, target) { 204 var bts = base.replace(//$/, "").split('/'); //base dir 205 var tts = target.split('/'); //target parts 206 while (isDefined(tts[0])) { 207 if (tts[0] === '.') { 208 return bts.join('/') + '/' + tts.slice(1).join('/'); 209 } else if (tts[0] === '..') { 210 bts.pop(); 211 tts.shift(); 212 } else { 213 return bts.join('/') + '/' + tts.join('/'); 214 } 215 } 216 }; 217 218 function isDefined(v) { 219 return v !== null && v !== undefined; 220 }; 221 222 function getModuleUrl(moduleId, relative) { 223 function getPackage(nm) { 224 for (var i = 0, len = require.parsedConfig.packages.length; i < len; i++) { 225 var pck = require.parsedConfig.packages[i]; 226 if (nm === pck.name) { 227 return pck; 228 } 229 } 230 return false; 231 } 232 var mts = moduleId.split('/'); 233 var pck = getPackage(mts[0]); 234 if (pck) { 235 mts.shift(); 236 return getRoute(pck.location, mts.join('/')); 237 } else if (mts[0] === '.' || mts[0] === '..') { 238 return getRoute(relative, moduleId); 239 } else { 240 return getRoute(require.parsedConfig.baseUrl, moduleId); 241 } 242 }; 243 244 function loadJS(url, mId) { 245 var script = document.createElement('script'); 246 script.setAttribute('data-moduleId', mId); //为script元素保留原始模块Id 247 script.type = "text/javascript"; 248 //判断模块是否在paths中定义了路径 249 script.src = (url in global.require.parsedConfig.paths ? global.require.parsedConfig.paths[url] : url) + '.js'; 250 script.onload = function() { 251 if (hasCircleReferece) { 252 return; 253 } 254 var module = modules[url]; 255 if (module && isReady(module) && loadings.indexOf(url) > -1) { 256 callFactory(module); 257 } 258 checkDeps(); 259 }; 260 var head = document.getElementsByTagName('head')[0]; 261 head.appendChild(script); 262 }; 263 264 function checkDeps() { 265 for (var p in modules) { 266 var module = modules[p]; 267 if (isReady(module) && loadings.indexOf(module.id) > -1) { 268 callFactory(module); 269 checkDeps(); // 如果成功,在执行一次,防止有些模块就差这次模块没有成功 270 } 271 } 272 }; 273 274 function isReady(m) { 275 var deps = m.deps; 276 var allReady = deps.every(function(dep) { 277 return modules[dep] && isReady(modules[dep]) && modules[dep].state === 2; 278 }) 279 if (deps.length === 0 || allReady) { 280 return true; 281 } 282 }; 283 284 function callFactory(m) { 285 var args = []; 286 for (var i = 0, len = m.deps.length; i < len; i++) { 287 args.push(modules[m.deps[i]].result); 288 } 289 m.result = m.factory.apply(window, args); 290 m.state = 2; 291 292 var idx = loadings.indexOf(m.id); 293 if (idx > -1) { 294 loadings.splice(idx, 1); 295 } 296 }; 297 298 function getCurrentScript(base) { 299 // 参考 https://github.com/samyk/jiagra/blob/master/jiagra.js 300 var stack; 301 try { 302 a.b.c(); //强制报错,以便捕获e.stack 303 } catch (e) { //safari的错误对象只有line,sourceId,sourceURL 304 stack = e.stack; 305 if (!stack && window.opera) { 306 //opera 9没有e.stack,但有e.Backtrace,但不能直接取得,需要对e对象转字符串进行抽取 307 stack = (String(e).match(/of linked script S+/g) || []).join(" "); 308 } 309 } 310 if (stack) { 311 /**e.stack最后一行在所有支持的浏览器大致如下: 312 *chrome23: 313 * at http://113.93.50.63/data.js:4:1 314 *firefox17: 315 *@http://113.93.50.63/query.js:4 316 *opera12:http://www.oldapps.com/opera.php?system=Windows_XP 317 *@http://113.93.50.63/data.js:4 318 *IE10: 319 * at Global code (http://113.93.50.63/data.js:4:1) 320 * //firefox4+ 可以用document.currentScript 321 */ 322 stack = stack.split(/[@ ]/g).pop(); //取得最后一行,最后一个空格或@之后的部分 323 stack = stack[0] === "(" ? stack.slice(1, -1) : stack.replace(/s/, ""); //去掉换行符 324 return stack.replace(/(:d+)?:d+$/i, "").replace(/.js$/, ""); //去掉行号与或许存在的出错字符起始位置 325 } 326 var nodes = (base ? document : head).getElementsByTagName("script"); //只在head标签中寻找 327 for (var i = nodes.length, node; node = nodes[--i]; ) { 328 if ((base || node.className === moduleClass) && node.readyState === "interactive") { 329 return node.className = node.src; 330 } 331 } 332 }; 333 })(window)