Express 能十年不衰,靠的就是一个极简又强大的设计:中间件(middleware)。整个框架的核心,几乎可以浓缩成一句话 —— 把请求依次交给一串函数处理,每个函数决定要不要往下传。
理解了中间件,你就理解了 Express 的全部精髓。这篇我们从用法讲到原理,最后手写一个迷你版。
中间件就是一个三参数函数
一个最基础的 Express 中间件长这样:
function logger(req, res, next) {
console.log(`${req.method} ${req.url}`)
next() // 把控制权交给下一个中间件
}
app.use(logger)签名固定是 (req, res, next):
req:请求对象,可以读 header、query、bodyres:响应对象,可以res.json()、res.send()结束请求next:最关键的一个 —— 调用它,请求才会流向下一个中间件
next() 是整条链的开关
记住一条铁律:一个请求进来后,要么被某个中间件用 res.send() 之类的方法"终结",要么通过 next() 一路传下去。如果一个中间件既不响应、也不调用 next(),请求就会永远挂起,客户端一直转圈直到超时:
app.use((req, res, next) => {
console.log('我处理完了,但忘了调 next 也忘了响应')
// 💀 请求卡死在这里
})next() 还有两个变体:
next(err):传入一个参数,Express 会跳过所有普通中间件,直接找错误处理中间件next('route'):跳过当前路由的剩余处理,进入下一个匹配路由(较少用)
执行顺序:注册顺序就是执行顺序
中间件严格按 app.use / 路由的注册先后执行。这一点至关重要,顺序写错功能就废了。比如鉴权中间件必须在业务路由之前注册:
app.use(logger) // 1. 所有请求先记日志
app.use(express.json()) // 2. 解析 JSON body
app.use('/api', authCheck) // 3. /api 下的请求要鉴权
app.get('/api/posts', (req, res) => { // 4. 业务逻辑
res.json({ posts: [] })
})
app.use(errorHandler) // 5. 错误处理,放最后如果把 authCheck 写到业务路由后面,鉴权就形同虚设了。
中间件的三种作用域
按挂载方式,中间件分三类:
// 1. 全局中间件:每个请求都过
app.use(logger)
// 2. 路径中间件:只对匹配前缀的请求生效
app.use('/admin', adminAuth)
// 3. 路由级中间件:只对某个具体路由生效,可串多个
app.get('/profile', authCheck, loadUser, (req, res) => {
res.json(req.user)
})第三种很常用:把 authCheck、loadUser 这些可复用的逻辑串在路由定义里,职责清晰。
错误处理中间件:四个参数
Express 靠参数个数来区分普通中间件和错误处理中间件。错误处理中间件有四个参数,第一个是 err:
// 注意:必须是 4 个参数,少一个 Express 就当成普通中间件了
function errorHandler(err, req, res, next) {
console.error(err.stack)
res.status(err.status || 500).json({
message: err.message || '服务器内部错误',
})
}
app.use(errorHandler) // 一定放在所有路由的最后任何中间件里调用 next(err),或在同步代码里 throw,都会跳到这里:
app.get('/post/:id', (req, res, next) => {
const post = findPost(req.params.id)
if (!post) {
return next({ status: 404, message: '文章不存在' }) // 跳到错误处理
}
res.json(post)
})一个大坑:异步错误不会自动捕获
这是 Express 4 最经典的陷阱。异步函数里 throw 的错误,Express 抓不到,会变成未处理的 Promise rejection:
// ❌ 危险:async 里抛错,errorHandler 收不到,请求挂死
app.get('/user', async (req, res) => {
const user = await db.findUser() // 如果这里 reject...
res.json(user)
})两种解法:
// 方案一:手动 try/catch + next(err)
app.get('/user', async (req, res, next) => {
try {
const user = await db.findUser()
res.json(user)
} catch (err) {
next(err)
}
})
// 方案二:包一个高阶函数,自动 catch
const asyncHandler = (fn) => (req, res, next) =>
Promise.resolve(fn(req, res, next)).catch(next)
app.get('/user', asyncHandler(async (req, res) => {
const user = await db.findUser()
res.json(user)
}))顺带一提:Express 5 已经能自动捕获 async 中间件返回的 rejected Promise 了,这个历史包袱在新版本里被解决了。但用 Express 4 时务必小心。
手写一个迷你版理解原理
中间件的本质就是一个数组 + 递归的 next。下面这 30 行能帮你彻底看穿它:
class MiniApp {
constructor() {
this.stack = [] // 中间件队列
}
use(fn) {
this.stack.push(fn)
return this
}
handle(req, res) {
let index = 0
const next = (err) => {
const fn = this.stack[index++]
if (!fn) return // 队列走完了
if (err) {
// 出错时只找 4 个参数的错误处理中间件
if (fn.length === 4) fn(err, req, res, next)
else next(err) // 跳过普通中间件
} else {
// 正常情况跳过错误处理中间件
if (fn.length === 4) next()
else fn(req, res, next)
}
}
next() // 启动这条链
}
}看明白这段,你会发现所谓"中间件机制",不过是把函数塞进数组,用一个递归的 next 依次取出来执行。Express 的本体远没有想象中神秘。
小结
- 中间件是
(req, res, next)三参数函数,next()控制请求是否往下流 - 不响应也不
next(),请求会永久挂起 - 注册顺序 = 执行顺序,鉴权类中间件必须放在业务路由之前
- 错误处理中间件靠四个参数识别,放在最后
- Express 4 里 async 错误不会自动捕获,要 try/catch 或用 asyncHandler 包装
- 核心原理就是"数组 + 递归 next",并不神秘
把中间件这条责任链理顺,你不仅能用好 Express,迁移到 Koa、Nest 这些框架时也会发现:大家的设计思想一脉相承。