刚学完 Node.js,听说 Express 很简单,结果被
app.use、router.use、错误处理中间件绕晕了?我花了很多时间,才真正搞明白中间件这回事。这篇文章,把我学到的东西全部分享给你。
从一个被绕晕的新手说起
最近我开始学后端,跟着教程一步步来:先装 Express,写个 app.get('/') 返回 Hello World,一切都很顺利。
然后教程开始讲"中间件"。
app.use(logger)、app.use(express.json())、router.use(auth)......各种 use 满天飞,还有什么"路由级中间件""应用级中间件""内置中间件""第三方中间件""错误处理中间件"......我当时脑子里只有一个想法:这些到底有什么区别?为什么要分这么多类?
如果你也正被这些问题困扰,恭喜你,来对地方了。
1. 中间件到底是什么?------ 一条流水线
我在学习的过程中,发现用工厂流水线来理解中间件,非常形象。
一个 HTTP 请求从客户端发出来,就像一件产品进入了流水线。流水线上有很多工位,每个工位就是一个中间件,负责做一件特定的事情:
- 有的工位负责记录日志(把产品的信息记下来)。
- 有的工位负责解析原材料(把请求里的 JSON 数据解析成 JS 对象)。
- 有的工位负责检查权限(没有工牌的,直接扔出流水线)。
- 有的工位负责真正的生产(返回数据给客户端)。
中间件就是一个函数,它长这样:
javascript
function middleware(req, res, next) {
// 对请求做点什么
console.log(`${req.method} ${req.url}`)
// 做完了,交给下一个工位
next()
// 或者,发现问题了,直接终止流水线
// return res.status(403).send('禁止访问')
}
这里有三个核心参数:req(请求对象)、res(响应对象)、next(放行到下一个工位的开关)。
调用 next() 就相当于把产品推到下一个工位。不调用,流水线就停了。
Express 本质上就是一个"中间件集合",它会按照你写代码的顺序,依次执行每一个中间件。
2. 把五种中间件串成一条线
弄明白中间件的基本概念之后,我回头整理了自己学过的所有"中间件类型",发现它们其实就是按照作用范围从大到小排列的。
2.1 应用级中间件 ------ 管整个流水线的安检口
javascript
app.use((req, res, next) => {
console.log(`${req.method} ${req.url}`) // 任何请求都会经过这里
next()
})
app.use 注册的中间件是全局生效的。不管用户访问什么路径,只要请求进来,都会先经过这个中间件。
典型用途:日志记录、CORS 跨域配置、全局的 JSON 解析。
2.2 路由级中间件 ------ 管某个部门的门禁
当应用变大了,我们不可能把所有代码都写在一个 app.js 里。Express 提供了一个叫 Router 的东西,可以把路由按模块拆分:
javascript
const adminRouter = express.Router()
// 这个中间件只对 /admin 路径下的请求生效
adminRouter.use((req, res, next) => {
if (!req.headers.token) {
return res.status(403).send('请先登录')
}
next()
})
adminRouter.get('/dashboard', (req, res) => {
res.send('管理员面板')
})
// 把路由模块挂载到 /admin 路径
app.use('/admin', adminRouter)
Router 就像一个迷你版的 Express 应用 ,它有自己的中间件栈,但必须挂载到 app 上才能工作。
2.3 内置中间件 ------ 官方自带的标准化工位
有些活儿太常用了,Express 官方直接帮你写好了:
| 中间件 | 作用 |
|---|---|
express.json() |
解析 application/json 格式的请求体 |
express.urlencoded({ extended: false }) |
解析表单提交的数据 |
express.static('public') |
托管静态文件(HTML、图片、CSS 等) |
express.raw() |
解析原始二进制数据(v5 新增) |
express.text() |
解析纯文本请求体(v5 新增) |
💡 版本更新提醒 :Express v5 已于 2024 年 9 月正式发布,新增了
express.raw()和express.text()中间件,同时异步错误处理有了重大改进。
2.4 第三方中间件 ------ 社区造好的轮子
内置的不够用,社区有大量现成的轮子可以拿来就用:
bash
npm install cookie-parser cors morgan
javascript
const cookieParser = require('cookie-parser')
const cors = require('cors')
const morgan = require('morgan')
app.use(cookieParser()) // 解析 Cookie
app.use(cors()) // 跨域
app.use(morgan('dev')) // 专业的日志
2.5 错误处理中间件 ------ 流水线的保险闸
前面四种都是处理"正常流程"的。如果某个工位出了问题(比如数据库挂了),我们需要一个专门的工位来兜底,避免整个应用崩溃。
错误处理中间件和普通中间件唯一的区别就是参数数量:
javascript
// 普通中间件:3 个参数
app.use((req, res, next) => { ... })
// 错误处理中间件:4 个参数(多一个 err)
app.use((err, req, res, next) => {
console.error(err.stack)
res.status(500).json({ msg: '服务器开小差了' })
})
关键 :错误处理中间件必须放在所有 app.use 和路由的最后面,否则捕获不到错误。
3. 顺序!顺序!顺序!------ 我被坑了 2 小时才明白的事
我把上面五种中间件都学完了,心想"懂了懂了",于是开始动手写一个简单的登录功能。
结果,一个莫名其妙的 bug 让我调了整整 2 个小时。
我的代码大概是这样的:
javascript
// 注册路由
app.use('/api', userRouter)
// 解析 JSON
app.use(express.json())
// 日志中间件
app.use((req, res, next) => {
console.log(req.body) // undefined! 为什么?
next()
})
为什么 req.body 是 undefined?我明明在请求里带了 JSON 数据啊!
后来我才明白:Express 中间件是按照代码书写的顺序依次执行的。
因为我把 express.json() 放在了路由注册的后面 ,所以当请求进入 /api 路由时,JSON 还没被解析,req.body 自然是 undefined。
正确顺序应该是:
javascript
// 1. 最先:解析请求体的中间件
app.use(express.json())
app.use(express.urlencoded({ extended: false }))
// 2. 其次:第三方中间件
app.use(cookieParser())
app.use(cors())
// 3. 全局日志等
app.use((req, res, next) => {
console.log(`${req.method} ${req.url}`)
next()
})
// 4. 路由模块
app.use('/api', userRouter)
// 5. 404 处理
app.use((req, res) => {
res.status(404).send('页面不存在')
})
// 6. 最后:错误处理中间件
app.use((err, req, res, next) => {
res.status(500).json({ error: err.message })
})
中间件的顺序,决定了请求的处理流程。 这个坑我帮你们踩过了,希望你能一次写对。
4. 路由到底是不是中间件?
学到这里,我产生了一个疑问:app.get('/') 到底算不算中间件?
答案是:路由是一种特殊的中间件。
它和普通中间件的区别在于:
| 特性 | app.use (普通中间件) |
app.get/post (路由) |
|---|---|---|
| 匹配条件 | 只看路径前缀 | 必须路径和 HTTP 方法都匹配 |
| 是否终结请求 | 通常调用 next() 继续 |
通常直接 res.send() 返回 |
但在 Express 内部,路由方法本质上也是往中间件数组里推入一个函数,只不过它带上了"只匹配 GET 请求"的条件。
所以"路由"和"中间件"不是对立的概念,路由是中间件的一种特殊形式。
5. 一个完整的登录例子,把学到的全用上
光说不练假把式。我把学到的所有中间件知识,整合成一个完整的用户登录 Demo:
javascript
const express = require('express')
const cookieParser = require('cookie-parser')
const app = express()
// 1. 内置中间件:解析 JSON
app.use(express.json())
// 2. 第三方中间件:解析 Cookie
app.use(cookieParser())
// 3. 应用级中间件:全局日志
app.use((req, res, next) => {
console.log(`[${new Date().toISOString()}] ${req.method} ${req.url}`)
next()
})
// 4. 路由:登录接口
app.post('/login', (req, res) => {
const { username, password } = req.body
if (username === 'admin' && password === '123456') {
// 下发 Cookie,7 天有效
res.cookie('token', 'admin-token', {
httpOnly: true,
maxAge: 7 * 24 * 60 * 60 * 1000
})
res.json({ code: 0, msg: '登录成功' })
} else {
res.status(401).json({ code: 1, msg: '用户名或密码错误' })
}
})
// 创建路由模块
const userRouter = express.Router()
// 5. 路由级中间件:认证检查(只对 /api/user 生效)
userRouter.use((req, res, next) => {
if (!req.cookies.token) {
return res.status(401).json({ code: 1, msg: '请先登录' })
}
next()
})
userRouter.get('/info', (req, res) => {
res.json({ username: 'admin', role: 'superadmin' })
})
app.use('/api/user', userRouter)
// 6. 404 处理
app.use((req, res) => {
res.status(404).json({ msg: '接口不存在' })
})
// 7. 错误处理中间件
app.use((err, req, res, next) => {
console.error('服务器错误:', err.stack)
res.status(500).json({ msg: '服务器内部错误' })
})
app.listen(3000, () => console.log('服务启动: http://localhost:3000'))
这个 Demo 里,五种中间件全部用上了:
- 内置中间件 :
express.json() - 第三方中间件 :
cookie-parser - 应用级中间件:全局日志
- 路由级中间件 :
userRouter.use认证检查 - 错误处理中间件:最后的 500 兜底
6. Express v5 带来了什么变化?
在我写这篇文章的时候(2026 年 4 月),Express v5 已经发布半年多了,有几个变化值得你从一开始就知道:
6.1 异步错误处理更简单了
v4 时代,异步中间件里如果 Promise 被拒绝,你必须手动 catch 然后调用 next(err)。v5 自动帮你处理了:
javascript
// v4: 必须 try-catch 或 .catch
app.get('/users', async (req, res, next) => {
try {
const users = await db.query('SELECT * FROM users')
res.json(users)
} catch (err) {
next(err) // 必须手动传递
}
})
// v5: 自动捕获,可以直接写
app.get('/users', async (req, res) => {
const users = await db.query('SELECT * FROM users')
res.json(users)
// 如果出错,自动传给错误处理中间件
})
这个改进让代码干净了不少。
6.2 路由通配符语法变了
v4 里你可能见过 /* 这种写法来匹配任意路径。v5 里必须给通配符起个名字:
javascript
// v4 写法(v5 不兼容)
app.get('/*', handler)
// v5 写法
app.get('/*splat', handler) // 或者
app.get('/{*splat}', handler)
如果你正在学 Express,建议直接学 v5 的语法,避免以后迁移麻烦。
6.3 新增了两个内置中间件
express.raw() 和 express.text() 是 v5 新增的,用于解析原始二进制数据和纯文本请求体。
7. 写在最后:一个新手给新手的建议
回顾我学习 Express 中间件的这段经历,我觉得最重要的三个收获是:
- 用流水线的比喻来理解 :每个中间件是一个工位,
next()是传送带。 - 五种类型其实是一回事:它们都是函数,只是挂载位置和作用范围不同。
- 顺序决定一切:先解析数据,再处理路由,最后处理错误。
如果你也在学 Express,我的建议是:别光看,一定要动手写 。照着最后的登录 Demo 自己敲一遍,把顺序调乱试试会发生什么,把 next() 删掉看看会发生什么------只有亲手踩过坑,才能真正理解。
Express 的中间件机制,本质上是一套非常优雅的设计。一旦你真正理解了它,你会发现 Koa、Redux 这些框架的中间件思想如出一辙,一通百通。
希望这篇文章能帮你少走一些弯路。
关于我:一个正在从零学后端的全栈方向学习者,欢迎在评论区交流你的学习心得。