跳到主要内容

认识

一、认识


Koa 中,中间件 其实就是一个函数,通常采用 async 函数形式,接收两个参数:ctx(上下文对象)和 next(下一个中间件的执行函数)。这种函数签名大致如下:

async function middleware(ctx, next) {
// 前置逻辑
await next(); // 调用下一个中间件,形成“洋葱模型”
// 后置逻辑
}

Koa 的中间件 执行遵循 洋葱模型: 请求依次进入中间件链(前置逻辑),在最内层的中间件结束后,再依次执行中间件链的后置逻辑, 中间件通过 await next() 串联起来,形成一个基于 Promise 的调用链, 通过决定是否调用 next(),中间件可以实现请求的拦截、提前结束请求或者在调用下一个中间件后执行后续操作, 所有中间件共享同一个 ctx 对象,可以在其中附加自定义属性或方法,实现跨中间件的数据传递。这种设计允许在中间件中进行请求前和请求后的处理,非常适合用于日志记录、错误处理、权限验证等场景。

Koa 中间件原理: 定义递归函数 递归函数 dispatch(i), 从第 i 个中间件开始执行, 如果中间件数组中存在第 i 个函数,就调用该函数,并传入 ctx 和一个匿名函数 () => dispatch(i + 1) 作为 next 参数, 每个中间件调用完毕后,返回的是一个 Promise。由于每个中间件返回的是 Promise,通过递归调用 dispatch(i + 1),整个调用过程就被包装成一个 Promise 链。这就确保了中间件按照顺序执行,并且每个中间件在调用 await next() 后,会等待后续中间件执行完毕再继续自己的后置逻辑。在实现过程中, 会有一个 index 记录上次执行的中间件索引, 在 dispatch 函数内部开始前会比较当前要执行的中间件索引与上次执行的中间件索引, 如果当前的索引小于等于上次的索引, 说明 next() 已经被调用过了,抛出错误, 保证 next() 只能被调用一次。 如果 next() 被多次调用,就可能导致意外的行为: 1. 多次执行同一个中间件,导致请求流程紊乱; 2. 破坏 Promise 链,破坏洋葱模型, 使整个执行流程不可预测;

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 引擎(如 V8SpiderMonkey 等)并未普遍实现这一特性。因此,在实际开发中,尽管我们可以采用尾递归的写法,但不能完全依赖底层引擎来保证优化效果。

二、语法


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)=>{ 路由逻辑 })