学 Express 被 app.use 绕晕了?用流水线思维一次性搞懂 5 种中间件

刚学完 Node.js,听说 Express 很简单,结果被 app.userouter.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.bodyundefined?我明明在请求里带了 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 中间件的这段经历,我觉得最重要的三个收获是:

  1. 用流水线的比喻来理解 :每个中间件是一个工位,next() 是传送带。
  2. 五种类型其实是一回事:它们都是函数,只是挂载位置和作用范围不同。
  3. 顺序决定一切:先解析数据,再处理路由,最后处理错误。

如果你也在学 Express,我的建议是:别光看,一定要动手写 。照着最后的登录 Demo 自己敲一遍,把顺序调乱试试会发生什么,把 next() 删掉看看会发生什么------只有亲手踩过坑,才能真正理解。

Express 的中间件机制,本质上是一套非常优雅的设计。一旦你真正理解了它,你会发现 Koa、Redux 这些框架的中间件思想如出一辙,一通百通。

希望这篇文章能帮你少走一些弯路。

关于我:一个正在从零学后端的全栈方向学习者,欢迎在评论区交流你的学习心得。

相关推荐
BduL OWED1 小时前
将 vue3 项目打包后部署在 springboot 项目运行
java·spring boot·后端
二月龙1 小时前
从C++到WebAssembly:让高并发计算跑在浏览器里
后端
ZJY1322 小时前
3-12:路由和重构
后端·node.js
掘金者阿豪2 小时前
我用 Codex Rule 模式“驯服AI写代码”:从翻车到稳定上线的完整实践(附企业级规则模板 + 架构图)
后端
鱼人2 小时前
现代C++启示录:告别裸指针,你的代码里还有很多“C的幽灵”
后端
cylgdzz1112 小时前
DSP技术架构深度拆解
后端·架构
imuliuliang2 小时前
Spring Boot 多数据源解决方案:dynamic-datasource-spring-boot-starter 的奥秘(上)
java·spring boot·后端
霸道流氓气质2 小时前
SpringBoot+LangChain4j+Ollama实现Function Calling工具调用-仿智能客服示例
java·spring boot·后端
Rust研习社3 小时前
深入浅出 Rust 泛型:从入门到实战
开发语言·后端·算法·rust