认识
一、认识
在 Koa
中,中间件 其实就是一个函数,通常采用 async
函数形式,接收两个参数:ctx
(上下文对象)和 next
(下一个中间件的执行函数)。这种函数签名大致如下:
async function middleware(ctx, next) {
// 前置逻辑
await next(); // 调用下一个中间件,形成“洋葱模型”
// 后置逻辑
}
Koa
的中间件 执行遵循 洋葱模型: 请求依次进入中间件链(前置逻辑),在最内层的中间件结束后,再依次执行中间件链的后置逻辑, 中间件通过 await next()
串联起来,形成一个基于 Promise
的调用链, 通过决定是否调用 next()
,中间件可以实现请求的拦截、提前结束请求或者在调用下一个中间件后执行后续操作, 所有中间件共享同一个 ctx
对象,可以在其中附加自定义属性或方法,实现跨中间件的数据传递。这种设计允许在中间件中进行请求前和请求后的处理,非常适合用于日志记录、错误处理、权限验证等场景。
Koa
中间件原理:
-
Promise
链, 定义递归函数 递归函数dispatch(i)
, 从第i
个中间件开始执行, 如果中间件数组中存在第i
个函数,就调用该函数,并传入ctx
和一个匿名函数() => dispatch(i + 1)
作为next
参数, 每个中间件调用完毕后,返回的是一个Promise
。由于每个中间件返回的是Promise
,通过递归调用dispatch(i + 1)
,整个调用过程就被包装成一个Promise
链。这就确保了中间件按照顺序执行,并且每个中间件在调用await next()
后,会等待后续中间件执行完毕再继续自己的后置逻辑。 -
组织
next()
调用两次 在实现过程中, 会有一个index
记录上次执行的中间件索引, 在dispatch
函数内部开始前会比较当前要执行的中间件索引与上次执行的中间件索引, 如果当前的索引小于等于上次的索引, 说明next()
已经被调用过了,抛出错误, 保证next()
只能被调用一次。 如果next()
被多次调用,就可能导致意外的行为: 1. 多次执行同一个中间件,导致请求流程紊乱; 2. 破坏Promise
链,破坏洋葱模型, 使整个执行流程不可预测; -
最后一个中间件中的
next()
可有可无: 当dispatch
调用到index
等于middleware.length
时, 说明所有中间件都已执行完毕, 将fn
设置为传入的next
函数(如果有传入), 如果next
没有传入(即fn
不存在), 则返回一个立即resolve
的Promise
。
Koa
尾调用优化: Koa
中间件基于 compose
来组合中间件。这个模块通过遍历中间件数组,并依次返回一个 Promise
链,使得中间件调用不会形成深层的同步递归,而是通过事件循环来调度执行。这种设计方式有效避免了传统递归调用带来的栈溢出风险,即使中间件数量很多,也不会消耗过多的同步调用栈。由于大多数 JavaScript
引擎(如 V8
)并未普遍支持尾调用优化,Koa
通过异步编程模式(async/await
+ Promise
链)实现了类似效果,确保中间件调用的高效性和稳定性。如果可以的话, 还是可以把 await next()
放在尾部,避免在其后有复杂的同步运算或递归调用。虽然在实际场景中很多中间件都需要做前置和后置处理,但保持调用链的简洁性有助于减少额外的栈帧积累。
尾调用扩展: 尾调用 Tail Call
, 指的是在一个函数的最后一步直接调用另一个函数(或者自己),并将这个调用的返回结果直接返回给调用者。 尾调用 Tail Call
在尾调用的位置,当前函数不需要保留自己的执行上下文(例如局部变量、后续操作等),因此理论上可以复用当前的栈帧。如果 JavaScript
引擎实现了尾调用优化(Tail Call Optimization, TCO
),那么尾调用就可以避免调用栈不断增长的问题。因此 尾调用 不会在调用栈上增加新的堆栈帧(不会增加调用栈的长度),而是直接更新调用栈,调用栈所占空间始终是常量,节省了内存,避免了爆栈的可能性。所以, 我们在开发中, 尽量采用尾递归写法, 当需要递归操作时,可以重构函数,使递归调用处于尾调用位置。例如,在计算累加和、遍历树结构等场景下。ES6
规范要求:ECMAScript 2015(ES6)
规范中要求实现尾调用优化,以便在尾调用位置复用调用栈。但这要求是在严格模式下进行的。尽管理论上 ES6
规范支持尾调用优化,但目前主流的 JavaScript
引擎(如 V8
、SpiderMonkey
等)并未普遍实现这一特性。因此,在实际开发中,尽管我们可以采用尾递归的写法,但不能完全依赖底层引擎来保证优化效果。
二、语法
2.1 无参定义
定义中间件
module.exports = async function (ctx, next) {
console.log("Koa 中间件---router2: next() 之前");
await next();
console.log("Koa 中间件---router2: next() 之后");
};
使用中间件
app.use(中间件1);
app.use(中间件2);
router.get(路由,中间件1,中间件2……,async (ctx)=>{ 路由逻辑 })
2.2 传参定义
定义中间件
module.exports = function (params) {
return async (ctx, next) => {
console.log("Koa 中间件---router1 参数:", params);
console.log("Koa 中间件---router1: next() 之前");
await next();
console.log("Koa 中间件---router1: next() 之后");
};
};
使用中间件
app.use(中间件1(参数));
app.use(中间件2(参数));
router.get(路由,中间件1(参数),中间件2(参数)……,async (ctx)=>{ 路由逻辑 })
三、问题
3.1 中间件中, 异常处理是怎么做的?
在 Koa
中, 异常处理通常通过在最外层 (最顶层) 的中间件中使用 try/catch
块来捕获所有下层中间件中的异常, 防止错误未被捕获而导致进程崩溃, 从而实现全局统一的错误处理。这种方式充分利用了 Koa
的洋葱模型, 如果某个中间件内部发生异常, 并没有自己处理, 则会向上冒泡, 直到被顶层中间件捕获。
const Koa = require('koa');
const app = new Koa();
// 顶层错误处理中间件
app.use(async (ctx, next) => {
try {
await next(); // 调用下层中间件
} catch (err) {
// 捕获异常后设置状态码和错误响应
ctx.status = err.status || 500;
ctx.body = { error: err.message };
// 发出 error 事件,可用于集中记录日志或报警
ctx.app.emit('error', err, ctx);
}
});
// 其他中间件示例
app.use(async (ctx, next) => {
// 模拟发生错误
throw new Error('发生了一个错误');
});
app.on('error', (err, ctx) => {
console.error('全局错误处理:', err);
});
app.listen(3000, () => {
console.log('服务器启动,监听 3000 端口');
});
3.2 如果没有 async/await , Koa 如何实现洋葱模型?
在没有 async/await
的时代, Koa
(特别是 Koa v1
)是通过 generator
函数和 co
库来实现洋葱模型的。
// Koa v1 中间件示例(使用 generator 函数)
app.use(function* (next) {
console.log('中间件1 - 前置逻辑');
yield next;
console.log('中间件1 - 后置逻辑');
});
app.use(function* (next) {
console.log('中间件2 - 前置逻辑');
yield next;
console.log('中间件2 - 后置逻辑');
});