前言
express是node.js最常用的web server应用框架。 框架就是遵循一定规则的代码架子,它有两个特点:
- 封装API,让开发者更关注于业务代码的开发
- 有一定的流程和标准
express框架的核心特性:
- 可以设置中间件来响应 HTTP 请求
- 定义了路由表用于执行不同的 HTTP 请求动作
- 可以通过向模板传递参数来动态渲染 HTML 页面
本文通过实现一个简单的Express类,来浅析express如何实现中间注册、next机制和路由处理。
express功能分析
首先通过两个express代码示例来分析一下express提供的功能:
express官网Hello world示例
const express = require('express')
const app = express()
const port = 3000
app.get('/', (req, res) => {
res.send('Hello World!')
})
app.listen(port, () => {
console.log(`Example app listening at http://localhost:${port}`)
})
入口文件app.js分析
以下是通过express-gernerator脚手架生成的express项目的入口文件app.js的代码:
// 处理路由不匹配的错误
var createError = require('http-errors');
var express = require('express');
var path = require('path');
// 记录日志
var logger = require('morgan');
var indexRouter = require('./routes/index');
var usersRouter = require('./routes/users');
// app是一个express实例
var app = express();
// view engine setup 注册视图引擎
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'jade');
// 解析post请求的json格式,给req加body字段
app.use(express.json());
// 解析post请求的urlencoded格式,给req加body字段
app.use(express.urlencoded({
extended: false
}));
// 静态文件处理
app.use(express.static(path.join(__dirname, 'public')));
// 注册父级路由
app.use('/', indexRouter);
app.use('/users', usersRouter);
// catch 404 and forward to error handler
app.use(function (req, res, next) {
next(createError(404));
});
// error handler
app.use(function (err, req, res, next) {
// set locals, only providing error in development
res.locals.message = err.message;
// 开发环境抛错,生产环境不抛错
res.locals.error = req.app.get('env') === 'development' ? err : {};
// render the error page
res.status(err.status || 500);
res.render('error');
});
module.exports = app;
从以上两段代码可以看出,express实例app主要有3个核心方法:
app.use([path,] callback [, callback...])注册中间件,当所请求路径的基数匹配时,将执行中间件函数。- path:调用中间件功能的路径
- callback:回调函数,可以是:
- 单个中间件函数
- 一系列中间件函数(以逗号分隔)
- 中间件函数数组
- 以上所有的组合
app.get()、app.post()与use()方法类似,都是实现中间件的注册,只不过与http请求进行了绑定,只有使用了相应的http请求方法才会触发中间件注册app.listen()创建httpServer,传递server.listen()需要的参数
代码实现
基于以上express代码的功能分析,可以看出express的实现有三个关键点:
- 中间件函数的注册
- 中间件函数中核心的next机制
- 路由处理,主要是路径匹配
基于以上关键点,下面实现一个简易的
LikeExpress类。
1、类的基本结构
先确定这个类要实现的主要方法:
use():实现通用的中间件注册get()、post():实现与http请求相关的中间件注册listen():实际上就是httpServer的listen()函数,所以在这个类的listen()函数里创建httpServer,透传server参数,监听请求,并执行回调函数(req, res) => {}复习一下node原生的httpServer的使用:const http = require("http"); const server = http.createServer((req, res) => { res.end("hello"); }); server.listen(3003, "127.0.0.1", () => { console.log("node服务启动成功了"); })
所以LikeExpress类基本结构如下:
const http = require('http');
class LikeExpress {
constructor() {}
use() {}
get() {}
post() {}
// httpServer回调函数
callback() {
return (req, res) => {
res.json = (data) => {
res.setHeader('content-type', 'application/json');
res.end(JSON.stringify(data));
};
}
}
listen(...args) {
const server = http.createServer(this.callback());
// 参数透传到httpServer里
server.listen(...args);
}
}
module.exports = () => {
return new LikeExpress();
}
2、中间件注册
从 app.use([path,] callback [, callback...]) 可以看出,中间件可以是函数数组,也可以是单个函数,这里为了简化实现,统一将中间件处理为函数数组。
而LikeExpress类可以实现中间件注册的方法有:use()、get()、post()
以上3个方法都可以实现中间件的注册,只是请求方法的不同,触发的中间件不一样。
因此可以考虑:
- 抽象出通用的中间件注册函数。
- 为这三个方法建立3个中间件函数数组,存放不同请求的中间件。
use()是所有请求通用的中间件注册方法,因此存放use()中间件的数组是get()和post()的并集。
(1)中间件队列数组
中间件数组需要存放在公用的地方,以便类里的方法都能读取到这些中间件,所以考虑将中间件数组放在constructor()构造函数中。
constructor() {
// 存放中间件的列表
this.routes = {
all: [],// 通用的中间件
get: [],// get请求的中间件
post: [],// post请求的中间件
};
}
(2)中间件注册函数
所谓的中间件注册,即把中间件存入相应的中间件数组中。中间件注册函数需要解析传入的参数,第一个参数可能是路由,也可能是中间件,所以需要判断第一个参数是不是路由,如果是路由,则将路由原样输出;否则默认是根路由。再将剩余中间件参数转换为数组。
register(path) {
const info = {};
// 如果第一个参数是路由
if (typeof path === "string") {
info.path = path;
// 从第二个参数开始,转换为数组,存入中间件数组中
info.stack = Array.prototype.slice.call(arguments, 1); // 取出第二个参数
} else {
// 如果第一个参数不是路由,则默认是根路由,则全部路由都会执行
info.path = '/';
// 从第一个参数开始,转换为数组,存入中间件数组中
info.stack = slice.call(arguments, 0);
}
return info;
}
(3)use()、get()、post() 实现
有了通用的中间件注册函数register(),就可以基于register()轻松实现 use()、get()、post(),将中间件存入相应的中间件数组中:
use() {
const info = this.register.apply(this, arguments);
this.routes.all.push(info);
}
get() {
const info = this.register.apply(this, arguments);
this.routes.get.push(info);
}
post() {
const info = this.register.apply(this, arguments);
this.routes.post.push(info);
}
3、路由匹配处理
当注册函数中第一个参数为路由时,只有当请求路径与路由匹配或者是它的子路由时,才会触发相应的中间件函数。
所以需要一个路由匹配函数,根据请求方法和请求路径,取出匹配路由的中间件数组,供后续的callback()去执行:
match(method, url) {
let stack = [];
// 浏览器自带的icon请求,忽略
if (url === "/favicon") {
return stack;
}
// 获取routes
let curRoutes = [];
curRoutes = curRoutes.concat(this.routes.all);// use()会在所有路由执行
curRoutes = curRoutes.concat(this.routes[method]); // 根据请求方法获取对应路由
curRoutes.forEach(route => {
if (url.indexOf(route.path) === 0) {
// 判断是否属于当前路由或子路由,如果是,则取出
stack = stack.concat(route.stack);
}
})
return stack;
}
然后在httpServer的回调函数callback()中取出需要执行的中间件:
callback() {
return (req, res) => {
res.json = (data) => {
res.setHeader('content-type', 'application/json');
res.end(JSON.stringify(data));
};
// 根据请求方法和路径,区分哪些中间件函数需要执行
const url = req.url;
const method = req.method.toLowerCase();
const resultList = this.match(method, url);
// handle是核心的next机制,接下来会讲
this.handle(req, res, resultList);
}
}
4、next机制实现
express的中间件函数参数是:req, res, next,next参数是一个函数,只有调用它才可以使中间件函数一个一个按照顺序执行下去,与ES6的Generator中的next()类似。
而回到我们的实现上,其实就是要实现一个next()函数,这个函数需要:
- 从中间件队列数组里每次按次序取出一个中间件
- 把
next()函数传入到取出的中间件中。由于中间件数组是公用的,每次执行next(),都会从中间件数组中取出第一个中间件函数执行,从而实现了中间件按次序的效果
// 核心的next机制
handle(req, res, stack) {
const next = () => {
// 中间件队列出队,拿到第一个匹配的中间件,stack数组是同一个,所以每执行一次next(),都会取出下一个中间件
const middleware = stack.shift();
if (middleware) {
// 执行中间件函数
middleware(req, res, next);
}
}
// 立马执行
next();
}
测试
为了验证上述LikeExpress类是否实现了中间件注册、路由匹配以及next机制的功能,用一段代码验证:
const express = require('./like-express');
const app = express();
// 1
app.use((req, res, next) => {
console.log('请求开始...', req.method, req.url);
next();
})
// 2
app.use((req, res, next) => {
console.log('处理cookie...');
req.cookie = {
useId: "test"
};
next();
})
// 3
app.use('/api', (req, res, next) => {
console.log('处理/api路由');
next();
})
// 4
app.get('/api', (req, res, next) => {
console.log('get /api路由');
next();
})
app.listen(7000, () => {
// console.log('server is running at 7000');
})
可以看到,实际结果与预期结果相同,证明我们的实现是正确的。
完整代码
const http = require('http');
const slice = Array.prototype.slice;
class LikeExpress {
constructor() {
// 存放中间件的列表
this.routes = {
all: [],
get: [],
post: [],
};
}
register(path) {
const info = {};
// 如果第一个参数是路由
if (typeof path === "string") {
info.path = path;
// 从第二个参数开始,转换为数组,存入stack
info.stack = slice.call(arguments, 1); // 取出第二个参数
} else {
// 如果第一个参数不是路由,则默认是根路由,则全部路由都会执行
info.path = '/';
// 从第一个参数开始,转换为数组,存入stack
info.stack = slice.call(arguments, 0);
}
return info;
}
use() {
const info = this.register.apply(this, arguments);
this.routes.all.push(info);
}
get() {
const info = this.register.apply(this, arguments);
this.routes.get.push(info);
}
post() {
const info = this.register.apply(this, arguments);
this.routes.post.push(info);
}
match(method, url) {
let stack = [];
// 浏览器自带的icon请求
if (url === "/favicon") {
return stack;
}
// 获取routes
let curRoutes = [];
curRoutes = curRoutes.concat(this.routes.all);
curRoutes = curRoutes.concat(this.routes[method]); // 根据请求方法获取对应路由
curRoutes.forEach(route => {
if (url.indexOf(route.path) === 0) {
// 判断是否属于当前路由或字路由
stack = stack.concat(route.stack);
}
})
return stack;
}
// 核心的next机制
handle(req, res, stack) {
const next = () => {
// 拿到第一个匹配的中间件
const middleware = stack.shift();
if (middleware) {
// 执行中间件函数
middleware(req, res, next);
}
}
// 立马执行
next();
}
callback() {
return (req, res) => {
res.json = (data) => {
res.setHeader('content-type', 'application/json');
res.end(JSON.stringify(data));
};
const url = req.url;
const method = req.method.toLowerCase();
// 根据方法区分哪些函数需要执行
const resultList = this.match(method, url);
this.handle(req, res, resultList);
}
}
listen(...args) {
const server = http.createServer(this.callback());
server.listen(...args);
}
}
module.exports = () => {
return new LikeExpress();
}