0.背景
中间件在前端的使用中越来越频繁,从最早提出中间件概念的express,再到后来的koa,再到前端框架Redux和库axios拦截器等,越来越多的库和框架用到了“中间件”的思想。
中间件模式特点:中间件模式中最妙的一处就是中间件的执行顺序
1.中间件的使用
const Koa = require('./like-koa2');
const app = new Koa();
// 中间件1
app.use(async (ctx, next) => {
console.log("第一层洋葱--开始")
await next();
console.log("第一层洋葱--结束")
});
// 中间件2
app.use(async (ctx, next) => {
console.log("第二层洋葱--开始")
await next();
console.log("第二层洋葱--结束")
});
// 中间件3
app.use(async ctx => {
console.log("第三层洋葱--开始")
ctx['body'] = 'Hello World';
console.log("第三层洋葱--结束")
});
app.listen(3010);
上述使用的功能本质上就是如下流程
// 本质上的中间件运行流程如下
// 中间件1 执行
async function1(ctx, next) => {
console.log("第一层洋葱--开始")
// 执行中间件1的next1():执行fn2
async function2(ctx, next) => {
console.log("第二层洋葱--开始")
// next2():执行fn3
async ctx => {
console.log("第三层洋葱--开始")
ctx['body'] = 'Hello World';
// next() 不管next有没有,执行没执行都是最后一个,dispatch(i+1)都超过了arr数组长度了
console.log("第三层洋葱--结束")
}
console.log("第二层洋葱--结束") // 执行完之后函数弹栈,继续走刚才弹栈函数的外部函数(即上一个函数的流程)
}
console.log("第一层洋葱--结束")
}
2.实现原理
上面的流程具体是如何实现的呢? koa中间件模拟实现,其中compose为核心函数:而compose函数的核心是next函数实现:
next函数本质上为: dispatch.bind(null, i+1)
const http = require('http');
// 组合中间件
function compose(middlewaraeList) {
return function (ctx) {
return dispatch(0);
function dispatch(i) {
const fn = middlewaraeList[i]; // 1.按照注册顺序依次取出中间件队列中的函数
try {
// 防止fn不是Promise
// 2.然后立即执行这个函数,use中的注册函数参数(context, next)实现
// 2.1 context就是ctx传入
// 2.2 next就是自身函数调用dispatch(i + 1):
// 这样执行中间件函数1过程中执行next,就是相当于执行函数1的过程中立即再次调用diapatch(i+1),取出middlewaraeList[i+1]函数执行,当然需要dispatch.bind(null, i+1)
return Promise.resolve(
fn(ctx, dispatch.bind(null, i + 1)) // 实现next机制
);
// 那么有个问题:如果最后注册的那个中间件执行完毕之后呢?
// 需要注意的是,最后注册的中间件不管有没有next执行,最后的注册函数都会走完,并且
// 在他之前的所有中间件都会倒序往下走。可以把它理解为函数堆栈的过程。
} catch (e) {
return Promise.reject(e);
}
}
}
}
class LikeKoa2 {
constructor() {
// 中间件存储的地方
this.middlewaraes = [];
}
listen(...args) {
const server = http.createServer(this.callback());
server.listen(...args);
}
callback() {
// 组合中间件
const fn = compose(this.middlewaraes);
return (req, res) => {
// 创建上下文对象
const ctx = this.createContext(req, res);
// 将上下文传入中间件
return this.handleRequest(ctx, fn);
}
}
// 注册中间件,传入的fn函数就是注册传入的参数函数,这个参数函数自身还有两个参数:
// 1.一个是context对象
// 2.一个是next函数参数
use(fn) {
this.middlewaraes.push(fn);
return this; // 链式调用
}
createContext(req, res) {
const ctx = {
req,
res,
};
return ctx;
}
// 把上下文传入中间件函数
handleRequest(ctx, fn) {
return fn(ctx);
}
}
module.exports = LikeKoa2;
axios中间件拦截器参考文献:juejin.cn/post/688275…