AMD加载器实现笔记(一)中,我们实现了一个简易的模块加载器。但到目前为止这个加载器还并不能称为AMD加载器,原因很简单,我们还不支持AMD规范中的config配置。这篇文章中我们来添加对config的中baseUrl和packages的支持。API设计如下:
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 });
主要原则是将baseUrl和packages中location的路径转化为绝对路径。核心算法如下:
翻译成代码则为:
1 function getRoute(base, target) { 2 var bts = base.replace(//$/, "").split('/'); //base dir 3 var tts = target.split('/'); //target parts 4 while (isDefined(tts[0])) { 5 if (tts[0] === '.') { 6 return bts.join('/') + '/' + tts.slice(1).join('/'); 7 } else if (tts[0] === '..') { 8 bts.pop(); 9 tts.shift(); 10 } else { 11 return bts.join('/') + '/' + tts.join('/'); 12 } 13 } 14 };
剩下的处理就变得简单起来,首先得到baseUrl的绝对路径,然后根据baseUrl得到各个package中location的绝对路径。代码如下:
global.require.config = function(config) { this.parsedConfig = {}; if (config.baseUrl) { var currentUrl = getCurrentScript(); var parts = currentUrl.split('/'); parts.pop(); var currentDir = parts.join('/'); this.parsedConfig.baseUrl = getRoute(currentDir, config.baseUrl); } var burl = this.parsedConfig.baseUrl; // 得到baseUrl后,location相对baseUrl定位 this.parsedConfig.packages = []; if (config.packages) { for (var i = 0, len = config.packages.length; i < len; i++) { var pck = config.packages[i]; var cp = { name: pck.name, location: getRoute(burl, pck.location) } this.parsedConfig.packages.push(cp); } } console.log(this.parsedConfig); }
到了这里模块的依赖模块Id就不用再使用绝对路径了,可以按照正常AMD规范中的来了。如:
define(["aaa", "bbb", "ccc", "fff"],function(a,b,c,f){ $.log("已加载ddd模块", 7); return { bbb: b, ddd: "ddd", length: arguments.length } })
那么问题来了,这个时候module仓库的key该变成生么样子呢?继续保持原来的绝对路径形式,还是使用上文中的moduleId(aaa、bbb、ccc、fff)。答案是前者;使用后者的话,如果一个依赖是相对路径,比如:"./utils",可能会有多个模块都依赖这个id,但这些模块未必是需要同一个utils文件。所以我们程序中对require函数需要做一些修改,将deps中的moduleId转化为绝对路径。
1 // dep为非绝对路径形式,而modules的key仍然需要绝对路径 2 deps = deps.map(function(dep) { 3 var rel = ""; 4 if (/^Bodhi/.test(id)) { 5 rel = global.require.parsedConfig.baseUrl; 6 } else { 7 var parts = parent.split('/'); 8 parts.pop(); 9 rel = parts.join('/'); 10 } 11 return getModuleUrl(dep, rel); 12 });
getModuleUrl函数的处理方式为:
- 如果dep在某一package中,则将package的location作为参考目录
- 如果dep像相对路径,则将baseUrl作为参考目录
- 以上两种除外,则使用baseUrl来拼接路径
代码如下:
1 function getModuleUrl(moduleId, relative) { 2 function getPackage(nm) { 3 for (var i = 0, len = require.parsedConfig.packages.length; i < len; i++) { 4 var pck = require.parsedConfig.packages[i]; 5 if (nm === pck.name) { 6 return pck; 7 } 8 } 9 return false; 10 } 11 var mts = moduleId.split('/'); 12 var pck = getPackage(mts[0]); 13 if (pck) { 14 mts.shift(); 15 return getRoute(pck.location, mts.join('/')); 16 } else if (mts[0] === '.' || mts[0] === '..') { 17 return getRoute(relative, moduleId); 18 } else { 19 return getRoute(require.parsedConfig.baseUrl, moduleId); 20 } 21 }
到目前为止我们加载器已经支持了config中的baseUrl和packages,下篇文章我们让它来支持paths与shim。
加载器整体代码如下:
(function(global){ global.$ = { log: function(m) { console.log(m); } }; global = global || window; modules = {}; loadings = []; loadedJs = []; //module: id, state, factory, result, deps; global.require = function(deps, callback, parent){ var id = parent || "Bodhi" + Date.now(); var cn = 0, dn = deps.length; var args = []; // dep为非绝对路径形式,而modules的key仍然需要绝对路径 deps = deps.map(function(dep) { var rel = ""; if (/^Bodhi/.test(id)) { rel = global.require.parsedConfig.baseUrl; } else { var parts = parent.split('/'); parts.pop(); rel = parts.join('/'); } return getModuleUrl(dep, rel); }); var module = { id: id, deps: deps, factory: callback, state: 1, result: null }; modules[id] = module; deps.forEach(function(dep) { if (modules[dep] && modules[dep].state === 2) { cn++ args.push(modules[dep].result); } else if (!(modules[dep] && modules[dep].state === 1) && loadedJs.indexOf(dep) === -1) { loadJS(dep); loadedJs.push(dep); } }); if (cn === dn) { callFactory(module); } else { loadings.push(id); checkDeps(); } }; global.require.config = function(config) { this.parsedConfig = {}; if (config.baseUrl) { var currentUrl = getCurrentScript(); var parts = currentUrl.split('/'); parts.pop(); var currentDir = parts.join('/'); this.parsedConfig.baseUrl = getRoute(currentDir, config.baseUrl); } var burl = this.parsedConfig.baseUrl; // 得到baseUrl后,location相对baseUrl定位 this.parsedConfig.packages = []; if (config.packages) { for (var i = 0, len = config.packages.length; i < len; i++) { var pck = config.packages[i]; var cp = { name: pck.name, location: getRoute(burl, pck.location) } this.parsedConfig.packages.push(cp); } } console.log(this.parsedConfig); } global.define = function(deps, callback) { var id = getCurrentScript(); if (modules[id]) { console.error('multiple define module: ' + id); } require(deps, callback, id); }; function getRoute(base, target) { var bts = base.replace(//$/, "").split('/'); //base dir var tts = target.split('/'); //target parts while (isDefined(tts[0])) { if (tts[0] === '.') { return bts.join('/') + '/' + tts.slice(1).join('/'); } else if (tts[0] === '..') { bts.pop(); tts.shift(); } else { return bts.join('/') + '/' + tts.join('/'); } } }; function isDefined(v) { return v !== null && v !== undefined; } function getModuleUrl(moduleId, relative) { function getPackage(nm) { for (var i = 0, len = require.parsedConfig.packages.length; i < len; i++) { var pck = require.parsedConfig.packages[i]; if (nm === pck.name) { return pck; } } return false; } var mts = moduleId.split('/'); var pck = getPackage(mts[0]); if (pck) { mts.shift(); return getRoute(pck.location, mts.join('/')); } else if (mts[0] === '.' || mts[0] === '..') { return getRoute(relative, moduleId); } else { return getRoute(require.parsedConfig.baseUrl, moduleId); } } function loadJS(url) { var script = document.createElement('script'); script.type = "text/javascript"; //var url = getModuleUrl(mId, rel); script.src = 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); }; function checkDeps() { for (var p in modules) { var module = modules[p]; if (isReady(module) && loadings.indexOf(module.id) > -1) { callFactory(module); checkDeps(); // 如果成功,在执行一次,防止有些模块就差这次模块没有成功 } } }; function isReady(m) { var deps = m.deps; var allReady = deps.every(function(dep) { return modules[dep] && isReady(modules[dep]) && modules[dep].state === 2; }) if (deps.length === 0 || allReady) { return true; } }; function callFactory(m) { var args = []; for (var i = 0, len = m.deps.length; i < len; i++) { args.push(modules[m.deps[i]].result); } m.result = m.factory.apply(window, args); m.state = 2; var idx = loadings.indexOf(m.id); if (idx > -1) { loadings.splice(idx, 1); } }; function getCurrentScript(base) { // 参考 https://github.com/samyk/jiagra/blob/master/jiagra.js var stack; try { a.b.c(); //强制报错,以便捕获e.stack } catch (e) { //safari的错误对象只有line,sourceId,sourceURL stack = e.stack; if (!stack && window.opera) { //opera 9没有e.stack,但有e.Backtrace,但不能直接取得,需要对e对象转字符串进行抽取 stack = (String(e).match(/of linked script S+/g) || []).join(" "); } } if (stack) { /**e.stack最后一行在所有支持的浏览器大致如下: *chrome23: * at http://113.93.50.63/data.js:4:1 *firefox17: *@http://113.93.50.63/query.js:4 *opera12:http://www.oldapps.com/opera.php?system=Windows_XP *@http://113.93.50.63/data.js:4 *IE10: * at Global code (http://113.93.50.63/data.js:4:1) * //firefox4+ 可以用document.currentScript */ stack = stack.split(/[@ ]/g).pop(); //取得最后一行,最后一个空格或@之后的部分 stack = stack[0] === "(" ? stack.slice(1, -1) : stack.replace(/s/, ""); //去掉换行符 return stack.replace(/(:d+)?:d+$/i, "").replace(/.js$/, ""); //去掉行号与或许存在的出错字符起始位置 } var nodes = (base ? document : head).getElementsByTagName("script"); //只在head标签中寻找 for (var i = nodes.length, node; node = nodes[--i]; ) { if ((base || node.className === moduleClass) && node.readyState === "interactive") { return node.className = node.src; } } }; })(window)
测试:
require([ 'bbb', 'aaa.bbb.ccc', 'ccc', 'ddd', 'fff' ], function(aaabbbccc){ console.log('simple loader'); console.log(arguments); });
输出结果: