在上一篇文章中侄泽,我們已經(jīng)基本完成了模塊加載器的基本功能,接下來來完成一下路徑解析的問題正勒。
在之前的功能中得院,我們所有的模塊默認(rèn)只能放在同級(jí)目錄下,而在實(shí)際項(xiàng)目中章贞,我們的js很有可能位于多個(gè)目錄祥绞,甚至是CDN中,所以現(xiàn)在這種路徑解析是非常不合理的鸭限,因此我們需要將每個(gè)模塊的name轉(zhuǎn)化為一個(gè)絕對(duì)路徑蜕径,這樣才是一個(gè)比較完美的解決方案。
借鑒部分requirejs的思想里覆,我們可以通過配置來配置一個(gè)baseUrl丧荐,當(dāng)沒有配置這個(gè)baseUrl的時(shí)候,我們認(rèn)為這個(gè)baseUrl就是html頁(yè)面的地址喧枷,所以我們需要對(duì)外暴露一個(gè)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;
上面的代碼中,我們定義了一個(gè)基本的全局配置對(duì)象cfg隧甚、一個(gè)用來合并對(duì)象屬性的merge方法和一個(gè)用來支持配置的config方法车荔。但是顯然這個(gè)時(shí)候配置baseUrl的時(shí)候需要使用一個(gè)絕對(duì)路徑。但是在實(shí)際中我們可能更會(huì)使用的是一個(gè)相對(duì)路徑戚扳,例如../或者./或者/這個(gè)需求是非常正常的忧便,因此我們需要也支持這些實(shí)現(xiàn)。首先我們先來寫這些的匹配的正則表達(dá)式帽借,為了之后的使用我們同時(shí)也寫出檢測(cè)完整路徑(包括http珠增、https和file協(xié)議)
var fullPathRegExp = /^[(https?\:\/\/) | (file\:\/\/\/)]/;
var absoPathRegExp = /^\//;
var relaPathRegExp = /^\.\//;
var relaPathBackRegExp = /^\.\.\//;
同時(shí)將這些判斷寫進(jìn)一個(gè)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;
}
}
這里可能需要關(guān)注的一個(gè)相對(duì)路徑的問題砍艾,因?yàn)橛锌赡苁切枰祷厣弦患?jí)目錄的蒂教,即形如./../../的形式,因此也應(yīng)該處理這種情況脆荷。另外之所以在這里都是要匹配baseUrl的最后一個(gè)斜杠/凝垛,是因?yàn)樘峁┑倪@個(gè)很有可能帶有斜杠,也很有可能不帶斜杠蜓谋。
最后使用config方法配置的時(shí)候梦皮,通過判斷提供的path來做相應(yīng)的處理,修改config方法如下:
function config(obj) {
if(obj){
if(obj.baseUrl) {
obj.baseUrl = outputPath(cfg.baseUrl, obj.baseUrl);
}
merge(cfg, obj);
}
}
最后我們修改一下每個(gè)模塊名為這個(gè)模塊的絕對(duì)路徑桃焕,這樣我們就不必再修改loadScript方法了剑肯,我們?cè)趌oadMod方法中修改name參數(shù),增加代碼:
name = outputPath(cfg.baseUrl, name);
我們?cè)賮韮?yōu)化一下观堂,畢竟如果我們每一個(gè)模塊都要使用./或者../之類的退子,很多模塊下這是要崩潰的岖妄,所以我們依舊是借鑒requirejs的方法,允許使用config方法來配置path屬性這個(gè)問題寂祥,當(dāng)我們配置了一個(gè)app的path之后我們認(rèn)為在模塊引用的時(shí)候荐虐,如果遇到app開頭則需要替換這個(gè)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方法中同時(shí)也應(yīng)該檢測(cè)cfg.path中是否含有這個(gè)屬性丸凭,這時(shí)候會(huì)比較復(fù)雜福扬,因此單獨(dú)抽出為一個(gè)函數(shù)來說是比較好的處理方式,單獨(dú)抽出為一個(gè)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方法中調(diào)用這個(gè)方法就可以了。
我們?cè)賰?yōu)化一下虽界,我們完全可以在define中將name替換為一個(gè)絕對(duì)路徑汽烦,同時(shí)在主模塊加載依賴的時(shí)候,將依賴替換為絕對(duì)路徑即可莉御,因此我們可以在定義模塊的時(shí)候就將這個(gè)這個(gè)路徑替換好撇吞。
不過這個(gè)時(shí)候我們需要明白的是,在定義模塊的時(shí)候是一個(gè)類似單詞礁叔,而聲明依賴的時(shí)候則有可能含有路徑牍颈,如何在模塊聲明的時(shí)候正確解析路徑呢?
很明顯我們可以使用一個(gè)變量來做這個(gè)事情琅关,這個(gè)變量存儲(chǔ)著所有模塊名和依賴這個(gè)模塊時(shí)的聲明煮岁。那么我們就應(yīng)該在use方法加載模塊的時(shí)候?qū)⑦@些變量名添加到這個(gè)變量名之下,之后再define中進(jìn)行轉(zhuǎn)化涣易,那么最后我們的整個(gè)代碼如下:
(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);
我們進(jìn)行一下測(cè)試:
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;
}
}
});
打開瀏覽器我們可以看到正常輸出画机,如下:
說明我們的所做的路徑解析工作是正確的。
系列文章:
動(dòng)手實(shí)現(xiàn)一個(gè)AMD模塊加載器(一)
動(dòng)手實(shí)現(xiàn)一個(gè)AMD模塊加載器(二)
動(dòng)手實(shí)現(xiàn)一個(gè)AMD模塊加載器(三)
最后是一個(gè)廣告貼新症,最近新開了一個(gè)分享技術(shù)的公眾號(hào)步氏,歡迎大家關(guān)注??