第五章:Express 进阶:中间件的力量
第五章:Express 进阶:中间件的力量
Prorise第五章:Express 进阶:中间件的力量
摘要: 在上一章,我们学会了使用 Express 定义独立的路由。然而,一个真实的应用程序,需要在多个路由间共享通用的逻辑,例如:记录每个请求、验证用户身份、解析请求数据等。如果在每个路由处理器中重复编写这些代码,将是一场灾难。本章,我们将深入 Express 的核心设计哲学——中间件 (Middleware)。您将理解中间件是如何构成一个请求处理的“流水线”,学习如何使用内置、第三方及自定义中间件来优雅地组织和复用代码。这是从“会用 Express”到“精通 Express”最关键的一步,也是理解未来 Next.js 等框架中类似概念的基石。
在本章,我们将彻底解构 Express 的“魔法”核心:
- 首先,我们将深入理解 中间件的核心概念,建立一个清晰的“请求-响应管道”心智模型。
- 接着,我们将系统学习 中间件的分类与加载顺序,精确控制代码的执行流。
- 然后,我们将实战演练最常用的 内置及第三方中间件,并提供完整的
curl
命令来验证其效果。 - 最后,我们将亲手 编写自定义中间件,实现一个请求日志记录器和一个简单的 API 密钥认证,将理论知识转化为实践能力。
5.1. 中间件的核心概念:请求处理的“流水线”
深度讲解:
我们可以将 Express 的请求处理过程想象成一条 工业流水线。当一个 HTTP 请求(原材料)进入 Express 应用时,它会被放到这条流水线上。流水线由一系列的 中间件(工人/工站)组成。
每个中间件都是一个函数,它接收三个参数:req
(请求对象)、res
(响应对象) 和 next
(一个回调函数)。
在这条流水线上,每个中间件(工人)都可以做三件事:
- 执行任何代码: 它可以读取或修改
req
和res
对象。例如,记录请求信息,或者给req
对象附加一些额外的数据(如用户信息)。 - 结束请求-响应周期: 它可以直接处理完这个请求,并向客户端发送响应(如
res.json(...)
)。这相当于一个工人完成了所有工序,直接将成品打包出厂。 - 调用
next()
将控制权传递给下一个中间件: 这是最常见的行为。它表示“我的工序处理完了,交给流水线上的下一个工人继续处理”。如果一个中间件既没有发送响应,也没有调用next()
,那么这个请求就会被“挂起”,客户端将永远等不到响应。
这个“流水线”模型,就是 Express 框架强大扩展性的根源。
5.2. 中间件的分类与加载顺序
深度讲解:
中间件的强大之处不仅在于其功能,还在于其灵活的加载方式。我们可以精确地控制一个中间件是全局生效,还是只对特定路由生效。而这一切的关键,在于 加载顺序。
5.2.1. 中间件的分类
- 应用级中间件: 通过
app.use()
或app.METHOD()
绑定到app
对象上。这是最常见的类型,通常用于全局配置。 - 路由级中间件: 绑定到
express.Router()
实例上。当我们使用express.Router
模块化路由时会用到(第六章将详述)。 - 错误处理中间件: 这是唯一一种有 4 个参数
(err, req, res, next)
的特殊中间件。它用于集中捕获和处理路由中发生的错误。 - 内置中间件: Express 官方提供的中间件,如
express.json()
、express.urlencoded()
。 - 第三方中间件: 由社区开发,通过
npm
安装的中间件,如cors
、morgan
。
5.2.2. 加载顺序实战验证
让我们通过一个例子,直观地感受加载顺序的重要性。
order-test.js
:
1 | import express from "express"; |
验证环节
打开两个终端。一个运行 node order-test.js
,另一个用来发送 curl
请求。
场景一:请求 /public
1 | curl http://localhost:3000/public |
服务器终端输出:
1 | [2025-09-14T01:30:15.123Z] 1. 全局中间件 #1 - 记录请求 |
分析: 请求首先通过全局中间件 #1,然后直接匹配到了 /public
路由并结束,它永远不会经过全局中间件 #2。
场景二:请求 /private
1 | curl http://localhost:3000/private |
服务器终端输出:
1 | [2025-09-14T01:30:20.456Z] 1. 全局中间件 #1 - 记录请求 |
分析: 请求依次通过了全局中间件 #1 和 #2,最终到达 /private
路由。因此,在 /private
处理器中可以成功访问到由中间件 #2 附加的 req.user
。
5.3. 常用内置与第三方中间件
5.3.1. express.json()
- 解析 JSON 请求体
- 痛点: 如果没有它,
POST
,PUT
,PATCH
请求中Content-Type
为application/json
的请求体将无法被解析,req.body
将是undefined
。 - 安装: 内置,无需安装。
- 使用:
app.use(express.json());
json-test.js
:
1 | import express from 'express'; |
验证环节:
1 | curl -X POST -H "Content-Type: application/json" -d "{\"username\":\"awakening\"}" http://localhost:3000/users |
1
{"message":"用户 awakening 创建成功"}
服务器日志: 收到的请求体: { username: 'awakening' }
5.3.2. cors
- 处理跨域资源共享
- 痛点: 作为前端开发者,您对浏览器报的 CORS 错误再熟悉不过了。默认情况下,浏览器出于安全考虑,禁止脚本向不同源(域名、协议、端口)的服务器发起 HTTP 请求。
- 解决方案: 在服务器端使用
cors
中间件,它会自动添加必要的 HTTP 响应头(如Access-Control-Allow-Origin: *
),告诉浏览器:“我允许来自任何源的请求”。 - 安装:
npm install cors
- 使用:
import cors from 'cors'; app.use(cors());
cors-test.js
:
1 | import express from 'express'; |
验证环节:
我们可以用 curl -v
来查看响应头。
1 | curl -v http://localhost:3000/api/data |
1
2
3
4
5
6
7
8
9
< HTTP/1.1 200 OK
< X-Powered-By: Express
< Access-Control-Allow-Origin: * <-- CORS 中间件添加的关键响应头
< Content-Type: application/json; charset=utf-8
< Content-Length: 47
< ETag: W/"2f-xxxx"
< Date: Sun, 14 Sep 2025 01:45:00 GMT
< Connection: keep-alive
...
5.3.3. morgan
- HTTP 请求日志
- 痛点:
console.log
手动打印日志很原始,信息不全且格式不统一。我们需要一个专业的、生产级的请求日志记录器。 - 解决方案:
morgan
是一个广受欢迎的日志中间件,能以多种预设格式输出详细的请求日志。 - 安装:
npm install morgan
- 使用:
import morgan from 'morgan'; app.use(morgan('dev'));
morgan-test.js
:
1 | import express from 'express'; |
验证环节:
启动服务器后,用 curl
请求。
1 | curl http://localhost:3000/ |
服务器终端输出 (来自 morgan):
1
GET / 404 1.487 ms - 139
这行日志清晰地告诉我们:GET
请求 /
路径,响应状态码 404
,耗时 1.487
毫秒,响应体大小 139
字节。
5.4. 编写你的第一个自定义中间件
现在,让我们亲手编写两个中间件来巩固所学。
5.4.1. 场景一:请求时间日志器
1 | import express from "express"; |
5.4.2. 场景二:简单的 API 密钥认证
这是一个更实用的例子,它保护一个路由,只有提供了正确 API 密钥的请求才能访问。
1 | import express from "express"; |
验证环节:
请求一:没有提供 API 密钥
1 | curl http://localhost:3000/private/data |
1
{"error":"未经授权:无效的 API 密钥"}
请求二:提供了正确的 API 密钥
1 | curl -H "x-api-key: prorise-is-awesome" http://localhost:3000/private/data |
1
{"secret":"这是只有通过认证才能看到的核心数据"}
这个例子完美地展示了中间件作为“守卫”的强大能力。
5.5. 本章核心速查总结
分类 | 关键项 | 核心描述 |
---|---|---|
核心签名 | (req, res, next) | 标准中间件的函数签名。 |
(err, req, res, next) | 错误处理中间件的特殊签名,必须有 4 个参数。 | |
核心函数 | next() | 将控制权传递给流水线中的下一个中间件。 |
加载方式 | app.use(middleware) | 将中间件应用到所有后续的路由和中间件。顺序很重要。 |
app.get(path, middleware, ...) | 将中间件仅应用于特定的路由。 | |
常用中间件 | express.json() | (内置) 解析 application/json 格式的请求体。 |
cors() | (第三方) 轻松解决浏览器跨域请求问题。 | |
morgan('dev') | (第三方) 添加专业的、彩色的 HTTP 请求日志。 |
5.6. 高频面试题与陷阱
在 Express 中,中间件的加载顺序非常重要。你能举一个例子来说明,如果顺序错误会导致什么问题吗?
当然。一个最经典的例子就是 express.json() 和需要解析请求体的路由处理器的顺序。
假设我先定义了 app.post(‘/users’, …) 这个路由,在它的处理函数内部我尝试去读取 req.body。然后,在这段路由代码的后面,我才写 app.use(express.json())。
那么当一个 POST 请求发送到 /users 时,会发生什么?
当请求到达时,它会首先匹配到 /users 的路由处理器。在这个处理器内部,代码尝试访问 req.body,但因为负责解析 JSON 请求体的 express.json() 中间件还没有被执行,所以此时 req.body 还是 undefined。代码如果尝试从 undefined 上解构或读取属性,就会直接抛出一个 TypeError,导致程序出错。
非常准确。那正确的做法是什么?
正确的做法是始终将像 express.json()、cors、morgan 这类全局性的中间件,放在所有业务路由定义之前。这样就能确保,当请求到达任何一个具体的业务路由处理器时,日志记录、CORS 检查、请求体解析等所有“预处理”工作都已经完成了。
很好。这说明你深刻理解了 Express 的“流水线”模型。