中间件实现原理
Express
在Express对象中存储一系列请求方法注册的中间件队列,使用use()
、get()
等方法向队列中添加中间件对象,中间件对象包括路径和方法两个属性,当请求匹配到中间件中的请求路径前缀时就会执行该中间件方法
next方法是一个有名的立即执行函数,当第一次匹配时会立即执行,并将该函数传递给用户中间件的方法为参数,若用户调用next方法时会调用我们定义好的next方法执行下一个中间件方法
const http = require("http");
/**
* 校验参数,并返回一个中间件对象
* @param {...any} args 路径或中间件回调函数的可变参数
*/
function checkout(...args) {
const middleware = {};
if (typeof args[0] === "string") {
middleware.path = args[0];
middleware.queue = args.slice(1);
} else {
middleware.path = "/";
middleware.queue = args.slice(0);
}
return middleware;
}
/**
* 为http中的请求响应对象扩展方法
* @param {Request} req 请求对象
* @param {Response} res 响应对象
*/
function extendsMethod(req, res) {
res.send = (data) => {
res.setHeader("Content-type", "application/json");
res.end(JSON.stringify(data))
}
}
/**
* 根据请求响应对象返回正真要触发的路由列表
* @param {Request} req 请求对象
* @param {Response} res 响应对象
* @param {Routers} routers Express中的路由对象
*/
function getRealRouters(req, res, routers) {
let curRouters = []; //存储可能要触发路由列表
let realRouters = []; //要真实触发的路由列表
curRouters.push(...routers.use); //use中所有中间件都可能会被执行
curRouters.push(...routers[req.method.toLowerCase()]); //根据请求方法获取相应可能执行的中间件
//由于中间件队列变量是小写的所以要要将请求方法转小写
curRouters.forEach(item => {
if (req.url.indexOf(item.path) === 0) { //请求路径与中间件路径前缀匹配时
//前缀匹配是为了注册前缀中间件可被执行到
realRouters.push(...item.queue); //会将中间件中的方法加入到要触发的中间件中
}
})
return realRouters;
}
class Express {
constructor() {
this.routers = {
use: [], //存放use方法注册的中间件
get: [], //存放get方法注册的中间件
}
}
use(...args) {
const middleware = checkout(...args);
this.routers.use.push(middleware); //添加到use队列
}
get(...args) {
const middleware = checkout(...args);
this.routers.get.push(middleware); //添加到get队列
}
listen(...args) {
const server = http.createServer((req, res) => {
extendsMethod(req, res); //扩展req和res对象方法
const realRouters = getRealRouters(req, res, this.routers); //获取要执行的中间件队列
(function next() {
const middleware = realRouters.shift();
if (middleware) { //取出一个中间件方法,若不为null
middleware(req, res, next); //则执行该方法,并将req和res参数传递給该中间件
//最重要的就是将next方法本身传递给该中间件,若中间件内部调用next方法就又会执行该立即执行函数
}
})();//立即执行
});
server.listen(...args);
}
}
module.exports = () => {
//工厂函数,外部调用该方法即可返回一个Express实例
return new Express()
}
Koa2
在Koa对象中存储只存储中间对应的方法,使用use()
方法向队列中添加中间件对象
next方法是一个有名的立即执行函数,当第一次匹配时会立即执行,使用函数对象的bind()
方法进行参数绑定,并将包装后的ctx
参数和该函数传递给用户中间件的方法为参数,若用户调用next方法时会调用我们定义好的next方法执行下一个中间件方法
将中间件函数的执行结果使用promise包装,兼容非async函数的中间件
const http = require("http");
/**
* 将请求对象和响应对象包装成ctx对象
* @param {Request} req 请求对象
* @param {Response} res 响应对象
*/
function extendsMethod(req, res) {
return {
req,
res
};
}
class Koa {
constructor() {
this.middlewares = []; //用于存放中间件方法
}
use(...args) {
this.middlewares.push(...args); //添加到中间件队列
return this; //链式调用
}
listen(...args) {
const server = http.createServer((req, res) => {
const ctx = extendsMethod(req, res); //包装成ctx对象
const middlewares = this.middlewares; //暂存中间件队列
//由于中间件队列是地址引用,所以不能使用出队方式,否则下次访问时就没了
(function next(i) {
const middleware = middlewares[i]; //取出第i个中间件
if (!middleware) return; //若取出的是null,说明后面已经没有中间件了,所以什么也不用做
try {
return Promise.resolve(middleware(ctx, next.bind(null, i + 1)));
//兼容非async函数,包装成promise
//next.bind(null, i + 1)返回一个参数为i + 1的next函数,用于递归向下调用
} catch (error) {
return Promise.reject(error);
}
})(0); //立即执行
});
server.listen(...args);
}
}
module.exports = Koa;
Comments NOTHING