在 Express 中,中间件是处理请求的核心机制。一个 Express 应用本质上就是一层层中间件的串联,请求依次流经每个中间件,直到某个中间件发送响应或调用 next() 将控制权移交给下一个。Node.js 生态为最常见的后端需求提供了大量成熟的中间件,它们安装简单、配置直观,几乎每个 Web 服务都会用到。以下是最常用的四类。
1. 静态资源中间件 express.static
任何一个 Web 应用都需要向外提供静态文件——HTML、CSS、JS、图片、字体等。如果手写路由返回文件,需要自行处理 MIME 类型、缓存头、路径安全问题,容易出错且不必要。
Express 内置的 express.static 中间件专门解决这个问题。只需指定一个本地文件夹路径,它就能自动处理所有对静态资源的请求,并根据文件扩展名设置正确的 Content-Type,同时利用 Last-Modified 和 ETag 实现基础的协商缓存。
使用示例:
const express = require('express');
const app = express();
// 将 public 文件夹下的所有文件作为静态资源暴露
app.use(express.static('public'));
// 也可以指定虚拟路径前缀
app.use('/static', express.static('public'));
真实使用场景:
- 前端构建后的产物(React/Vue 打包后的
dist目录)可直接放在public中,由 Express 作为静态文件服务器提供服务。 - 允许用户通过 URL
/images/logo.png直接访问服务器上的图片资源。
生产环境的注意点:
- 不要暴露敏感目录(如
node_modules、.env)。 - 在生产环境中,通常会前置 Nginx 来处理静态文件,但开发环境和低流量场景下直接用
express.static非常方便。 - 可以传入一个选项对象来设置
maxAge(强缓存时间),例如express.static('public', { maxAge: '1d' })来减少请求数。
2. 请求解析中间件 express.json 与 express.urlencoded
HTTP 请求体中的数据并不会自动解析为 JavaScript 对象。曾经需要引入 body-parser 这个第三方包,但从 Express 4.16 开始,Express 内置了两个解析中间件:
express.json()—— 解析Content-Type: application/json的请求体,结果挂载到req.body。express.urlencoded({ extended: true })—— 解析表单提交(Content-Type: application/x-www-form-urlencoded),extended: true允许使用qs库解析嵌套对象。
必须手动挂载,因为并非所有路由都需要解析请求体,Express 将选择权留给开发者。
使用示例:
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.post('/api/login', (req, res) => {
console.log(req.body.username);
console.log(req.body.password);
res.send('ok');
});
常见配置问题:
- 忘记挂载这两个中间件导致
req.body为undefined是新手最常遇到的坑。 express.urlencoded主要用于兼容传统表单提交(如<form>的 POST),现代 SPA 应用通常只传 JSON,但仍建议保留它以兼容某些第三方回调或旧版客户端。- 如果接口需要接收原始二进制数据(如文件上传),则需要使用
multer等专用中间件,不能用express.raw()草率处理。
3. 跨域中间件 cors
浏览器出于安全考虑,会阻止前端 JavaScript 从不同源(协议、域名、端口任一不同)发起的 HTTP 请求,这就是同源策略。后端需要在响应头中明确允许跨域,浏览器才会放行。
手动设置响应头虽然可以做到(例如 res.setHeader('Access-Control-Allow-Origin', '*')),但面对预检请求(OPTIONS)、自定义请求头、Cookie 凭证等场景时,代码会变得繁琐且容易遗漏。cors 中间件封装了所有跨域相关的逻辑,只需几行代码即可安全地配置 CORS 策略。
安装:
npm install cors
使用示例:
const cors = require('cors');
// 简单粗暴:允许所有来源跨域
app.use(cors());
// 生产环境应指定具体来源
app.use(cors({
origin: 'https://www.example.com',
methods: ['GET', 'POST'],
credentials: true // 允许携带 Cookie
}));
真实开发注意事项:
- 开发环境下可以开启全放开的
cors(),方便前后端联调;但上线前务必限制origin为可信域名,避免被恶意站点利用。 - 如果使用了 JWT 鉴权并将 token 存放在
Authorization头,需要在allowedHeaders中加入该字段,否则预检请求会被拒绝。 cors中间件默认会处理预检请求(OPTIONS),无需额外编写路由。
4. 日志中间件 morgan
在生产环境中,追踪每个请求的来源、时长、状态码是排查问题的基础。morgan 是一款轻量级的 HTTP 请求日志记录中间件,它提供多种预定义格式,也支持自定义日志格式。
安装:
npm install morgan
使用示例:
const morgan = require('morgan');
// 使用预定义的标准格式
app.use(morgan('dev'));
// 输出示例:GET /api/users 200 12.345 ms - 1234
// 线上使用 combined 格式,包含更多信息(IP、Referer、User-Agent)
app.use(morgan('combined'));
与日志框架集成:
morgan 默认将日志输出到控制台(stdout),在实际项目中通常会和 winston 或 pino 这类日志框架结合,以便将日志写入文件、发送到集中式日志平台。
const logger = require('./logger'); // 自定义 winston 实例
app.use(morgan('combined', {
stream: { write: message => logger.info(message.trim()) }
}));
实用技巧:
- 开发环境用
dev格式,高亮不同状态码且简洁;线上用combined或自定义 token 记录请求处理时间、用户 ID 等业务信息。 - 避免在日志中记录敏感数据(如密码、token),可以在自定义 token 时排除。
- 日志中间件应放在路由之前,这样才能记录所有请求;但如果想要精确记录响应时间,需要注意它与错误处理中间件的顺序。
中间件的串联顺序
因为这四类中间件在任何 Web 服务中几乎都会同时使用,这里给出一个典型的 Express 项目入口文件顺序作为参考:
const express = require('express');
const cors = require('cors');
const morgan = require('morgan');
const app = express();
// 1. 日志中间件(最先注册,记录所有请求)
app.use(morgan('dev'));
// 2. 跨域中间件(尽早处理预检请求)
app.use(cors({ origin: 'http://localhost:3000' }));
// 3. 请求体解析中间件
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
// 4. 静态资源中间件
app.use('/static', express.static('public'));
// 5. 路由
app.use('/api', require('./routes'));
// 6. 错误处理中间件(最后捕获异常)
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).send('服务器内部错误');
});
app.listen(3000);
这个顺序遵循了一个原则:通用性高、需要尽早处理的任务放在前面,具体业务路由放在后面,错误处理放在最后。 日志、跨域、请求体解析几乎每个请求都需要,因此理应前置;静态资源通常也需要在路由匹配前响应,避免被路由拦截。
掌握这四类中间件,基本就能搭建出一个健壮的 Express 服务骨架。在实际项目中,这些也是最先被引入和配置的基础设施,之后的业务逻辑都建立在这一层之上。