跳到主要内容

中间件

中间件是 Express.js 最核心的设计理念。理解中间件机制,是掌握 Express 开发的关键。本章将深入讲解中间件的原理、类型和使用方法。

什么是中间件?

中间件(Middleware)是一个函数,它可以访问请求对象(req)、响应对象(res)和应用程序请求-响应循环中的下一个中间件函数。这个名字来源于它在请求和响应之间起到"中间人"的作用。

中间件函数的签名

function middleware(req, res, next) {
// req: 请求对象,包含请求数据
// res: 响应对象,用于发送响应
// next: 调用后传递控制权给下一个中间件
}

中间件函数可以执行以下任务:

  1. 执行任何代码:日志记录、数据验证、权限检查等
  2. 修改请求和响应对象:添加属性、设置头信息等
  3. 结束请求-响应循环:发送响应给客户端
  4. 调用下一个中间件:通过 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()JSONObjectAPI 请求
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 4Express 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'
}));

这种模式在社区中非常常见,比如 helmetcors 等中间件都支持配置。

常用中间件实践

日志中间件

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 中间件机制:

  1. 中间件概念:函数可以访问 req、res、next,在请求和响应之间处理数据
  2. 核心规则:要么结束响应,要么调用 next(),否则请求会被挂起
  3. 执行顺序:先加载的先执行,顺序很重要
  4. 中间件类型:应用级、路由级、错误处理、内置、第三方
  5. next() 函数:调用下一个中间件、跳过路由、传递错误
  6. 异步处理:Express 5 自动处理,Express 4 需要手动捕获
  7. 配置化中间件:工厂函数返回中间件,支持自定义配置

中间件是 Express 的灵魂,理解它的工作原理对于构建可维护的应用至关重要。

练习

  1. 编写一个日志中间件,记录每个请求的方法、路径、状态码和耗时
  2. 实现一个可配置的身份验证中间件,支持白名单路径
  3. 创建一个请求限流中间件,限制同一 IP 的请求频率
  4. 使用 next('route') 实现一个条件路由,根据用户类型返回不同内容