动手实现一个 AMD 模块加载器(二)
在上一篇文章中,我们已经基本完成了模块加载器的基本功能,接下来来完成一下路径解析的问题。
在之前的功能中,我们所有的模块默认只能放在同级目录下,而在实际项目中,我们的js很有可能位于多个目录,甚至是 CDN 中,所以现在这种路径解析是非常不合理的,因此我们需要将每个模块的 name 转化为一个绝对路径,这样才是一个比较完美的解决方案。
借鉴部分 requirejs 的思想,我们可以通过配置来配置一个 baseUrl,当没有配置这个 baseUrl 的时候,我们认为这个 baseUrl 就是 html 页面的地址,所以我们需要对外暴露一个 config 方法,如下:
var cfg = { baseUrl: location.href.replace(/(/)[^/]+$/g, function(s, s1){ return s1 }) } function config(obj) { obj && merge(cfg, obj); } function merge(obj1, obj2) { if(obj1 && obj2) { for(var key in obj2) { obj1[key] = obj2[key] } } } loadjs.config = config;
上面的代码中,我们定义了一个基本的全局配置对象 cfg、一个用来合并对象属性的 merge 方法和一个用来支持配置的 config 方法。但是显然这个时候配置 baseUrl 的时候需要使用一个绝对路径。但是在实际中我们可能更会使用的是一个相对路径,例如../或者./或者/这个需求是非常正常的,因此我们需要也支持这些实现。首先我们先来写这些的匹配的正则表达式,为了之后的使用我们同时也写出检测完整路径(包括 http、https 和 file 协议)
var fullPathRegExp = /^[(https?://) | (file:///)]/; var absoPathRegExp = /^//; var relaPathRegExp = /^.//; var relaPathBackRegExp = /^..//;
同时将这些判断写进一个 outputPath 方法中。
function outputPath(baseUrl, path) { if (relaPathRegExp.test(path)) { if(/..//g.test(path)) { var pathArr = baseUrl.split('/'); var backPath = path.match(/..//g); var joinPath = path.replace(/[(^./)|(../)]+/g, ''); var num = pathArr.length - backPath.length; return pathArr.splice(0, num).join('/').replace(//$/g, '') + '/' +joinPath; } else { return baseUrl.replace(//$/g, '') + '/' + path.replace(/[(^./)]+/g, ''); } } else if (fullPathRegExp.test(path)) { return path; } else if (absoPathRegExp.test(path)) { return baseUrl.replace(//$/g, '') + path; } else { return baseUrl.replace(//$/g, '') + '/' + path; } }
这里可能需要关注的一个相对路径的问题,因为有可能是需要返回上一级目录的,即形如 ./../../ 的形式,因此也应该处理这种情况。另外之所以在这里都是要匹配 baseUrl 的最后一个斜杠 /,是因为提供的这个很有可能带有斜杠,也很有可能不带斜杠。
最后使用 config 方法配置的时候,通过判断提供的 path 来做相应的处理,修改 config 方法如下:
function config(obj) { if(obj){ if(obj.baseUrl) { obj.baseUrl = outputPath(cfg.baseUrl, obj.baseUrl); } merge(cfg, obj); } }
最后我们修改一下每个模块名为这个模块的绝对路径,这样我们就不必再修改 loadScript 方法了,我们在 loadMod 方法中修改 name 参数,增加代码:
name = outputPath(cfg.baseUrl, name);
我们再来优化一下,毕竟如果我们每一个模块都要使用 ./ 或者 ../ 之类的,很多模块下这是要崩溃的,所以我们依旧是借鉴 requirejs 的方法,允许使用 config 方法来配置 path 属性这个问题,当我们配置了一个 app 的 path 之后我们认为在模块引用的时候,如果遇到 app 开头则需要替换这个 path。
所以先来看 config 方法修改如下:
function config(obj) { if(obj){ if(obj.baseUrl) { obj.baseUrl = outputPath(cfg.baseUrl, obj.baseUrl); } if(obj.path) { var base = obj.baseUrl || cfg.baseUrl; for(var key in obj.path) { obj.path[key] = outputPath(base, obj.path[key]); } } merge(cfg, obj); } }
因此在 loadMod 方法中同时也应该检测 cfg.path 中是否含有这个属性,这时候会比较复杂,因此单独抽出为一个函数来说是比较好的处理方式,单独抽出为一个 replaceName 方法,如下:
function replaceName(name) { if(fullPathRegExp.test(name) || absoPathRegExp.test(name) || relaPathRegExp.test(name)) { return outputPath(cfg.baseUrl, name); } else { var prefix = name.split('/')[0] || name; if(cfg.path[prefix]) { if(name.split('/').length === 0) { return cfg.path[prefix]; } else { var endPath = name.split('/').slice(1).join('/'); return outputPath(cfg.path[prefix], endPath); } } } }
这样,我们只需要在 loadMod 方法中调用这个方法就可以了。
我们再优化一下,我们完全可以在 define 中将 name 替换为一个绝对路径,同时在主模块加载依赖的时候,将依赖替换为绝对路径即可,因此我们可以在定义模块的时候就将这个这个路径替换好。
不过这个时候我们需要明白的是,在定义模块的时候是一个类似单词,而声明依赖的时候则有可能含有路径,如何在模块声明的时候正确解析路径呢?
很明显我们可以使用一个变量来做这个事情,这个变量存储着所有模块名和依赖这个模块时的声明。那么我们就应该在 use 方法加载模块的时候将这些变量名添加到这个变量名之下,之后再 define 中进行转化,那么最后我们的整个代码如下:
(function(root){
var modMap = {};
var moduleMap = {};
var cfg = {
baseUrl: location.href.replace(/(/)[^/]+$/g, function(s, s1){
return s1
}),
path: {
}
};
var fullPathRegExp = /^[(https?://) | (file:///)]/;
var absoPathRegExp = /^//;
var relaPathRegExp = /^.//;
var relaPathBackRegExp = /^..//;
function outputPath(baseUrl, path) {
if (relaPathRegExp.test(path)) {
if(/..//g.test(path)) {
var pathArr = baseUrl.split('/');
var backPath = path.match(/..//g);
var joinPath = path.replace(/[(^./)|(../)]+/g, '');
var num = pathArr.length - backPath.length;
return pathArr.splice(0, num).join('/').replace(//$/g, '') + '/' +joinPath;
} else {
return baseUrl.replace(//$/g, '') + '/' + path.replace(/[(^./)]+/g, '');
}
} else if (fullPathRegExp.test(path)) {
return path;
} else if (absoPathRegExp.test(path)) {
return baseUrl.replace(//$/g, '') + path;
} else {
return baseUrl.replace(//$/g, '') + '/' + path;
}
}
function replaceName(name) {
if(fullPathRegExp.test(name) || absoPathRegExp.test(name) || relaPathRegExp.test(name) || relaPathBackRegExp.test(name)) {
return outputPath(cfg.baseUrl, name);
} else {
var prefix = name.split('/')[0] || name;
if(cfg.paths[prefix]) {
if(name.split('/').length === 0) {
return cfg.paths[prefix];
} else {;
var endPath = name.split('/').slice(1).join('/');
return outputPath(cfg.paths[prefix], endPath);
}
} else {
return outputPath(cfg.baseUrl, name);
}
}
}
function fixUrl(name) {
return name.split('/')[name.split('/').length-1]
}
function config(obj) {
if(obj){
if(obj.baseUrl) {
obj.baseUrl = outputPath(cfg.baseUrl, obj.baseUrl);
}
if(obj.paths) {
var base = obj.baseUrl || cfg.baseUrl;
for(var key in obj.paths) {
obj.paths[key] = outputPath(base, obj.paths[key]);
}
}
merge(cfg, obj);
}
}
function merge(obj1, obj2) {
if(obj1 && obj2) {
for(var key in obj2) {
obj1[key] = obj2[key]
}
}
}
function use(deps, callback) {
if(deps.length === 0) {
callback();
}
var depsLength = deps.length;
var params = [];
for(var i = 0; i < deps.length; i++) {
moduleMap[fixUrl(deps[i])] = deps[i];
deps[i] = replaceName(deps[i]);
(function(j){
loadMod(deps[j], function(param) {
depsLength--;
params[j] = param;
if(depsLength === 0) {
callback.apply(null, params);
}
})
})(i)
}
}
function loadMod(name, callback) {
if(!modMap[name]) {
modMap[name] = {
status: 'loading',
oncomplete: []
};
loadscript(name, function() {
use(modMap[name].deps, function() {
execMod(name, callback, Array.prototype.slice.call(arguments, 0));
})
});
} else if(modMap[name].status === 'loading') {
modMap[name].oncomplete.push(callback);
} else if (!modMap[name].exports){
use(modMap[name].deps, function() {
execMod(name, callback, Array.prototype.slice.call(arguments, 0));
})
}else {
callback(modMap[name].exports);
}
}
function execMod(name, callback, params) {
var exp = modMap[name].callback.apply(null, params);
modMap[name].exports = exp;
callback(exp);
execComplete(name);
}
function execComplete(name) {
for(var i = 0; i < modMap[name].oncomplete.length; i++) {
modMap[name].oncomplete[i](modMap[name].exports);
}
}
function loadscript(name, callback) {
var doc = document;
var node = doc.createElement('script');
node.charset = 'utf-8';
node.src = name + '.js';
node.id = 'loadjs-js-' + (Math.random() * 100).toFixed(3);
doc.body.appendChild(node);
node.onload = function() {
callback();
}
}
function define(name, deps, callback) {
if(moduleMap[name]) {
name=moduleMap[name]
}
name = replaceName(name);
deps = deps.map(function(ele, i) {
return replaceName(ele);
});
modMap[name] = modMap[name] || {};
modMap[name].deps = deps;
modMap[name].status = 'loaded';
modMap[name].callback = callback;
modMap[name].oncomplete = modMap[name].oncomplete || [];
}
var loadjs = {
define: define,
use: use,
config: config
};
root.define = define;
root.loadjs = loadjs;
root.modMap = modMap;
})(window);
我们进行一下测试:
loadjs.config({
baseUrl:'./static',
paths: {
app: './app'
}
});
loadjs.use(['app/b', 'a'], function(b) {
console.log('main');
console.log(b.equil(1,2));
})
define('a', ['app/c'], function(c) {
console.log('a');
console.log(c.sqrt(4));
return {
add: function(a, b) {
return a + b;
}
}
});
define('c', ['http://ce.sysu.edu.cn/hope/Skin/js/jquery.min.js'], function() {
console.log('c');
return {
sqrt: function(a) {
return Math.sqrt(a)
}
}
});
define('b', ['c'], function(c) {
console.log('b');
console.log(c.sqrt(9));
return {
equil: function(a,b) {
return a===b;
}
}
});
打开浏览器我们可以看到正常输出,如下:
说明我们的所做的路径解析工作是正确的。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
上一篇: 动手实现一个 AMD 模块加载器(三)
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论