AMD加载器实现笔记(三)

时间:2021-12-31 13:42:56

  上一篇文章中我们为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加载器实现笔记(三)

  这个配置的目的是想将window.some.thing这个全局变量包装成id为‘some/thing’的模块。模块的依赖项Id为'a'、'b',输出值为some.thing但因为定义了init函数,所以最后模块的输出值变成了some.thing + 'another'。

  因为shim是要将全局变量包装成模块,所以直接用shim中的配置项来定义模块即可。上例中的配置最终将转化为:

 define('some/thing', ['a', 'b'], function(a, b) {
var initResult = shimItem.init(a, b);
return initResult === undefined ? shimItem.exports : initResult;
});

  但前面我的加载器中,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都转化为绝对路径了。

 // dep为非绝对路径形式,而modules的key仍然需要绝对路径
deps = deps.map(function(dep) {
if (modules[dep]) { //jquery
return dep;
} else if (dep in global.require.parsedConfig.paths) {
return 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);
});

 到此,shim已经完美支持了。

  下面轮到了paths,paths也是一个对象,格式为{模块Id:模块所在路径}。当其他模块引用该模块时,该模块的加载地址使用paths中所配置的地址,这个地址可以是绝对的,也可以是相对路径(相对于baseUrl)。首先要在require.config函数中解析path路径

 this.parsedConfig.paths = {};
if (config.paths) {
for (var p in config.paths) {
this.parsedConfig.paths[p] = /^http(s)?/.test(config.paths[p]) ? config.paths[p] : getRoute(burl, config.paths[p]);
}
}

  然后当依赖模块Id在paths中有配置时,那就需要从paths中拿到加载地址,所以需要修改loadJs中代码

 function loadJS(url) {
var script = document.createElement('script');
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);
};

  同时如果dep在paths中有配置,也不能将dep转化为绝对路径(require函数)

  这里面需要注意的是,模块的Id必须与路径所在文件中定义的模块的id保持一致。以jquery为例:

 require.config({
baseUrl: "./",
packages: [{
name: "more",
location: "./more"
}, {
name: "mass",
location: "../"
}, {
name: "wab",
location: "../../../"
}],
paths: {
'jquery': "../../Bodhi/src/roots/jquery"
}
});

  当在paths中配置了jquery模块时,那么路径对应的文件一定要定义jquery模块,如jquery源码中用define定义jquery模块:

 if ( typeof define === "function" && define.amd ) {
define( "jquery", [], function() {
return jQuery;
});
}

  

  本文内容结束,目前为止loader加载器的源码为:

 (function(global){
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) {
if (modules[dep]) { //jquery
return dep;
} else if (dep in global.require.parsedConfig.paths) {
return 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);
}
} this.parsedConfig.paths = {};
if (config.paths) {
for (var p in config.paths) {
this.parsedConfig.paths[p] = /^http(s)?/.test(config.paths[p]) ? config.paths[p] : getRoute(burl, config.paths[p]);
}
}
//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;
});
}
} console.log(this.parsedConfig);
} 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);
}; global.define.amd = {};//AMD规范 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";
//判断模块是否在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);
}; 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)

  

  测试demo:

 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);
return something + " in shim";
}
}
},
paths: {
'jquery': "../../Bodhi/src/roots/jquery"
}
});
require([
'bbb',
'aaa.bbb.ccc',
'ccc',
'ddd',
'fff',
'something'
], function(aaabbbccc){
console.log('simple loader');
console.log(arguments);
});