Node.js: Express 进阶
最后更新:2026-08-26
1. 1 Bob 的生产危机
Bob 的 API 上线第一天就出了问题:用户提交了乱码邮箱,数据库报错整站崩溃;有人上传了 500MB 的图片,磁盘瞬间爆满;竞争对手写了脚本每秒发 1000 个请求,服务器直接 502。
一条
app.use(errorHandler)就能挡住 80% 的生产事故。
TEXT
📖 仅展示
Bob 的排雷时间线:
Day 1 📧 无效邮箱 → 数据库异常 → 500 错误
Day 2 🖼️ 500MB 上传 → 磁盘满 → 服务宕机
Day 3 🤖 1000 req/s → CPU 100% → 502 Bad Gateway
Day 4 🔒 加中间件 → 逐个击破 → 服务稳定
2. 2 错误处理中间件
(1) 四参数签名机制
Express 通过参数数量识别错误中间件——必须是 4 个参数 (err, req, res, next),少一个就变成普通中间件。
▶ 示例:全局错误处理中间件
JAVASCRIPT
const express = require('express');
const app = express();
app.get('/boom', (req, res, next) => {
try {
throw new Error('故意炸了');
} catch (err) {
next(err);
}
});
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(err.status || 500).json({
code: err.status || 500,
data: null,
message: err.message
});
});
app.listen(3000);
(2) 自定义业务错误类
▶ 示例:业务错误与 HTTP 错误分离
JAVASCRIPT
class AppError extends Error {
constructor(message, status) {
super(message);
this.status = status;
this.isOperational = true;
Error.captureStackTrace(this, this.constructor);
}
}
class NotFoundError extends AppError {
constructor(resource) {
super(`${resource} 未找到`, 404);
}
}
class ValidationError extends AppError {
constructor(message) {
super(message, 400);
}
}
app.get('/users/:id', (req, res, next) => {
const user = findUser(req.params.id);
if (!user) return next(new NotFoundError('用户'));
res.json({ code: 0, data: user, message: 'ok' });
});
(3) 错误处理方式对比
| 方式 | 参数个数 | 捕获范围 | 适用场景 | 异步支持 |
|---|---|---|---|---|
app.use(errHandler) |
4 | 全局所有错误 | 最后兜底 | 需手动 next(err) |
try/catch + next(err) |
— | 单个路由 | 同步代码 | 仅同步 |
express-async-errors |
0 | 全局异步错误 | async/await 路由 | 自动 |
domain 模块(已废弃) |
— | 进程级 | 不推荐 | — |
process.on('uncaughtException') |
— | 进程级 | 最后防线 | 全局 |
Promise .catch() |
— | 单个 Promise | 单个异步 | 单个 |
3. 3 express-validator 请求验证
(1) 验证链与中间件用法
express-validator 基于 validator.js,用验证链(validation chain)声明式定义规则,验证失败自动收集错误。
▶ 示例:用户注册验证
JAVASCRIPT
const { body, validationResult } = require('express-validator');
app.post('/register',
body('username')
.isLength({ min: 3, max: 20 }).withMessage('用户名需3-20字符')
.isAlphanumeric().withMessage('用户名仅允许字母数字'),
body('email')
.isEmail().withMessage('邮箱格式无效')
.normalizeEmail(),
body('password')
.isLength({ min: 8 }).withMessage('密码至少8位')
.matches(/\d/).withMessage('密码需包含数字')
.matches(/[A-Z]/).withMessage('密码需包含大写字母'),
body('age')
.optional()
.isInt({ min: 1, max: 150 }).withMessage('年龄需1-150'),
(req, res, next) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({
code: 400,
data: errors.array(),
message: '请求验证失败'
});
}
next();
},
(req, res) => {
res.json({ code: 0, data: req.body, message: 'ok' });
}
);
(2) express-validator 常用验证器
| 验证器 | 用途 | 示例 | 配套修饰符 |
|---|---|---|---|
isEmail() |
邮箱 | body('email').isEmail() |
.normalizeEmail() |
isLength() |
长度 | body('name').isLength({min:2,max:50}) |
— |
isInt() / isFloat() |
数字 | query('page').isInt({min:1}) |
.toInt() |
isBoolean() |
布尔 | body('active').isBoolean() |
.toBoolean() |
isDate() |
日期 | body('birthday').isDate() |
.toDate() |
isURL() |
URL | body('website').isURL() |
— |
isIn() |
枚举 | body('role').isIn(['admin','user']) |
— |
matches() |
正则 | body('code').matches(/^\d{6}$/) |
— |
isMongoId() |
ObjectId | param('id').isMongoId() |
— |
optional() |
可选 | body('nickname').optional().isLength({max:30}) |
{nullable:true} |
(3) express-validator 与 Joi 对比
| 对比维度 | express-validator | Joi |
|---|---|---|
| 集成方式 | Express 原生中间件 | 独立验证库,需手动集成 |
| 语法风格 | 链式调用,逐字段定义 | Schema 对象,一次性定义 |
| 底层依赖 | validator.js | 自实现 |
| 错误收集 | validationResult() 自动收集 |
validate().error 手动处理 |
| 适用场景 | Express 项目快速集成 | 任意 Node.js 项目 |
| 学习曲线 | 低(若熟悉 validator.js) | 中(独立 Schema 语法) |
| 类型转换 | .toInt() .toDate() 等 |
自动类型转换 |
| 社区规模 | 周下载 ~1.5M | 周下载 ~5M |
4. 4 文件上传 multer
(1) 三种存储策略
multer 提供 single、array、fields 三种上传模式,存储方式可选 memoryStorage(内存)和 diskStorage(磁盘)。
▶ 示例:磁盘存储 + 文件过滤
JAVASCRIPT
const multer = require('multer');
const path = require('path');
const storage = multer.diskStorage({
destination: (req, file, cb) => {
cb(null, 'uploads/');
},
filename: (req, file, cb) => {
const ext = path.extname(file.originalname);
const uniqueName = `${Date.now()}-${Math.round(Math.random() * 1e9)}${ext}`;
cb(null, uniqueName);
}
});
const fileFilter = (req, file, cb) => {
const allowed = /\.(jpg|jpeg|png|gif|webp)$/i;
if (allowed.test(path.extname(file.originalname))) {
cb(null, true);
} else {
cb(new Error('仅支持 jpg/png/gif/webp 格式'), false);
}
};
const upload = multer({
storage,
fileFilter,
limits: { fileSize: 5 * 1024 * 1024 }
});
app.post('/avatar', upload.single('avatar'), (req, res) => {
if (!req.file) return res.status(400).json({ code: 400, data: null, message: '请上传文件' });
res.json({
code: 0,
data: { url: `/uploads/${req.file.filename}`, size: req.file.size },
message: 'ok'
});
});
app.post('/photos', upload.array('photos', 9), (req, res) => {
const urls = req.files.map(f => `/uploads/${f.filename}`);
res.json({ code: 0, data: urls, message: 'ok' });
});
(2) multer 配置选项对比
| 配置项 | 类型 | 默认值 | 说明 | 示例 |
|---|---|---|---|---|
storage |
StorageEngine | memoryStorage |
存储引擎 | multer.diskStorage({...}) |
dest |
string | — | 目标目录(与 storage 二选一) | 'uploads/' |
fileFilter |
Function | 全部允许 | 文件过滤回调 | (req,file,cb)=>{...} |
limits.fileSize |
number | 无限制 | 单文件最大字节数 | 5*1024*1024 |
limits.files |
number | 无限制 | 多文件上传最大数量 | 9 |
limits.fields |
number | 无限制 | 非文件字段最大数量 | 10 |
limits.fieldSize |
number | 1MB | 非文件字段最大字节数 | 1024*100 |
limits.parts |
number | 无限制 | multipart 总 part 数 | 20 |
(3) memoryStorage vs diskStorage
| 对比维度 | memoryStorage | diskStorage |
|---|---|---|
| 存储位置 | 内存(Buffer) | 磁盘文件 |
req.file 属性 |
buffer |
path, filename |
| 适用场景 | 小文件、即时处理(如图片压缩后转存) | 大文件、持久化存储 |
| 性能 | 快(无磁盘 I/O) | 稍慢(需写磁盘) |
| 内存风险 | 大文件或并发高时 OOM | 无 |
| 重启丢失 | 是 | 否 |
| 文件名控制 | 不需要 | 需自定义 filename 回调 |
5. 5 静态文件服务配置
(1) express.static 详解
▶ 示例:多目录静态服务 + 缓存控制
JAVASCRIPT
const express = require('express');
const app = express();
app.use('/static', express.static('public', {
maxAge: '7d',
etag: true,
lastModified: true,
immutable: true,
setHeaders: (res, filePath) => {
if (filePath.endsWith('.html')) {
res.setHeader('Cache-Control', 'no-cache');
}
if (filePath.match(/\.(jpg|png|gif|webp|svg)$/)) {
res.setHeader('Cache-Control', 'public, max-age=2592000, immutable');
}
}
}));
app.use('/uploads', express.static('uploads', {
maxAge: '30d',
dotfiles: 'deny'
}));
(2) 静态服务安全注意事项
dotfiles设为'deny'防止.env等敏感文件泄露- 上传目录与代码目录分离,避免上传
.js被执行 - 生产环境用 Nginx/CDN 托管静态文件,Express 仅做 API
- 设置合理的
Cache-Control减少带宽消耗
6. 6 统一响应格式
(1) {code, data, message} 规范
▶ 示例:响应封装中间件
JAVASCRIPT
const responseHandler = (req, res, next) => {
res.success = (data = null, message = 'ok') => {
res.json({ code: 0, data, message });
};
res.fail = (message = '操作失败', code = -1, data = null) => {
res.json({ code, data, message });
};
res.paginate = (list, total, page, pageSize) => {
res.json({
code: 0,
data: { list, total, page, pageSize, totalPages: Math.ceil(total / pageSize) },
message: 'ok'
});
};
next();
};
app.use(responseHandler);
app.get('/users', (req, res) => {
const users = getUserList();
res.success(users);
});
app.get('/users/:id', (req, res, next) => {
const user = findUser(req.params.id);
if (!user) return res.fail('用户不存在', 404);
res.success(user);
});
(2) 国际化 6 铁律
- 响应
message不硬编码中文,使用 i18n key 如"error.user_not_found" - 服务端根据
Accept-Language头或?lang=zh参数切换语言 - 错误码
code与语言无关,前端按 code 查本地化文案 - 日期时间统一返回 ISO 8601 格式(
2025-01-15T08:30:00Z),前端按 locale 格式化 - 数字/货币不预格式化,返回原始值 + 货币代码,前端按 locale 展示
- 验证错误数组中
msg字段也走 i18n,不直接返回中文提示
7. 7 rate-limit 限流
(1) express-rate-limit 配置
▶ 示例:分级限流策略
JAVASCRIPT
const rateLimit = require('express-rate-limit');
const globalLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 100,
standardHeaders: true,
legacyHeaders: false,
message: { code: 429, data: null, message: '请求过于频繁,请稍后再试' }
});
const apiLimiter = rateLimit({
windowMs: 60 * 1000,
max: 30,
message: { code: 429, data: null, message: 'API 调用超过限制' }
});
const loginLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 5,
skipSuccessfulRequests: true,
message: { code: 429, data: null, message: '登录失败次数过多,请15分钟后重试' }
});
app.use(globalLimiter);
app.use('/api/', apiLimiter);
app.use('/auth/login', loginLimiter);
(2) 安全中间件列表
| 中间件 | 用途 | 默认行为 | 关键配置 | npm 包 |
|---|---|---|---|---|
helmet |
HTTP 安全头 | 设置 15 个安全头 | contentSecurityPolicy, hsts |
helmet |
express-rate-limit |
限流 | — | windowMs, max |
express-rate-limit |
cors |
跨域控制 | 拒绝所有跨域 | origin, methods, credentials |
cors |
express-validator |
输入验证 | — | body(), query(), param() |
express-validator |
multer |
文件上传 | — | limits, fileFilter |
multer |
express-mongo-sanitize |
NoSQL 注入 | 移除 $ 和 . |
— | express-mongo-sanitize |
xss-clean |
XSS 清洗 | 转义 HTML | — | xss-clean |
hpp |
参数污染 | 取最后值 | whitelist |
hpp |
compression |
gzip 压缩 | — | threshold, level |
compression |
8. 8 环境配置 dotenv
(1) dotenv 基础用法
▶ 示例:环境变量分层加载
JAVASCRIPT
const dotenv = require('dotenv');
const path = require('path');
dotenv.config({ path: path.resolve(process.env.NODE_ENV ? `.env.${process.env.NODE_ENV}` : '.env') });
const config = {
port: parseInt(process.env.PORT, 10) || 3000,
env: process.env.NODE_ENV || 'development',
db: {
host: process.env.DB_HOST || 'localhost',
port: parseInt(process.env.DB_PORT, 10) || 27017,
name: process.env.DB_NAME || 'myapp_dev'
},
jwt: {
secret: process.env.JWT_SECRET,
expiresIn: process.env.JWT_EXPIRES_IN || '7d'
},
upload: {
maxFileSize: parseInt(process.env.MAX_FILE_SIZE, 10) || 5 * 1024 * 1024,
allowedTypes: (process.env.ALLOWED_TYPES || 'jpg,jpeg,png,gif,webp').split(',')
},
rateLimit: {
windowMs: parseInt(process.env.RATE_WINDOW_MS, 10) || 15 * 60 * 1000,
max: parseInt(process.env.RATE_MAX, 10) || 100
}
};
module.exports = config;
▶ 示例:(2) .env 文件最佳实践
TEXT
📖 仅展示
# .env ← 默认(开发),不提交到 Git
# .env.production ← 生产环境,严格限制访问
# .env.test ← 测试环境
PORT=3000
NODE_ENV=development
DB_HOST=localhost
DB_PORT=27017
DB_NAME=myapp_dev
JWT_SECRET=your-super-secret-key-change-in-production
JWT_EXPIRES_IN=7d
MAX_FILE_SIZE=5242880
ALLOWED_TYPES=jpg,jpeg,png,gif,webp
RATE_WINDOW_MS=900000
RATE_MAX=100
TEXT
📖 仅展示
# .gitignore 必须包含
.env
.env.*
!.env.example
9. 9 Express 请求处理完整流程
▶ 示例:(1) Mermaid 流程图
flowchart TD
A[客户端请求] --> B[helmet 安全头]
B --> C[cors 跨域检查]
C --> D[rate-limit 限流检查]
D -->|429| E[返回限流响应]
D -->|通过| F[express.json 解析体]
F --> G[multer 文件上传处理]
G -->|文件过大/格式错误| H[next error]
G -->|通过| I[express-validator 验证]
I -->|验证失败| J[返回 400 验证错误]
I -->|通过| K[业务路由处理]
K -->|业务错误| L[next error]
K -->|成功| M[统一响应封装 success/fail]
M --> N[返回 JSON 响应]
L --> O[全局错误处理中间件]
H --> O
O --> P[统一错误响应 code/data/message]
P --> N
style E fill:#f66,stroke:#333,color:#fff
style J fill:#f66,stroke:#333,color:#fff
style P fill:#f66,stroke:#333,color:#fff
style N fill:#6f6,stroke:#333
(2) 中间件加载顺序原则
- 安全防护类(helmet、cors、rate-limit)放最前
- 请求解析类(json、urlencoded、cookie)其次
- 文件处理(multer)在解析之后
- 验证类(express-validator)在业务之前
- 业务路由居中
- 响应封装在业务路由之前注册
- 错误处理始终放最后
10. 10 综合示例:完整 Express API 安全配置
▶ 示例:生产级 Express 服务器
JAVASCRIPT
📖 仅展示
const express = require('express');
const helmet = require('helmet');
const cors = require('cors');
const rateLimit = require('express-rate-limit');
const multer = require('multer');
const { body, param, validationResult } = require('express-validator');
const compression = require('compression');
const path = require('path');
const config = require('./config');
const app = express();
app.use(helmet());
app.use(cors({ origin: config.corsOrigin, credentials: true }));
app.use(compression({ threshold: 1024 }));
app.use(express.json({ limit: '10kb' }));
app.use(express.urlencoded({ extended: true }));
const globalLimiter = rateLimit({
windowMs: config.rateLimit.windowMs,
max: config.rateLimit.max,
standardHeaders: true,
legacyHeaders: false,
message: { code: 429, data: null, message: 'error.rate_limited' }
});
app.use(globalLimiter);
const upload = multer({
storage: multer.diskStorage({
destination: 'uploads/',
filename: (req, file, cb) => {
const ext = path.extname(file.originalname);
cb(null, `${Date.now()}-${Math.random().toString(36).slice(2)}${ext}`);
}
}),
fileFilter: (req, file, cb) => {
const ext = path.extname(file.originalname).toLowerCase();
if (config.upload.allowedTypes.some(t => `.${t}` === ext)) {
cb(null, true);
} else {
cb(new Error('error.invalid_file_type'), false);
}
},
limits: { fileSize: config.upload.maxFileSize, files: 5 }
});
const responseHandler = (req, res, next) => {
res.success = (data = null, message = 'ok') => {
res.json({ code: 0, data, message });
};
res.fail = (message = 'error.internal', code = -1, data = null) => {
res.json({ code, data, message });
};
next();
};
app.use(responseHandler);
const validate = (rules) => [
...rules,
(req, res, next) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({
code: 400,
data: errors.array().map(e => ({ field: e.path, message: e.msg })),
message: 'error.validation_failed'
});
}
next();
}
];
app.post('/api/users',
validate([
body('username').isLength({ min: 3, max: 20 }).withMessage('error.username_length'),
body('email').isEmail().withMessage('error.invalid_email').normalizeEmail(),
body('password').isLength({ min: 8 }).withMessage('error.password_length')
]),
(req, res) => {
const user = createUser(req.body);
res.success(user, 'ok');
}
);
app.post('/api/upload',
upload.array('files', 5),
(req, res) => {
if (!req.files || req.files.length === 0) {
return res.fail('error.no_file_uploaded', 400);
}
const urls = req.files.map(f => `/uploads/${f.filename}`);
res.success(urls, 'ok');
}
);
app.get('/api/users/:id',
validate([param('id').isMongoId().withMessage('error.invalid_id')]),
(req, res) => {
const user = findUser(req.params.id);
if (!user) return res.fail('error.user_not_found', 404);
res.success(user);
}
);
app.use('/uploads', express.static('uploads', { maxAge: '30d', dotfiles: 'deny' }));
app.use((req, res) => {
res.status(404).json({ code: 404, data: null, message: 'error.not_found' });
});
class AppError extends Error {
constructor(message, status) {
super(message);
this.status = status;
this.isOperational = true;
}
}
app.use((err, req, res, next) => {
if (err instanceof multer.MulterError) {
if (err.code === 'LIMIT_FILE_SIZE') {
return res.status(413).json({ code: 413, data: null, message: 'error.file_too_large' });
}
return res.status(400).json({ code: 400, data: null, message: 'error.upload_failed' });
}
const status = err.status || 500;
const message = err.isOperational ? err.message : 'error.internal';
if (config.env === 'development') console.error(err.stack);
res.status(status).json({ code: status, data: null, message });
});
app.listen(config.port, () => {
console.log(`Server running on port ${config.port} [${config.env}]`);
});
11. 11 本课小结
- 错误中间件 4 参数签名
(err, req, res, next)是 Express 识别的关键 - express-validator 用 验证链 声明规则,
validationResult()收集错误 - multer 的
limits+fileFilter是防止磁盘爆满的双保险 - 统一响应
{code, data, message}让前端处理一致可预测 - rate-limit 分级限流:全局宽松、登录严格、API 适中
- dotenv 分层加载
.env.{NODE_ENV},.env文件绝不提交 Git - 中间件加载顺序:安全→解析→文件→验证→业务→错误处理
❓ 常见问题
Q 中间件和路由哪个先执行?
A 按注册顺序执行。app.use 注册的全局中间件先于路由中间件,路由内中间件按路由定义顺序。
Q 如何实现 API 版本控制?
A 常用三种方式:URL 路径(/v1/users)、请求头(Accept: application/vnd.api.v1+json)、查询参数(?version=1)。
Q 错误处理中间件必须 4 个参数吗?
A 是的。Express 通过参数个数识别错误处理中间件,必须签名为 (err, req, res, next),否则会被当作普通中间件。
Q 如何做请求限流?
A 使用 express-rate-limit 中间件,设置 windowMs 时间窗口和 max 最大请求数,超限返回 429 状态码。
Q 静态文件服务性能如何优化?
A 生产环境用 Nginx 或 CDN 托管静态文件,Express 只处理动态 API;开发环境可用 express.static 的 maxAge 缓存。
- Q: 错误中间件为什么必须是 4 个参数? A: Express 通过
fn.length检测函数参数数量,4 个参数才识别为错误处理中间件,否则当作普通中间件,收不到 err 对象。 - Q: express-validator 和 Joi 的核心区别是什么? A: express-validator 是 Express 中间件风格、链式逐字段定义,与路由深度集成;Joi 是独立 Schema 验证库,定义完整数据结构,需手动调用
validate()并处理结果。 - Q: memoryStorage 和 diskStorage 怎么选? A: 小文件(<1MB)且需即时处理(如缩略图生成后转存云存储)用 memoryStorage;大文件或需持久化用 diskStorage,避免内存溢出。
- Q: 如何限制上传文件大小? A: 三层防护:multer
limits.fileSize拦截应用层、express.json({limit:'10kb'})拦截请求体、Nginxclient_max_body_size拦截网关层。 - Q: express-async-errors 是什么? A: 一个仅需
require('express-async-errors')一行的包,自动捕获 async 路由中的未处理 Promise 拒绝并转发给错误中间件,无需每条路由写 try/catch。 - Q: 统一响应中 code 用 0 表示成功还是用 HTTP 状态码? A: 推荐业务 code 用
0表示成功(与 HTTP 状态码解耦),HTTP 状态码仍按 REST 规范返回(200/400/404/500),业务错误用负数或特定正数编码。
📖 小节
- 1 Bob 的生产危机的核心概念与使用方法
- 2 错误处理中间件的核心概念与使用方法
- 3 express-validator 请求验证的核心概念与使用方法
- 4 文件上传 multer的核心概念与使用方法
- 5 静态文件服务配置的核心概念与使用方法
- 6 统一响应格式的核心概念与使用方法
- 7 rate-limit 限流的核心概念与使用方法
- 8 环境配置 dotenv的核心概念与使用方法
📝 作业
- 为现有 Express 项目添加全局错误处理中间件,区分业务错误(
isOperational)和未知错误,未知错误返回通用提示不暴露堆栈。 - 用 express-validator 为用户注册接口编写完整验证链(用户名、邮箱、密码强度、确认密码一致),验证失败返回字段级错误数组。
- 配置 multer 实现头像上传(单文件、限 2MB、仅 jpg/png),上传成功返回文件 URL,上传失败返回具体原因。
- 添加 express-rate-limit 分级限流:全局 15 分钟 100 次、API 1 分钟 30 次、登录 15 分钟 5 次失败。
- 创建
.env.example文件列出所有环境变量及默认值,确保.env已加入.gitignore。