Koa 简介

中间件与洋葱模型

深入理解 Koa 中间件的执行机制,掌握洋葱模型的执行顺序,学会编写实用的中间件。

🎯 引言

学完这篇文章,你能彻底搞懂 Koa 中间件的执行顺序,说清楚洋葱模型是怎么一层层进入又一层层返回的,还能写出日志记录、响应时间统计这类实用中间件。洋葱模型是 Koa 的灵魂,也是面试和实际开发中的高频考点。


🧱 Koa 中间件长什么样

Koa 中间件就是一个 async 函数,接收两个参数:

app.js
app.use(async (ctx, next) => {
    // 进入时:处理请求,比如记录日志
    await next(); // 放行,交给后面的中间件
    // 返回时:收尾工作,比如统计耗时
});
  • ctx:上一篇学过的上下文对象。
  • next:一个函数,调用它(必须加 await)才会执行下一个中间件,不调用请求就停在这里了。

Express 的中间件也有 next,但它调用后代码就一路往后执行,不会“回来”。Koa 的不同之处就在于 await next() 这行代码:后面的中间件全部执行完后,控制权会回到这里继续往下走。这就是洋葱模型的由来。


🧅 洋葱模型:进去又出来

先看三个中间件的例子:

app.js
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  │   │
  │   │  ┌────┐  │   │
  │   │  │核心│  │   │
  │   │  └────┘  │   │
  │   └──────────┘   │
  └──────────────────┘
       ← 响应
记住口诀:先进后出。先进入的中间件,最后才完成返回。这和函数调用栈是一个道理。

🛠 经典实战:响应时间统计

洋葱模型不是花架子,它让“包裹整个请求过程”的统计变得自然。看一个经典例子:统计每个请求的处理耗时。

app.js
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() 只能调用一次,多次调用会抛出错误。


🧪 小练习

  1. 写一个日志中间件,在每次请求进入时打印 [请求] GET /users,在返回时打印 [响应] 200
app.js
import Koa from 'koa';

const app = new Koa();

app.use(async (ctx, next) => {
    // 请在这里编写代码
});

app.use(async (ctx) => {
    ctx.body = '用户列表';
});

app.listen(3000);
  1. 在中间件里故意去掉 await next()await,观察终端输出顺序有什么变化,理解为什么必须加 await

🎉 恭喜你已经拿下 Koa 的洋葱模型!下一篇我们给 Koa 装上路由,让不同的地址返回不同的内容。