什么是洋葱模型
洋葱模型是一种中间件的设计模式,特别适用于 Web 应用程序的请求处理流程。这个模型的名字来源于其结构和执行流程类似于洋葱的横截面:请求从最外层进入,依次经过每一层中间件,到达核心处理逻辑后,又依次经过每一层中间件返回。
在洋葱模型中,每个中间件函数都可以执行两次:一次是在请求到达时(从外到内),另一次是在响应返回时(从内到外)。这种模式允许中间件不仅可以控制请求的处理,还可以参与响应的生成过程。
import Koa from 'koa';
const app = new Koa();
app.use(async (ctx, next) => {
console.log(1);
await next();
console.log(2;
})
app.use(async (ctx, next) => {
console.log(3);
await next();
console.log(4);
})
app.use(async (ctx) => {
console.log(5);
})
app.listen(3000);
// 输出结果13542
koa使用 use方法注册中间件,维护一个中间件栈,每个中间件是个接收 ctx 以及 next 为参数的函数,其中 ctx是请求上下文,可用于获取请求相关的信息,比如 ctx.url ctx.method等,发送响应ctx.body,next方法用于调用下一个中间件。当遇到没有调用next的中间件或者栈内的最后一个中间件时,该中间件执行完会将控制权返还给上一个中间件,直到回到中间件栈内的第一个中间件。
为什么Koa使用洋葱模型
- 洋葱模型允许开发者更灵活地控制请求和响应的处理流程。中间件可以在请求处理之前和之后执行操作,这为复杂的业务逻辑提供了更大的空间。
- 可组合性:洋葱模型使得中间件可以更容易地组合和重用。每个中间件都可以专注于特定的功能,而不需要关心整个请求处理流程。
- 错误处理:洋葱模型使得错误处理变得更加直观和集中。错误可以在任何层被捕获和处理,而不会中断整个流程
- 异步流程控制:Koa 的洋葱模型非常适合处理异步操作。通过 async/await 语法,可以轻松地在中间件中执行异步操作,而不会陷入回调地狱。
洋葱模型实现
- 中间件栈:Koa 使用一个数组来存储所有的中间件函数。每个中间件函数接收两个参数:context 对象和 next 函数。
- compose 函数:这是实现洋葱模型的核心。compose 函数将所有中间件组合成一个单一的函数,确保它们按照正确的顺序执行。
- next 函数:每个中间件通过调用 next() 来将控制权传递给下一个中间件。当所有中间件执行完毕后,控制权会沿着原路返回。
compose函数实现
compose接收中间件栈,并将栈内的所有中间件组合成一个单一的函数,compose函数的基本结构如下:
function compose(middlewares) {
return function(context, next) {
}
}
- 当执行
compose返回的函数时,会依次调用每个中间件,直到执行到没有调用next的中间件或最后一个中间件后依次返回。因此,compose返回的函数内部应该有一个调度中间件执行的方法,该方法会从第一个中间件开始调度。
function compose(middlewares) {
return function(context, next) {
function dispatch(i) {
}
return dispatch(0);
}
}
dispatch方法内部会取出中间件,传入context和next参数,执行该中间件, 其中next是一个不带参数的方法,中间件内执行该方法,会将控制权交给下一个中间件,因此, 传给中间件函数的next实现应该是调度方法dispatch绑定了下一个中间件下标生成的函数。
function compose(middlewares){
return function(context, next) {
function dispatch(i) {
let fn = middlewares[i];
return fn(context, dispatch.bind(null, i + 1));
}
return dispatch(0);
}
}
- 支持
promise
function compose(middlewares){
return function(context, next) {
function dispatch(i) {
let fn = middlewares[i];
try {
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
}catch(error){
return Promise.reject(error);
}
}
return dispatch(0);
}
}
- 边界条件
middlewares需要是数组middlewares内的每个元素需要是函数- 连续调用
next的判断 - 执行到最后一个中间件,在最后一个中间件内调用
next
function compose(middlewares){
// 检查middlewares是否是数组
if (!Array.isArray(middlewares)) throw new Error('middlewares should be an array')
// 检查middleware是否是函数
if (middlewares.some(item => typeof item !== 'function')) throw new Error('middleware should be a function')
let index = -1;
return function(context, next) {
function dispatch(i) {
if (i <= index) return Promise.reject(new Error('连续调用next'));
let fn = middlewares[i];
index = i;
// 执行到最后一个中间件,在最后一个中间件内调用next
if(i === middlewares.length) fn = next;
// 当next不为空,则中间件链执行完还可以执行额外的操作,若为空,则中间件链也能正常结束
if (!fn) return Promise.resolve();
try {
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
}catch(error){
return Promise.reject(error);
}
}
return dispatch(0);
}
}