Skip to content
本章作为系列的收尾,针对中间件顺序、路由匹配优先级、CORS 配置和静态文件服务中容易出错的问题,给出最小复现示例和修正方法。随后以知识地图形式整理前六章的核心收获,并列出后续学习方向。
中间件顺序
Express 中间件的执行顺序由 app.use() / router.use() 或路由方法(app.get() 等)的注册顺序决定。请求进入后从第一个匹配的中间件开始,沿链路逐个执行,直到某个处理函数发送响应或调用 next()。如果某个中间件既不调用 next() 也不发送响应,请求会一直挂起。错误处理中间件(四参数)必须注册在所有普通中间件和路由之后,否则无法捕获通过 next(err) 传递的异步错误。全局中间件与路由中间件的先后位置也直接影响业务逻辑是否被拦截。
示例:全局、路由与错误处理中间件
下面的脚本同时注册全局中间件、路由中间件和错误处理中间件,观察执行过程。
typescript
import express from 'express';
const app = express();
// 全局中间件:每次请求都会经过
app.use((req, res, next) => {
console.log('[全局] 记录时间: ', new Date().toISOString());
next(); // 继续到下一个中间件或路由
});
// 路由中间件
app.get('/hello', (req, res, next) => {
console.log('[路由] GET /hello 处理中');
// 模拟异步操作中出错
setImmediate(() => {
next(new Error('故意抛出的错误'));
});
});
// 普通中间件,位于路由之后
app.use((req, res, next) => {
console.log('[中间件] 路由之后');
next();
});
// 错误处理中间件(四参数)
app.use((err: Error, req: express.Request, res: express.Response, next: express.NextFunction) => {
console.error('[错误处理] 捕获:', err.message);
res.status(500).json({ error: err.message });
});
app.listen(3001, () => {
console.log('服务运行在 3001 端口');
});访问 http://localhost:3001/hello,终端输出如下:
[全局] 记录时间: 2025-03-15T08:20:00.123Z
[路由] GET /hello 处理中
[错误处理] 捕获: 故意抛出的错误关键点:
- 全局中间件最先执行,路由匹配后进入
GET /hello处理函数,next(err)会跳过所有非错误处理中间件,直接进入错误处理中间件。 [中间件] 路由之后没有被执行,因为next(err)不会将其视为目标。- 若错误处理中间件没有注册在所有
app.use()的最后,则无法捕获这里的异步错误。错误处理中间件的注册位置必须是所有普通中间件和路由之后。
另一个常见错误是,在某个中间件中既不调用 next() 也不发送响应,导致请求挂起:
typescript
app.use((req, res, next) => {
console.log('这个中间件没有 next 也没有 res.send');
// 缺少 next() 或 res.send* → 请求挂起
});对于该路径,浏览器会一直处于等待状态,直到超时。
路由匹配:声明顺序与匹配结果
Express 按照代码中书写的顺序进行路由匹配。一旦某个路由方法匹配了 HTTP 方法和路径,就会执行对应的处理函数并结束响应(除非内部显式调用了 next())。这意味着同路径不同方法的处理函数不会相互干扰,但动态路由和具体路径的声明顺序至关重要。
同路径不同方法
下面的代码对同一个路径 /items 分别注册 GET 和 POST 处理器。
typescript
app.get('/items', (req, res) => {
res.json({ method: 'GET' });
});
app.post('/items', (req, res) => {
res.json({ method: 'POST' });
});GET /items 只触发第一个路由,POST /items 只触发第二个,彼此独立。因为方法不同,顺序可以交换而不影响结果。
动态路由与静态路径冲突
动态路由使用 :param 捕获任何路径片段,包括具体的路径名。如果把动态路由写在具体路径之前,就会错误地捕获本应由具体路径处理的请求。
错误的顺序:
typescript
app.get('/users/:id', (req, res) => {
res.json({ type: 'dynamic', id: req.params.id });
});
app.get('/users/me', (req, res) => {
res.json({ type: 'specific', name: 'alice' });
});当客户端请求 GET /users/me 时,第一个路由会匹配,此时 req.params.id 的值为 "me",第二个路由永远不会被触发。
正确的做法是把 /users/me 写在动态路由之前:
typescript
app.get('/users/me', (req, res) => {
res.json({ type: 'specific', name: 'alice' });
});
app.get('/users/:id', (req, res) => {
res.json({ type: 'dynamic', id: req.params.id });
});这样 /users/me 会先被匹配,/users/123 则进入动态路由。路由的匹配还遵循前缀匹配规则:app.use('/prefix', ...) 中间件会匹配 /prefix 以及 /prefix/anything,而路由方法(app.get() 等)要求路径完全匹配或符合 ? + * 等正则模式。动态路由的声明顺序不仅影响精确匹配,也影响 app.use 的前缀匹配范围。
CORS:跨域请求的处理方法
前后端分离开发时,浏览器从一个域(例如 http://localhost:5173)向另一个域(http://localhost:3001)发起请求,会触发浏览器的同源策略限制。如果服务器没有返回正确的 CORS 头,请求会被浏览器拦截,前端代码无法读取响应。
未配置 CORS 时的报错
一个不添加 CORS 中间件的 API 服务器:
typescript
import express from 'express';
const app = express();
app.get('/data', (req, res) => {
res.json({ message: 'hello' });
});
app.listen(3001, () => console.log('API on 3001'));在前端页面(运行在 http://localhost:5173)发起 fetch:
html
<script>
fetch('http://localhost:3001/data')
.then(res => res.json())
.then(data => console.log(data))
.catch(err => console.error('请求失败', err));
</script>浏览器控制台会输出类似错误:
Access to fetch at 'http://localhost:3001/data' from origin 'http://localhost:5173' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.此时后端实际已正常返回响应,网络面板也可以看到状态码 200,但浏览器出于安全策略拒绝了响应体。
使用 cors 中间件解决
安装 cors 包:
bash
npm install cors添加中间件:
typescript
import cors from 'cors';
// 允许所有来源(开发常用,但无法携带凭据)
app.use(cors());
// 或者仅允许特定源
app.use(cors({ origin: 'http://localhost:5173' }));重新启动服务后,前端的 fetch 可正常获取数据,响应头中会包含 Access-Control-Allow-Origin: *(或指定的源)。
对于复杂请求(如 PUT、DELETE 或带有自定义头),浏览器会先发送 OPTIONS 预检请求。cors 中间件默认会处理预检并返回适当的响应头,如 Access-Control-Allow-Methods 等。如果需要跨域携带 Cookie,则需设置 credentials: true 并指明具体的 origin(不能使用 *)。
静态文件:路径映射与安全注意
express.static 是 Express 内置的专门用于提供文件服务的中间件。一般用法为 app.use(express.static('public')),会将 public 目录下的文件对外暴露到根路径,例如 public/style.css 可通过 GET /style.css 访问。
路由前缀、多目录挂载
给静态资源统一加上前缀是常见的做法,以避免与 API 路由冲突:
typescript
app.use('/static', express.static('public'));此时 public/logo.png 映射为 /static/logo.png。
也可以挂载多个静态目录,它们按注册顺序依次查找文件:对于每个请求,Express 会依次尝试每个静态中间件,如果文件存在则立即响应,否则调用 next() 进入下一个静态中间件或路由。
typescript
app.use('/assets', express.static('public'));
app.use('/assets', express.static('files'));请求 /assets/photo.jpg 会先在 public 目录查找,找不到再去 files 目录查找。
安全注意事项
- 点文件:
express.static默认忽略以.开头的隐藏文件(如.env、.gitignore),不会对外暴露。可通过dotfiles选项改变这一行为,但通常不建议。 - 目录泄漏:
express.static在访问目录路径且存在index.html时,会自动返回该文件,而不会列出目录内容。若没有index.html,默认返回 404,除非显式开启了index选项或直接暴露目录。 - 敏感文件:不要将包含密钥、配置或
.git目录的路径作为静态目录暴露。express.static对路径遍历攻击有内建防护,不会解析包含..等路径片段的请求,因此无法通过/../etc/passwd之类的方式跳出静态根目录。 - 与路由的优先级:
express.static也是中间件,如果放在路由定义之前,静态文件请求会先于路由处理,可能拦截对路径的 API 调用。通常会把静态文件中间件注册在app.use(cors())之后、路由处理之前,或使用路径前缀来隔离。
系列知识地图:核心知识点回顾
本系列从零开始讲解了 Express.js 的关键机制与实战用法。以下是对七篇文章核心内容的提炼。
| 篇序 | 主题 | 核心收获 |
|---|---|---|
| 1 | Express.js 概述 | 与原生 http 模块对比,通过最少代码理解 Express 如何简化请求处理与响应发送。 |
| 2 | 基本概念:中间件与路由 | 请求-响应循环、app.use 与路由方法的匹配规则、next() 的流转控制。 |
| 3 | 请求与响应对象 | req.query、req.params、req.body、响应方法 res.send() / res.json() 的行为和自动类型处理。 |
| 4 | 内置中间件与 Router | express.json() / urlencoded() / static() 的用途与限制;Router 的模块化挂载与路径前缀。 |
| 5 | 错误处理与异步 | 同步错误自动捕获、四参数错误处理中间件、next(err) 的传播、异步处理函数的封装(asyncHandler)和 404 兜底。 |
| 6 | 综合示例:构建 REST API | 从项目结构到路由模块化,实现完整的待办事项 CRUD 接口,串联请求解析、中间件与错误处理。 |
| 7 | 常见问题与总结 | 中间件顺序、路由匹配优先级、CORS 配置、静态文件挂载与安全要点,以及后续学习方向。 |
7 篇文章覆盖了从入门到能够独立编写具有一定健壮性的 API 所需要的核心知识。遇到问题时,可以回到对应的文章查找最小复现场景和理解行为边界。
后续学习路径:从 Express 到全栈
掌握 Express 之后,围绕它建立一整套后端开发能力的方向大致包括:
- 数据库集成:MongoDB 配合 Mongoose,或 SQL(PostgreSQL / MySQL)配合 Sequelize / Prisma。学习点包括连接管理、ORM 建模、迁移脚本。
- 认证与授权:JWT 认证、session 中间件(
express-session)、Passport.js 等第三方策略库。 - TypeScript 支持:在 Express 类型声明中使用
Request,Response,NextFunction泛型;编写强类型路由与中间件;结合ts-node或tsx运行。 - 测试:使用
mocha或jest搭配supertest进行集成测试;了解单元测试中间件与路由的隔离技巧。 - 性能与限流:集成
rate-limit、compression等中间件,理解内存泄漏、事件循环阻塞对服务器的影响。 - 框架探索:Koa 的洋葱模型与 async/await 原生支持,Fastify 的 schema 序列化与高性能,NestJS 的依赖注入与模块化架构。
- 部署与容器化:通过 Docker 容器化应用,了解进程管理(PM2)和反向代理(Nginx)的基础角色。
这些方向不需要一次性掌握,可根据实际应用需求逐步深入。对于侧重与前端配合的开发者,CORS、静态文件服务、RESTful 接口约定和错误格式标准化通常是日常工作中反复接触的知识点。
参考链接
- [1] https://expressjs.com/en/guide/using-middleware.html
- [3] https://expressjs.com/en/guide/routing.html
- [5] https://expressjs.com/en/guide/error-handling.html
- [6] https://expressjs.com/en/resources/middleware/cors.html
- [7] https://github.com/expressjs/cors
- [8] https://expressjs.com/en/starter/static-files.html
- [10] https://expressjs.com/en/4x/api.html#express.static
- [11] https://expressjs.com/en/4x/api.html
- [12] https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS
