nodejs封装一个类似express路由
路由
如前面文章所述,路由是前端浏览器访问路径,后端程序解析访问路径并返回其对应的页面,这一实现过程称为路由。express实现的路由如下:
app.get("index",function(req,res){
res.send("hello express")
})
假设运行在本地,以上程序实现访问 http://127.0.0.1/index
时,后端nodejs执行对应的function
也就是返回helloworld
.
这种方式定义和解析路由非常方便且清晰明了。
与thinkphp的对比
之前开发网站使用的thinkphp框架是MVC架构,如果使用默认配置时,路由是根据 模块 -> 控制器 -> 下属文件
来确定访问路径与处理函数的关系的,比如有文件admin->user->list存在,那么访问
admin/user/list`tp框架就会运行list.php文件中的函数来执行处理逻辑并返回执行结果。
在thinkPHP中如果想要自定义路径实现访问http://127.0.0.1/abc
来执行admin->user->list
,需要修改rewrite配置,而nodejs的express框架默认就采用自定义的配置,也就是可以直接指定http://127.0.0.1/abc
的处理函数,如上例所示。
实现
这篇文章的目的是我们要用nodejs来自己实现封装一个类似exprss的路由。
自定义路由文件主要用到了nodejs的内置模块:url、fs、http和模板引擎ejs模块。
路由文件单独封装为一个文件:express_router.js,app一个文件:app.js。
思路
express_router主要有两个功能,第一是注册路由处理函数,第二是解析并执行浏览器请求。
注册路由处理函数是利用全局变量来实现的,这里利用到了js的闭包实现:
var router = function (req, res) {
var self = this;//保存this,用来实现闭包后的访问
this._get = [];
this._post = [];
var handle = function (req, res) {
......
}
......
return handle()
}
_get与_post
是两个数组变量,用于保存get请求方法和post请求方法的路由与处理函数键值对,如本文第一个程序片段中的index
与对应的function
。
handle是返回给app的实例,也就是用来处理请求的地方。
有了存储空间,那么必须有对应的操作方法,以get为例如下,post类似。
handle.get = function (str, callback) {
if (!str.startsWith("/")) {
str = "/" + str;
}
if (!str.endsWith("/")) {
str = str + "/";
}
self._get[str] = callback;
}
路由的解析有handle函数内部来实现,
var handle = function (req, res) {
rewriteRes(res);//为什么修改形参却会修改了实参???
var reqUrl = url.parse(req.url, true);
var pathname = reqUrl.pathname;
if (!pathname.startsWith("/")) {
pathname = "/" + pathname;
}
if (!pathname.endsWith("/")) {
pathname = pathname + "/";
}
console.log("请求路径是:", pathname);
var method = req.method.toLowerCase();
console.log("请求方法是:", method);
console.log("_post", self._post);
if (self["_" + method][pathname]) {
var query = req.query;
if (method == "get") {
req.query = query;
console.log("请求参数是:", req.query);
self["_" + method][pathname](req, res);
} else if (method == "post") {
var chunk = "";
req.on("data", function (data) {
chunk += data;
})
req.on("end", function () {
// console.log("chunk is:", chunk)
try {
let queryData = {}
// chunk is: username=asfas&password=12134
let chunkArr = chunk.split("&");
// console.log("chunkArr", chunkArr);
chunkArr.forEach((item) => {
let key = item.split("=")[0];
let val = item.split("=")[1];
queryData[key] = val;
})
// console.log("queryData", queryData);
req.query = queryData;
//如果要实现程序健壮,这里需要怎么处理。
console.log("请求参数是:", req.query);
self["_" + method][pathname](req, res);
} catch{
console.log("post传参解析错误")
}
})
}
} else {
console.log("访问的pathname不存在:", pathname);
res.send("404");
}
首先拿到请求的pathname
和请求方法,然后判断对应请求方法数组中是否有以pathname
为键值的元素,如果有,那么进行下一步查询参数的解析,get和post的参数解析方法不同,get直接可以按到,post是通过流数据来拿到的。无论通过哪种方式,最终执行开始注册的callback函数来执行处理并返回给浏览器数据。这就是简易的express路由实现。
完整程序
express_route.js
var url = require("url");
//res的rewrite
var rewriteRes = function (res) {
res.send = function (data) {
res.writeHead(200, { "Content-Type": "text/html;charset='utf-8'" });
res.end(data);
}
}
var router = function (req, res) {
var self = this;//保存this,用来实现闭包后的访问
this._get = [];
this._post = [];
var handle = function (req, res) {
rewriteRes(res);//为什么修改形参却会修改了实参???
var reqUrl = url.parse(req.url, true);
var pathname = reqUrl.pathname;
if (!pathname.startsWith("/")) {
pathname = "/" + pathname;
}
if (!pathname.endsWith("/")) {
pathname = pathname + "/";
}
console.log("请求路径是:", pathname);
var method = req.method.toLowerCase();
console.log("请求方法是:", method);
console.log("_post", self._post);
if (self["_" + method][pathname]) {
var query = req.query;
if (method == "get") {
req.query = query;
console.log("请求参数是:", req.query);
self["_" + method][pathname](req, res);
} else if (method == "post") {
var chunk = "";
req.on("data", function (data) {
chunk += data;
})
req.on("end", function () {
// console.log("chunk is:", chunk)
try {
let queryData = {}
// chunk is: username=asfas&password=12134
let chunkArr = chunk.split("&");
// console.log("chunkArr", chunkArr);
chunkArr.forEach((item) => {
let key = item.split("=")[0];
let val = item.split("=")[1];
queryData[key] = val;
})
// console.log("queryData", queryData);
req.query = queryData;
//如果要实现程序健壮,这里需要怎么处理。
console.log("请求参数是:", req.query);
self["_" + method][pathname](req, res);
} catch{
console.log("post传参解析错误")
}
})
}
} else {
console.log("访问的pathname不存在:", pathname);
res.send("404");
}
return handle;
}
//路由处理函数的注册函数,分为get和post两种
handle.get = function (str, callback) {
if (!str.startsWith("/")) {
str = "/" + str;
}
if (!str.endsWith("/")) {
str = str + "/";
}
self._get[str] = callback;
}
handle.post = function (str, callback) {
if (!str.startsWith("/")) {
str = "/" + str;
}
if (!str.endsWith("/")) {
str = str + "/";
}
self._post[str] = callback;
}
return handle;
}
module.exports = router();
app.js
var http = require("http");
var app = require("./model/express_router")
var fs = require("fs")
var ejs = require("ejs")
var server = http.createServer(app);
server.listen(9999);
app.get("/", function (req, res) {
console.log("访问到了 /");
res.send("访问到了 /");
})
// 网站标题图片
app.get("favicon.ico", function (req, res) {
fs.readFile("./view/favicon.ico", function (err, data) {
if (err) {
console.log("读取 favicon.ico 失败,err:", err);
} else {
//这里应该设置图片类型
res.send(data);
}
})
})
app.get("/login", function (req, res) {
ejs.renderFile("./view/login.ejs", {}, function (err, data) {
if (err) {
console.log("read login.ejs err:", err);
} else {
res.send(data);
}
})
})
//接受post请求
app.post("/dologin", function (req, res) {
console.log("req.query", req.query);
res.send("<script>alert('登录完成')</script>")
})
app.get("/register", function (req, res) {
ejs.renderFile("./view/register.ejs", {}, function (err, data) {
if (err) {
console.log("read register.ejs err:", err);
} else {
res.send(data);
}
})
})
//渲染变量到ejs中
app.get("/result", function (req, res) {
ejs.renderFile("./view/result.ejs", {
operate: "渲染变量",
result: "成功",
}, function (err, data) {
if (err) {
console.log("read result.ejs err:", err);
} else {
res.send(data);
}
})
})
console.log("自定义的类express应用运行于 http://127.0.0.1:9999")