Koa 使用洋葱模型(Onion Model) ,核心原因是:让中间件既可以在请求进入时处理,又可以在后续中间件执行完之后回来处理响应。
可以把它理解成:
css
请求
↓
中间件 A:进入
↓
中间件 B:进入
↓
中间件 C:进入
↓
业务处理
↓
中间件 C:返回
↓
中间件 B:返回
↓
中间件 A:返回
↓
响应
1. 最直观的例子
Koa 中间件通常这样写:
javascript
app.use(async (ctx, next) => {
console.log('A start')
await next()
console.log('A end')
})
app.use(async (ctx, next) => {
console.log('B start')
await next()
console.log('B end')
})
请求进来时:
css
A start
B start
B end
A end
所以它像洋葱:
css
┌──────────────────────┐
│ A start │
│ ┌────────────────┐ │
│ │ B start │ │
│ │ ┌──────────┐ │ │
│ │ │ 业务逻辑 │ │ │
│ │ └──────────┘ │ │
│ │ B end │ │
│ └────────────────┘ │
│ A end │
└──────────────────────┘
2. 为什么需要这种设计?
最重要的是:可以非常自然地实现"前置处理 + 后置处理"。
例如日志统计:
javascript
app.use(async (ctx, next) => {
const start = Date.now()
await next()
console.log(`耗时:${Date.now() - start}ms`)
})
这里:
ini
const start = Date.now()
是在请求进入时执行的。
而:
scss
await next()
console.log(...)
是在所有后续中间件执行完之后执行的。
所以一个中间件就能完整包住后面的请求处理过程。
3. 这对异常处理尤其方便
例如统一错误处理:
vbnet
app.use(async (ctx, next) => {
try {
await next()
} catch (err) {
ctx.status = err.status || 500
ctx.body = {
message: err.message
}
}
})
后面的业务代码:
javascript
app.use(async ctx => {
throw new Error('数据库连接失败')
})
异常会一路向外冒泡:
javascript
错误处理
↓
业务中间件
↓
❌ Error
↑
错误处理捕获
这就是洋葱模型非常重要的价值:外层中间件可以包住内层中间件,从而统一处理内层产生的结果和异常。
4. 为什么 Express 不完全一样?
Express 经典的中间件模型更接近:
css
A → B → C → 响应
调用:
scss
next()
主要意味着:
"继续执行下一个中间件。"
而 Koa:
python
await next()
意味着:
"执行下一个中间件,并且等它执行完成以后,我还可以继续执行。"
这个 await 就是洋葱模型的关键。
可以简单对比:
| Express 传统模型 | Koa 洋葱模型 | |
|---|---|---|
| 向后执行 | next() |
await next() |
| 后续执行完再回来 | 不自然 | 非常自然 |
| 前置 + 后置逻辑 | 相对麻烦 | 很方便 |
| 异常统一处理 | 需要额外设计 | 很自然 |
| 性能统计 | 可以做 | 很自然 |
| 事务/锁/资源释放 | 可以做 | 很适合 |
5. 本质上,Koa 为什么选择它?
一句话:
Koa 希望中间件不仅能"处理请求",还能"包裹请求的整个生命周期"。
因此可以很方便地实现:
markdown
中间件开始
↓
进入下一层
↓
进入下一层
↓
业务逻辑
↓
返回上一层
↓
后置处理
↓
最终响应
这也是为什么 Koa 的核心代码其实非常简单------它本质上就是通过 async/await 把一组中间件组合成一个可嵌套、可回溯的执行链。
如果你正在学习 Koa,下一步最值得搞懂的是 koa-compose 是怎么用几十行代码实现这个洋葱模型的 。看懂 compose() 基本就彻底理解 Koa 中间件了。