前置核心概念
- Express :线性瀑布式中间件模型,基于回调,无原生
async/await支持; - Koa2 :洋葱圈中间件模型,基于
async/await + compose,原生支持异步; - 同步:代码无定时器、IO、数据库、Promise;
- 异步:定时器、文件读写、数据库、网络请求、Promise、async 函数。
统一测试需求:
3 层中间件,要求:
- 中间件1:前置拼接
a,后置输出最终结果 - 中间件2:拼接
b - 中间件3:拼接
c
最终在中间件1后置打印message = abc
一、同步场景:Express vs Koa(无异步操作)
1. Express 同步中间件
js
const express = require('express')
const app = express()
app.use((req, res, next) => {
req.msg = 'a'
console.log('Express 中间件1 前置')
next()
// next之后的代码,所有下游中间件执行完才执行
console.log('Express 中间件1 后置:', req.msg)
})
app.use((req, res, next) => {
req.msg += 'b'
console.log('Express 中间件2 前置')
next()
console.log('Express 中间件2 后置')
})
app.use((req, res) => {
req.msg += 'c'
console.log('Express 中间件3 执行完毕')
res.send(req.msg)
})
app.listen(3000)
执行顺序(同步无任何异步)
Express 中间件1 前置
Express 中间件2 前置
Express 中间件3 执行完毕
Express 中间件2 后置
Express 中间件1 后置: abc
同步下 Express 特点
- 同步代码下,执行流程和Koa洋葱模型表现一致;
next()是同步跳转,会立刻执行下一个中间件;next()后面代码会等所有下游中间件全部走完再回头执行;- 同步场景二者行为几乎无差异。
2. Koa 同步中间件
js
const Koa = require('koa')
const app = new Koa()
app.use((ctx, next) => {
ctx.msg = 'a'
console.log('Koa 中间件1 前置')
next()
console.log('Koa 中间件1 后置:', ctx.msg)
})
app.use((ctx, next) => {
ctx.msg += 'b'
console.log('Koa 中间件2 前置')
next()
console.log('Koa 中间件2 后置')
})
app.use(ctx => {
ctx.msg += 'c'
console.log('Koa 中间件3 执行完毕')
ctx.body = ctx.msg
})
app.listen(8000)
输出顺序和Express完全一致
Koa 中间件1 前置
Koa 中间件2 前置
Koa 中间件3 执行完毕
Koa 中间件2 后置
Koa 中间件1 后置: abc
同步场景小结
同步代码时,Express 和 Koa 表现完全相同 :
自上而下执行 next() 前代码,全部走完后,自下而上执行 next() 后代码。
差异只在异步场景爆发。
二、异步场景:Express(巨大缺陷)vs Koa(完美可控)
场景设定:中间件2中加入异步延时操作(模拟数据库/接口请求)
js
// 模拟异步IO
function sleep(time) {
return new Promise(resolve => setTimeout(resolve, time))
}
1. Express 异步中间件(重大问题)
js
const express = require('express')
const app = express()
app.use((req, res, next) => {
req.msg = 'a'
console.log('Express 中间件1 前置')
next()
// 异步导致此处提前执行,拿不到完整abc
console.log('Express 中间件1 后置:', req.msg)
})
app.use(async (req, res, next) => {
req.msg += 'b'
console.log('Express 中间件2 前置')
// 异步延时2秒
await sleep(2000)
next()
console.log('Express 中间件2 后置')
})
app.use((req, res) => {
req.msg += 'c'
console.log('Express 中间件3 执行完毕')
res.send(req.msg)
})
app.listen(3000)
执行输出顺序(错误)
less
Express 中间件1 前置
Express 中间件2 前置
Express 中间件1 后置: a // 重点:异步阻塞前直接回头执行后置!
// 等待2秒后
Express 中间件3 执行完毕
Express 中间件2 后置
问题分析
- Express 不识别
async/await,next()调用后直接同步返回,不会等待异步代码完成; - 中间件2内部
await sleep阻塞时,事件循环让出,Express 直接回到中间件1执行next()后面的代码; - 中间件1后置代码提前执行,
req.msg只有a,拿不到最终拼接结果; - 无法实现"等所有下游逻辑完成再执行后置"的需求。
Express 异步解决方案(丑陋)
必须手动在异步结束后调用 next(),强行嵌套回调,回调地狱:
js
app.use((req, res, next) => {
req.msg += 'b'
console.log('中间件2 前置')
setTimeout(() => {
req.msg += '延迟b'
next() // 异步完成后再放行
}, 2000)
})
缺点:多层异步会无限嵌套,流程不可读、难以维护。
2. Koa 异步中间件(原生支持,流程不乱)
js
const Koa = require('koa')
const app = new Koa()
function sleep(time) {
return new Promise(resolve => setTimeout(resolve, time))
}
app.use(async (ctx, next) => {
ctx.msg = 'a'
console.log('Koa 中间件1 前置')
await next() // 等待所有下游中间件全部执行完成才往下走
console.log('Koa 中间件1 后置:', ctx.msg)
})
app.use(async (ctx, next) => {
ctx.msg += 'b'
console.log('Koa 中间件2 前置')
await sleep(2000) // 异步阻塞,不会跳出当前中间件
await next()
console.log('Koa 中间件2 后置')
})
app.use(ctx => {
ctx.msg += 'c'
console.log('Koa 中间件3 执行完毕')
ctx.body = ctx.msg
})
app.listen(8000)
正确输出顺序
arduino
Koa 中间件1 前置
Koa 中间件2 前置
// 等待2秒
Koa 中间件3 执行完毕
Koa 中间件2 后置
Koa 中间件1 后置: abc
Koa 异步核心原理
- Koa 使用
compose组合中间件,所有中间件被包装成 Promise 调用链; await next()会暂停当前中间件,完整等待下游所有中间件(含异步)全部执行完毕,才恢复当前中间件剩余代码;- 无论中间件内部有多少层异步、定时器、数据库操作,洋葱模型执行顺序永远稳定不变;
- 无回调嵌套,线性代码书写复杂异步流程。
三、同步/异步场景完整异同对照表
1. 相同点
- 同步代码场景
-
- 两者执行顺序完全一致:自上而下前置 → 自下而上后置;
next()同步跳转,下游全部执行完再回头执行后置逻辑;- 简单同步接口(无数据库/文件)表现无区别。
- 基础能力一致
-
- 都基于 Node http 模块封装;
- 都依靠中间件处理请求;
- 都支持多层中间件堆叠。
2. 不同点(分同步、异步区分)
| 维度 | Express | Koa2 |
|---|---|---|
| 同步执行逻辑 | 线性流转,next后代码后置执行 | 洋葱流转,和Express同步表现一致 |
| 异步执行逻辑 | next() 同步返回,不会等待异步;异步阻塞会直接跳出当前中间件,后置代码提前执行,流程错乱 | await next() 阻塞等待全部下游异步完成,洋葱顺序永久稳定 |
| 异步语法支持 | 原生不支持async/await,需回调嵌套,极易产生回调地狱 | 原生基于Promise+async/await,无嵌套 |
| 中间件底层实现 | 数组顺序遍历,线性瀑布模型 | compose递归调用,Promise链式洋葱模型 |
| 上下文对象 | req、res分离,全局挂载数据容易冲突 | 统一ctx上下文,一次请求独立ctx,数据隔离 |
| 异步错误捕获 | 异步内部抛出错误无法被全局错误捕获,必须手动try/catch传err给next(err) | 全局可捕获async中间件抛出的错误,统一error事件处理 |
| 多异步串联 | 多层异步必须嵌套,可读性极差 | 平铺书写,await顺序执行,逻辑清晰 |
四、异步错误处理对比(补充关键差异)
Express 异步错误捕获缺陷
js
app.use(async (req, res, next) => {
// 异步抛出错误,Express 捕获不到,直接崩溃
throw new Error('数据库查询失败')
next()
})
解决方式:必须手动 try/catch + next(err)
js
app.use(async (req, res, next) => {
try {
await sleep(1000)
throw new Error('异常')
} catch (err) {
next(err) // 手动传递错误
}
})
// 错误中间件接收
app.use((err, req, res, next) => {
res.status(500).send(err.message)
})
Koa 异步错误天然支持
js
app.use(async (ctx, next) => {
await sleep(1000)
throw new Error('数据库异常')
})
// 全局统一捕获,无需手动传递
app.on('error', (err, ctx) => {
ctx.status = 500
ctx.body = { msg: err.message }
})
所有 async 中间件抛出的异常都会被 Koa 内部 Promise 捕获,自动触发全局 error 事件。
五、总结核心结论
- 只写同步代码:Express 和 Koa 几乎没有区别,洋葱/瀑布执行效果一样;
- 项目存在大量异步(数据库、Redis、文件、接口) :二者出现本质鸿沟:
-
- Express:异步会打乱中间件执行顺序,回调嵌套、错误处理繁琐;
- Koa:依靠
await next()固定洋葱执行流,异步代码平铺书写,错误统一捕获;
- 核心根源:
Express 中间件只是普通回调函数,无 Promise 封装;
Koa 通过compose将所有中间件包装成 Promise 调用链,让异步流程可控。