中间件
中间件是 Express.js 最核心的设计理念。理解中间件机制,是掌握 Express 开发的关键。本章将深入讲解中间件的原理、类型和使用方法。
什么是中间件?
中间件(Middleware)是一个函数,它可以访问请求对象(req)、响应对象(res)和应用程序请求-响应循环中的下一个中间件函数。这个名字来源于它在请求和响应之间起到"中间人"的作用。
中间件函数的签名
function middleware(req, res, next) {
// req: 请求对象,包含请求数据
// res: 响应对象,用于发送响应
// next: 调用后传递控制权给下一个中间件
}
中间件函数可以执行以下任务:
- 执行任何代码:日志记录、数据验证、权限检查等
- 修改请求和响应对象:添加属性、设置头信息等
- 结束请求-响应循环:发送响应给客户端
- 调用下一个中间件:通过
next()传递控制权
中间件的核心规则
如果一个中间件没有结束请求-响应循环(比如调用 res.send() 或 res.json()),它必须调用 next() 将控制权传递给下一个中间件。否则,请求会被挂起,客户端将永远收不到响应。
// 错误示例:忘记调用 next()
app.use((req, res, next) => {
console.log('请求到达');
// 没有 next(),请求被挂起
});
// 正确示例
app.use((req, res, next) => {
console.log('请求到达');
next(); // 传递控制权
});
// 也可以直接结束响应
app.use((req, res, next) => {
res.json({ message: '请求已处理' });
// 不需要 next(),因为已经结束了响应
});
中间件执行流程
Express 中间件的执行遵循"洋葱模型",请求依次经过每个中间件,响应则反向返回。
执行顺序的重要性
中间件的加载顺序非常重要:先加载的中间件先执行。如果中间件 A 在路由处理器之后加载,那么请求永远不会到达 A,因为路由处理器已经结束了请求-响应循环。
// 正确的顺序
app.use(logger); // 1. 先执行日志
app.use(express.json()); // 2. 再解析请求体
app.get('/', handler); // 3. 最后处理路由
// 错误的顺序
app.get('/', handler); // 路由处理器结束了响应
app.use(logger); // 永远不会执行
中间件的类型
Express 中间件主要分为以下几类:
1. 应用级中间件
使用 app.use() 或 app.METHOD() 绑定到 app 实例的中间件。
const express = require('express');
const app = express();
// 匹配所有请求
app.use((req, res, next) => {
console.log('每个请求都会经过这里');
next();
});
// 只匹配 /api 路径开头的请求
app.use('/api', (req, res, next) => {
console.log('API 请求');
next();
});
// 只匹配特定 HTTP 方法
app.get('/users', (req, res, next) => {
console.log('GET /users');
next();
}, (req, res) => {
res.json({ users: [] });
});
2. 路由级中间件
使用 express.Router() 创建的路由器实例上的中间件。
const express = require('express');
const router = express.Router();
// 路由级中间件
router.use((req, res, next) => {
console.log('这个路由器的所有请求都会经过');
next();
});
// 只对当前路由器生效
router.get('/list', (req, res) => {
res.json({ list: [] });
});
module.exports = router;
3. 错误处理中间件
错误处理中间件有四个参数,这是区分普通中间件和错误处理中间件的关键。
// 错误处理中间件必须有四个参数
app.use((err, req, res, next) => {
console.error('错误:', err.message);
res.status(500).json({ error: '服务器内部错误' });
});
当中间件或路由处理器抛出错误,或者调用 next(err) 时,Express 会跳过所有普通中间件,直接进入错误处理中间件。
4. 内置中间件
Express 内置了多个请求体解析中间件和静态文件服务中间件:
// 解析 JSON 请求体(Content-Type: application/json)
app.use(express.json());
// 解析 URL 编码的请求体(Content-Type: application/x-www-form-urlencoded)
app.use(express.urlencoded({ extended: true }));
// 解析原始 Buffer 请求体(Content-Type: application/octet-stream)
app.use(express.raw());
// 解析文本请求体(Content-Type: text/plain)
app.use(express.text());
// 提供静态文件服务
app.use(express.static('public'));
express.json()
解析 JSON 格式的请求体,是最常用的中间件。没有这个中间件,req.body 会是 undefined。
app.use(express.json());
app.post('/data', (req, res) => {
console.log(req.body); // { name: 'test', value: 123 }
res.send('收到');
});
常用配置选项:
app.use(express.json({
limit: '10mb', // 请求体大小限制
strict: true, // 只接受对象和数组
type: 'application/json' // 接受的 Content-Type
}));
express.urlencoded()
解析 URL 编码的请求体,常用于处理表单提交。
// extended: false 使用 querystring 库(默认)
// extended: true 使用 qs 库(支持嵌套对象)
app.use(express.urlencoded({ extended: true }));
app.post('/form', (req, res) => {
console.log(req.body); // { username: 'john', password: 'secret' }
res.send('表单已提交');
});
express.raw()
将请求体解析为 Buffer 对象,适用于需要处理二进制数据的场景,如文件上传、Webhook 回调等。
app.use(express.raw({
type: 'application/octet-stream',
limit: '50mb'
}));
app.post('/binary', (req, res) => {
// req.body 是 Buffer 对象
console.log(req.body); // <Buffer ...>
console.log(req.body.length); // 字节数
res.send('二进制数据已接收');
});
// 常见用途:处理 GitHub Webhook 签名验证
app.post('/webhook',
express.raw({ type: 'application/json' }),
(req, res) => {
const signature = req.get('X-Hub-Signature-256');
// 使用原始 Buffer 验证签名
const expectedSig = 'sha256=' + crypto
.createHmac('sha256', SECRET)
.update(req.body)
.digest('hex');
if (signature === expectedSig) {
// 验证通过,解析 JSON
const payload = JSON.parse(req.body);
res.json({ received: true });
} else {
res.status(401).send('Invalid signature');
}
}
);
express.text()
将请求体解析为字符串,适用于处理纯文本数据。
app.use(express.text({ type: 'text/plain' }));
app.post('/text', (req, res) => {
console.log(req.body); // '这是一段文本内容'
res.send('文本已接收');
});
// 指定字符编码
app.use(express.text({
defaultCharset: 'utf-8',
type: ['text/plain', 'text/html']
}));
内置中间件对比
| 中间件 | 解析类型 | req.body 类型 | 常用场景 |
|---|---|---|---|
express.json() | JSON | Object | API 请求 |
express.urlencoded() | URL 编码 | Object | 表单提交 |
express.raw() | 原始数据 | Buffer | 二进制、Webhook |
express.text() | 纯文本 | String | 文本处理 |
express.static() | 静态文件 | - | 静态资源服务 |
重要提示:这些中间件都基于 body-parser 库,请求体数据来自用户输入,所有值都不可信,使用前应该验证。在 Express 5 中,如果请求没有请求体、Content-Type 不匹配或解析出错,req.body 会是 undefined。
5. 第三方中间件
社区提供了大量第三方中间件,可以直接安装使用:
npm install morgan helmet cors cookie-parser
const morgan = require('morgan'); // HTTP 请求日志
const helmet = require('helmet'); // 安全头设置
const cors = require('cors'); // 跨域处理
const cookieParser = require('cookie-parser'); // Cookie 解析
app.use(morgan('dev')); // 开发模式日志
app.use(helmet()); // 设置安全相关的 HTTP 头
app.use(cors()); // 允许跨域
app.use(cookieParser()); // 解析 Cookie 到 req.cookies
next() 函数详解
next() 函数是中间件机制的核心,它决定了请求的流向。
基本用法
app.use((req, res, next) => {
console.log('第一个中间件');
next(); // 调用下一个中间件
});
app.use((req, res, next) => {
console.log('第二个中间件');
next(); // 继续传递
});
app.get('/', (req, res) => {
console.log('路由处理器');
res.send('Hello');
});
// 控制台输出:
// 第一个中间件
// 第二个中间件
// 路由处理器
next('route') 跳过当前路由
在路由中间件中,调用 next('route') 可以跳过当前路由的剩余处理函数,直接跳到下一个匹配的路由。
app.get('/users/:id',
(req, res, next) => {
// 如果 id 是 'special',跳过后续处理
if (req.params.id === 'special') {
return next('route');
}
next(); // 继续下一个处理函数
},
(req, res) => {
// 普通用户处理
res.send('普通用户: ' + req.params.id);
}
);
// 另一个匹配 /users/:id 的路由
app.get('/users/:id', (req, res) => {
// 特殊用户处理
res.send('特殊用户');
});
// GET /users/123 → "普通用户: 123"
// GET /users/special → "特殊用户"
这个机制非常适合实现前置条件检查。比如,只有验证通过才继续处理,否则跳过:
app.get('/admin',
(req, res, next) => {
if (!req.user || req.user.role !== 'admin') {
return next('route'); // 跳到下一个路由
}
next(); // 继续处理
},
(req, res) => {
res.send('管理员面板');
}
);
app.get('/admin', (req, res) => {
res.status(403).send('无权访问');
});
next('router') 跳出整个路由器
在路由器级中间件中,调用 next('router') 可以跳过整个路由器的剩余中间件,将控制权返回给父级。这个功能常用于权限验证失败时直接跳过整个路由模块。
const express = require('express');
const app = express();
const router = express.Router();
// 路由器级中间件:验证管理员权限
router.use((req, res, next) => {
if (!req.headers['x-admin-token']) {
return next('router'); // 没有 token,跳出整个路由器
}
// 验证 token...
next();
});
// 下面的路由只有在验证通过后才会执行
router.get('/users', (req, res) => {
res.json({ users: [] });
});
router.delete('/users/:id', (req, res) => {
res.json({ deleted: true });
});
// 挂载路由器,并添加兜底处理
app.use('/admin', router, (req, res) => {
// 如果路由器中间件调用了 next('router'),会执行这里
res.status(401).json({ error: '需要管理员权限' });
});
// 访问 /admin/users 时:
// - 有 x-admin-token 头:返回用户列表
// - 没有 x-admin-token 头:返回 401 错误
next('route') 与 next('router') 的区别:
| 特性 | next('route') | next('router') |
|---|---|---|
| 作用范围 | 跳过当前路由的剩余处理函数 | 跳出整个路由器 |
| 使用位置 | 只在 app.METHOD() 或 router.METHOD() 中有效 | 在路由器级中间件中有效 |
| 控制权流向 | 传递给下一个匹配的路由 | 传递给路由器挂载点后的中间件 |
next(err) 错误传递
向 next() 传递参数(除 'route' 和 'router' 字符串外),Express 会认为发生了错误,跳过所有普通中间件,直接进入错误处理中间件。
app.get('/user/:id', (req, res, next) => {
const id = req.params.id;
if (!/^\d+$/.test(id)) {
return next(new Error('ID 必须是数字')); // 传递错误
}
res.send('用户ID: ' + id);
});
// 错误处理中间件
app.use((err, req, res, next) => {
res.status(400).json({ error: err.message });
});
// GET /user/abc → { "error": "ID 必须是数字" }
// GET /user/123 → "用户ID: 123"
异步中间件
异步操作在现代 Web 应用中非常常见,如数据库查询、API 调用、文件操作等。Express 对异步错误处理的支持在不同版本中有显著差异。
Express 5:原生 Promise 支持
Express 5 最期待的特性之一就是原生 Promise 错误处理。当路由处理函数或中间件返回一个被拒绝(reject)的 Promise 时,Express 会自动将拒绝原因作为错误传递给错误处理中间件。
工作原理:
// Express 5 写法:简洁优雅
app.get('/users/:id', async (req, res) => {
// 如果 findById 抛出错误或 Promise 被 reject
// Express 会自动捕获并传递给错误处理中间件
const user = await User.findById(req.params.id);
if (!user) {
// throw 也会被自动捕获
throw new Error('用户不存在');
}
res.json(user);
});
// 错误处理中间件(统一处理所有错误)
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(err.status || 500).json({
error: process.env.NODE_ENV === 'production'
? '服务器错误'
: err.message
});
});
重要提示:Express 5 的 Promise 支持只针对错误处理。成功完成操作后,你仍然需要手动调用 next() 或发送响应:
// 中间件示例
app.use(async (req, res, next) => {
req.locals.user = await getUserFromToken(req);
next(); // 成功时必须调用
});
// 路由处理器示例
app.get('/profile', async (req, res) => {
const profile = await getProfile(req.user.id);
res.json(profile); // 发送响应
});
Express 4:手动错误传递
在 Express 4 中,异步代码中的错误必须手动捕获并传递给 next():
// Express 4 写法:需要 try-catch
app.get('/users/:id', async (req, res, next) => {
try {
const user = await User.findById(req.params.id);
if (!user) {
return res.status(404).json({ error: '用户不存在' });
}
res.json(user);
} catch (err) {
next(err); // 必须手动传递
}
});
Express 4 的替代方案
方案一:express-async-errors
这个包通过 monkey patching 自动处理异步错误:
npm install express-async-errors
// 在应用入口最顶部引入(必须在使用路由之前)
require('express-async-errors');
// 之后异步函数中的错误会被自动捕获
app.get('/users/:id', async (req, res) => {
const user = await User.findById(req.params.id);
res.json(user);
});
方案二:自定义包装函数
如果你不想使用额外的依赖,可以自己实现包装函数:
// 异步处理包装器
const asyncHandler = fn => (req, res, next) =>
Promise.resolve(fn(req, res, next)).catch(next);
// 使用
app.get('/users/:id', asyncHandler(async (req, res) => {
const user = await User.findById(req.params.id);
if (!user) {
throw new Error('用户不存在');
}
res.json(user);
}));
// 带类型的 TypeScript 版本
const asyncHandler = (fn: Function) => (req: Request, res: Response, next: NextFunction) =>
Promise.resolve(fn(req, res, next)).catch(next);
版本对比总结
| 特性 | Express 4 | Express 5 |
|---|---|---|
| async 函数错误处理 | 需要手动 try-catch | 自动捕获 |
| rejected Promise | 需要手动传递 | 自动传递 |
| throw 错误 | 在 async 函数中需要 try-catch | 自动传递 |
| 成功后的 next() | 需要 | 需要 |
| 额外依赖 | express-async-errors | 无需 |
Express 5 的原生 Promise 支持大大简化了异步代码的编写,减少了样板代码,降低了忘记捕获错误的风险。
配置化中间件
有时我们需要中间件能够接受配置参数。实现方式是导出一个返回中间件函数的工厂函数:
// middleware/logger.js
function createLogger(options = {}) {
const {
logBody = false,
logHeaders = false,
format = 'simple'
} = options;
return (req, res, next) => {
const timestamp = new Date().toISOString();
let logMessage = `[${timestamp}] ${req.method} ${req.url}`;
if (logBody && req.body) {
logMessage += ` | Body: ${JSON.stringify(req.body)}`;
}
if (logHeaders) {
logMessage += ` | Headers: ${JSON.stringify(req.headers)}`;
}
console.log(logMessage);
next();
};
}
module.exports = createLogger;
使用配置化中间件:
const createLogger = require('./middleware/logger');
// 使用默认配置
app.use(createLogger());
// 自定义配置
app.use(createLogger({
logBody: true,
logHeaders: true,
format: 'detailed'
}));
这种模式在社区中非常常见,比如 helmet、cors 等中间件都支持配置。
常用中间件实践
日志中间件
const logger = (req, res, next) => {
const start = Date.now();
// 响应完成时记录日志
res.on('finish', () => {
const duration = Date.now() - start;
console.log(`${req.method} ${req.url} ${res.statusCode} - ${duration}ms`);
});
next();
};
app.use(logger);
res.on('finish', ...) 会在响应发送完成后触发,这样可以准确记录请求处理时间。
身份验证中间件
const auth = (req, res, next) => {
const token = req.headers.authorization?.split(' ')[1];
if (!token) {
return res.status(401).json({ error: '请先登录' });
}
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.user = decoded; // 将用户信息挂载到 req 上
next();
} catch (err) {
return res.status(401).json({ error: 'Token 无效或已过期' });
}
};
// 只保护特定路由
app.use('/api/admin', auth);
app.get('/api/admin/users', (req, res) => {
// 这里可以访问 req.user
res.json({ admin: req.user });
});
权限检查中间件
const checkRole = (...allowedRoles) => {
return (req, res, next) => {
if (!req.user) {
return res.status(401).json({ error: '未登录' });
}
if (!allowedRoles.includes(req.user.role)) {
return res.status(403).json({ error: '权限不足' });
}
next();
};
};
// 使用
app.delete('/users/:id', auth, checkRole('admin'), (req, res) => {
// 只有 admin 角色可以访问
res.json({ message: '删除成功' });
});
app.put('/posts/:id', auth, checkRole('admin', 'editor'), (req, res) => {
// admin 和 editor 都可以访问
res.json({ message: '更新成功' });
});
请求限流中间件
npm install express-rate-limit
const rateLimit = require('express-rate-limit');
// 通用限流:15分钟内最多100次请求
const generalLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15分钟
max: 100, // 最多100次
message: { error: '请求过于频繁,请稍后再试' }
});
// 登录限流:15分钟内最多5次尝试
const loginLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 5,
message: { error: '登录尝试过多,请15分钟后再试' }
});
app.use('/api', generalLimiter);
app.post('/login', loginLimiter, loginHandler);
请求体大小限制
// 限制 JSON 请求体大小
app.use(express.json({ limit: '10mb' }));
// 限制 URL 编码请求体大小
app.use(express.urlencoded({
extended: true,
limit: '1mb'
}));
中间件最佳实践
1. 合理的加载顺序
// 推荐的顺序
const express = require('express');
const app = express();
// 1. 基础中间件
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// 2. 安全中间件
app.use(helmet());
app.use(cors());
// 3. 日志中间件
app.use(morgan('dev'));
// 4. 自定义中间件
app.use(require('./middleware/auth'));
// 5. 静态文件
app.use(express.static('public'));
// 6. 路由
app.use('/api', require('./routes'));
// 7. 404 处理
app.use((req, res) => {
res.status(404).json({ error: '路由不存在' });
});
// 8. 错误处理(永远放最后)
app.use(require('./middleware/error'));
2. 模块化组织
将中间件分离到独立文件中:
middleware/
├── auth.js # 身份验证
├── error.js # 错误处理
├── logger.js # 日志记录
├── validate.js # 数据验证
└── index.js # 统一导出
// middleware/index.js
module.exports = {
auth: require('./auth'),
error: require('./error'),
logger: require('./logger'),
validate: require('./validate')
};
// app.js
const { auth, error, logger } = require('./middleware');
app.use(logger);
app.use('/api/protected', auth);
app.use(error);
3. 避免过度使用中间件
不是所有逻辑都需要放在中间件里。对于只在特定路由使用的逻辑,直接写在路由处理器中更清晰:
// 不推荐:为每个路由都创建中间件
app.get('/users', validateUserQuery, getUsers);
app.get('/posts', validatePostQuery, getPosts);
// 推荐:在控制器中处理
app.get('/users', (req, res) => {
const errors = validateUserQuery(req.query);
if (errors) {
return res.status(400).json({ errors });
}
// ...
});
4. 性能考虑
每个中间件都会增加请求处理时间,应该只保留必要的中间件:
// 开发环境启用详细日志
if (process.env.NODE_ENV === 'development') {
app.use(morgan('dev'));
}
// 生产环境使用精简日志
if (process.env.NODE_ENV === 'production') {
app.use(morgan('combined'));
}
小结
本章深入讲解了 Express 中间件机制:
- 中间件概念:函数可以访问 req、res、next,在请求和响应之间处理数据
- 核心规则:要么结束响应,要么调用 next(),否则请求会被挂起
- 执行顺序:先加载的先执行,顺序很重要
- 中间件类型:应用级、路由级、错误处理、内置、第三方
- next() 函数:调用下一个中间件、跳过路由、传递错误
- 异步处理:Express 5 自动处理,Express 4 需要手动捕获
- 配置化中间件:工厂函数返回中间件,支持自定义配置
中间件是 Express 的灵魂,理解它的工作原理对于构建可维护的应用至关重要。
练习
- 编写一个日志中间件,记录每个请求的方法、路径、状态码和耗时
- 实现一个可配置的身份验证中间件,支持白名单路径
- 创建一个请求限流中间件,限制同一 IP 的请求频率
- 使用
next('route')实现一个条件路由,根据用户类型返回不同内容