轉載nodejs學習之實現簡易路由
此前實現了個數據轉發功能,但是要建本地服務器,還需要一個簡易的路由功能。因為只是用於本地服務器用於自己測試用,所以不需要太完善的路由功能,所以也就不去使用express框架,而是自己實現一個簡易路由,可以針對自己的需求來定制路由功能。
在制作路由功能之前,我先寫了一張路由表,表明了自己大概想要實現的四種路由轉換效果,這四種效果也正是自己項目需要的:
{
"/my/**/*":"func:testFun",
"index":"url:index.html",
"test?v=*":"url:my*.html",
"/public/bi*/**/*":"url:public/**/*"
}
第一種:只要我的地址是/my/**/*的格式,**/*意思就是my目錄下任意目錄目錄的任意文件都會觸發testFun這個方法。比如/my/test/index.html或者/my/1/2/3/index.html都會觸發testFun,因為會觸發這個方法,所以路由不會進行頁面輸出。
第二種:就是常規的,當我訪問/index時,將index.html頁面輸出。
第三種:如果我輸入為test?v=index,輸出的頁面則為myindex.html,兩邊的*即數值相同。
第四種:用於靜態資源的獲取,當我訪問/public/bi*/**/*時,就會將public下的任意文件輸出。比如我的請求路徑為/public/biz009/stylesheets/css/main.css,那么路由轉換出的文件路徑即為:public/stylesheets/css/main.css
抱着實現這四種效果的目的,就開始了自己的實現。
第一段代碼,mimes里的內容比較長,所以就用...代替了。
首先把正則寫好,正則主要用於替換**和*,替換成相應的正則字符串。
然后實現Router的構造函數,對傳入的參數進行簡易處理,傳入的參數可以直接為上面的對象,也可以為json文件的路徑,構造函數中會用eval轉換成對象,之所以不用JSON.parse是因為其對json格式的要求比較嚴,不方便書寫。
后面再繼承一下事件類,方便外部調用事件綁定。
"use strict";
var fs = require("fs");
var url = require("url");
var events = require("events");
var util = require("util");
var path = require("path");
var mimes = '...'.split(",");
var ALL_FOLDER_REG = /\/\*\*\//g;
var ALL_FOLDER_REG_STR = '/([\\w._-]*\/)*'; //匹配XXX/XXX/XX/
var ALL_FILES_REG = /\*+/g;
var ALL_FILES_REG_STR = '[\\w._-]+'; //匹配XX
var noop = function () {};
var Router = function (arg) {
this.methods = {};
if ((typeof arg == "object") && !(arg instanceof Array)) {
this.maps = arg;
} else if (typeof arg == "string") {
try {
var json = fs.readFileSync(arg).toString();
this.maps = eval('(' + json + ')');
} catch (e) {
console.log(e);
this.maps = {};
}
} else {
this.maps = {};
}
this.handleMaps();
};
//繼承事件類
util.inherits(Router, events.EventEmitter);
var rp = Router.prototype;
rp.constructor = Router;
上面代碼中再構造函數里還執行了一個handleMaps方法,該方法是用於將路由表中的路由地址和目標地址進行處理后,再放到數組里保存起來。__A__代表**,__B__代表*,這兩個也對應了上面寫的正則字符串:ALL_FOLDER_REG_STR 和 ALL_FILES_REG_STR
rp.handleMaps = function () {
this.filters = []; //存放路由地址
this.address = []; //存放目標地址
for (var k in this.maps) {
var fil = trim(k);
var ad = trim(this.maps[k]);
fil = fil.charAt(0) == "/" ? fil : ("/" + fil);
ad = ad.replace(ALL_FOLDER_REG, '__A__').replace(ALL_FILES_REG, '__B__');
fil = fil.replace(/\?/g , "\\?").replace(ALL_FOLDER_REG, '__A__').replace(ALL_FILES_REG, '__B__');
this.filters.push(fil);
this.address.push(ad);
}
};
然后還要實現一個保存function的方法,因為要根據路由表執行方法,所以有了set方法:
rp.set = function (name, func) {
if (!name)return;
this.methods[name] = (func instanceof Function) ? func : noop;
};
前面的都實現好后,就要實現具體的路由方法,這段代碼相對比較簡單,當發生請求時,跟據請求地址,遍歷上面保存的路由地址,並將路由地址中的__A__和__B__轉成相應正則字符串,再通過RegExp實現正則實例,對請求地址進行匹配。如果匹配成功,當前索引 i 即為目標地址中的索引。
然后對字符串進行分割,判斷如果是url則進行相應的url處理,如果是function則執行保存的方法,並且傳入req,res。
rp.route = function (req, res) {
var urlobj = url.parse(req.url);
var pathname = urlobj.pathname;
var i = 0;
var match = false;
var fil;
for (; i < this.filters.length; i++) {
fil = this.filters[i];
var reg = new RegExp("^" + fil.replace(/__A__/g, ALL_FOLDER_REG_STR).replace(/__B__/g, ALL_FILES_REG_STR) + "$");
if (reg.test(fil.indexOf("?") >= 0 ? (pathname = urlobj.path) : pathname)) {
match = true;
break;
}
}
if (match) {
var ad = this.address[i];
var array = ad.split(':' , 2);
if(array[0] === "url"){
//如果是url則查找相應url的文件
var filepath = getpath(fil , array[1] , pathname);
this.emit("match", filepath , pathname);
this.routeTo(res , filepath);
}else if(array[0] === "func" && (array[1] in this.methods)){
//如果是func則執行保存在methods里的方法
this.methods[array[1]].call(this , req , res , pathname);
}else {
throw new Error("route Error");
}
}else {
this.emit("notmatch");
this.error(res);
}
};
上面代碼中有個getpath方法,該方法就是將**和*映射為實際地址,也即使將/public/biz009/stylesheets/css/main.css 轉換為public/stylesheets/css/main.css 的邏輯。
function getpath(fil , ad , pathname){
var filepath = ad;
if(/__(A|B)__/g.test(fil) && /__(A|B)__/g.test(ad)){
var ay = fil.split("__");
var dy = ad.split("__");
var index = 0;
for(var k=0;k<ay.length;k++){
if(!ay[k]) continue;
var reg;
if (ay[k] === 'A' || ay[k] === 'B') {
reg = new RegExp(ay[k] === 'A' ? ALL_FOLDER_REG_STR : ALL_FILES_REG_STR);
//掃描路徑,當遇到AB關鍵字時處理,如果兩者不相等,停下dy的掃描,繼續執行對ay的掃描,直至遇到相等數值
while(index < dy.length){
if(dy[index] === 'A' || dy[index] === 'B'){
if(dy[index] === ay[k]){
dy[index] = pathname.match(reg)[0];
index++;
}
break;
}
index++;
}
} else {
reg = new RegExp(ay[k]);
}
pathname = pathname.replace(reg, '');
}
filepath = dy.join("");
}
filepath = path.normalize(filepath);
filepath = filepath.charAt(0) == path.sep ? filepath.substring(1,filepath.length):filepath;
return filepath;
}
說說實現原理:先將路由地址和目標地址轉成數組
/public/bi*/**/* ==> ['/public/bi','B','','A','','B'] public/**/* ==> ['public','A','','B']
而當我請求/public/biz009/stylesheets/css/main.css 的時候,即要將
['/public/bi','B','','A','','B'] ==> ['/public/bi','z009','','/stylesheets/css/','','main.css']
然后再跟上面的['public','A','','B']對應,即
['public','A','','B'] ==> ['public','/stylesheets/css/','','main.css']
實現邏輯為:
請求的pathname還是為/public/biz009/stylesheets/css/main.css ,掃描['/public/bi','B','','A','','B']:
掃描第一個即'/public/bi'時,將/public/bi轉成正則,通過匹配將/public/biz009/stylesheets/css/main.css 變為:z009/stylesheets/css/main.css
掃描第二個即B,因為B所以用上面的ALL_FILES_REG_STR 即 [\w._-]+匹配,將從而獲取到了B對應的z009,同時將pathname變成/stylesheets/css/main.css,此時再掃描 ['public','A','','B'],掃描到A或B的時候,發現是A而不是對應的B,因此不更新掃描索引,所以上面沒有進行index++,而是直接break,繼續下一步。
掃描第三個''所以不管繼續掃描
掃描第四個為A,同上,獲取到/stylesheets/css/,並將pathname變成main.css,此時再掃描['public','A','','B'],掃描索引還停留在A上,所以再進行判斷,結果兩者都是A,因此將['public','A','','B']中的A替換成了/stylesheets/css/,即變成了['public','/stylesheets/css/','','B']
然后同上繼續掃描直至掃描完,就會將['public','A','','B']變成['public','/stylesheets/css/','','main.css'];
最后再join出來的結果:public/stylesheets/css/main.css就是轉換出來的最終路徑,也就是匹配的文件路徑。
還有兩個方法一個是輸出文件內容,一個就是404了,比較簡單,就不作贅述
rp.routeTo = function(res , filepath){
var that = this;
fs.stat(filepath , function(err , stats){
if(err || !stats.isFile()){
that.emit("error" , err || (new Error("path is not file")));
that.error(res);
return;
}
var fileKind = filepath.substring((filepath.lastIndexOf(".")+1)||0 , filepath.length);
var readstream = fs.createReadStream(filepath);
var index = mimes.indexOf('.'+fileKind);
var options = {
'Cache-Control':'no-cache',
'Content-Type': mimes[index+1]+';charset=utf-8',
'Content-Length':stats.size
};
res.writeHead(200, options);
readstream.pipe(res);
})
}
rp.error = function(res){
res.writeHead(404);
res.end("404 not found");
}
該源碼放在github上
https://github.com/whxaxes/easy-router/blob/master/index.js

