中间件与洋葱模型
🎯 引言
学完这篇文章,你能彻底搞懂 Koa 中间件的执行顺序,说清楚洋葱模型是怎么一层层进入又一层层返回的,还能写出日志记录、响应时间统计这类实用中间件。洋葱模型是 Koa 的灵魂,也是面试和实际开发中的高频考点。
🧱 Koa 中间件长什么样
Koa 中间件就是一个 async 函数,接收两个参数:
app.use(async (ctx, next) => {
// 进入时:处理请求,比如记录日志
await next(); // 放行,交给后面的中间件
// 返回时:收尾工作,比如统计耗时
});
ctx:上一篇学过的上下文对象。next:一个函数,调用它(必须加await)才会执行下一个中间件,不调用请求就停在这里了。
Express 的中间件也有 next,但它调用后代码就一路往后执行,不会“回来”。Koa 的不同之处就在于 await next() 这行代码:后面的中间件全部执行完后,控制权会回到这里继续往下走。这就是洋葱模型的由来。
🧅 洋葱模型:进去又出来
先看三个中间件的例子:
import Koa from 'koa';
const app = new Koa();
app.use(async (ctx, next) => {
console.log('1 进入');
await next();
console.log('1 返回');
});
app.use(async (ctx, next) => {
console.log('2 进入');
await next();
console.log('2 返回');
});
app.use(async (ctx) => {
console.log('3 核心处理');
ctx.body = 'Hello';
});
app.listen(3000);
访问一次 http://localhost:3000,终端的输出顺序是:
1 进入
2 进入
3 核心处理
2 返回
1 返回
把中间件想象成一层层的洋葱:请求从外往里钻,穿过第 1 层、第 2 层,到达最核心的业务处理;处理完后响应再一层层往外传出来,原路返回。await next() 就是“往里钻”的那一步,它之后的代码在“往外传”时才执行。
请求 →
┌──────────────────┐
│ 中间件1 │
│ ┌──────────┐ │
│ │ 中间件2 │ │
│ │ ┌────┐ │ │
│ │ │核心│ │ │
│ │ └────┘ │ │
│ └──────────┘ │
└──────────────────┘
← 响应
🛠 经典实战:响应时间统计
洋葱模型不是花架子,它让“包裹整个请求过程”的统计变得自然。看一个经典例子:统计每个请求的处理耗时。
import Koa from 'koa';
const app = new Koa();
// 响应时间统计中间件
app.use(async (ctx, next) => {
const start = Date.now(); // 进入时记录开始时间
await next(); // 交给后面的中间件处理业务
// 返回时:业务已经处理完,计算耗时
const ms = Date.now() - start;
ctx.set('X-Response-Time', `${ms}ms`);
console.log(`${ctx.method} ${ctx.path} - ${ms}ms`);
});
// 业务中间件
app.use(async (ctx) => {
ctx.body = 'Hello Koa';
});
app.listen(3000);
访问页面后,终端会打印类似 GET / - 3ms 的日志,浏览器响应头里也能看到 X-Response-Time。
这个例子的巧妙之处:await next() 一行代码,把整个业务处理过程夹在了中间。start 在它之前记录,耗时计算在它之后进行,刚好完整包裹了请求的生命周期。如果换成分布在请求、响应两个钩子里的写法,就没有这么直观了。
⚖️ 与 Express 中间件的区别
Express 的中间件是线性的:一个接一个往下执行,next() 之后的代码会立刻执行,不会等其他中间件完成。想在 Express 里统计响应时间,得监听响应的 finish 事件,写法绕了一圈。
Koa 基于 async/await,天然支持“环绕式”的执行模型,一个 await next() 就能实现前置处理和后置处理。可以理解为:Express 是一根直线,Koa 是一个来回的折返跑。
另外两点写法差异:
- Koa 中间件必须写成
async函数,next()前要加await,否则执行顺序会乱。 - Koa 中间件里的异步错误能被
try/catch正常捕获,这为统一错误处理打下基础(后面会讲)。
await next() 前面的 await。少了它,后续中间件不会按预期完成,返回时的代码会在错误的时机执行。看到中间件里写了 next() 却没用 await,基本可以确定是 bug。🧾 小节总结
- Koa 中间件是接收
(ctx, next)的 async 函数,await next()放行到下一个中间件。 - 多个中间件按洋葱模型执行:请求一层层进入,响应原路返回,先进后出。
await next()之前的代码在进入时执行,之后的代码在返回时执行。- 利用洋葱模型可以优雅地实现日志、耗时统计这类“包裹整个请求”的功能。
next()前必须加await,漏掉会导致执行顺序错乱。
❓ 知识问答
Q1:中间件里不写 next() 会怎样?
请求就停在这个中间件里,后面的中间件不会执行。最后一个中间件通常不写 next(),因为它是处理的终点。
Q2:next() 后面的代码里能修改 ctx.body 吗?
可以。await next() 返回时,业务中间件已经设置好 ctx.body,你在这里可以读取甚至覆盖它。比如统一包装响应格式的中间件就是这么做的。
Q3:中间件的执行顺序由什么决定?
由 app.use() 的注册顺序决定,先注册的先进入、后返回。所以日志、错误处理这类中间件一般都注册在靠前的位置。
Q4:一个中间件里能调用多次 next() 吗?
不能。一个中间件里 next() 只能调用一次,多次调用会抛出错误。
🧪 小练习
- 写一个日志中间件,在每次请求进入时打印
[请求] GET /users,在返回时打印[响应] 200。
import Koa from 'koa';
const app = new Koa();
app.use(async (ctx, next) => {
// 请在这里编写代码
});
app.use(async (ctx) => {
ctx.body = '用户列表';
});
app.listen(3000);
- 在中间件里故意去掉
await next()的await,观察终端输出顺序有什么变化,理解为什么必须加await。
🎉 恭喜你已经拿下 Koa 的洋葱模型!下一篇我们给 Koa 装上路由,让不同的地址返回不同的内容。
