Express 5.x 新特性与迁移
Express 5.0.0 于 2024 年 10 月正式发布,这是 Express 近十年来最重要的版本更新。本章将详细介绍 Express 5.x 的新特性、破坏性变化以及如何从 Express 4.x 迁移。
版本概述
Express 5 的设计目标是"稳定且安全",重点在于:
- 放弃旧版 Node.js 支持,拥抱现代 JavaScript
- 原生支持 Promise 和 async/await 错误处理
- 增强安全性,移除潜在的安全隐患
- 统一 API 签名,提升开发体验
环境要求
Express 5 要求 Node.js 18 或更高版本。如果你使用的是旧版本 Node.js,需要先升级:
# 检查 Node.js 版本
node --version
# 推荐使用 Node.js 20 LTS
安装 Express 5
新项目安装
npm init -y
npm install express@5
现有项目升级
npm install express@5
使用自动迁移工具
Express 官方提供了代码迁移工具(codemod),可以自动修复大部分废弃的 API 调用:
# 运行所有可用的 codemods(推荐)
npx codemod@latest @expressjs/v5-migration-recipe
# 运行特定的 codemod
npx codemod@latest @expressjs/route-del-to-delete # app.del() → app.delete()
npx codemod@latest @expressjs/pluralize-method-names # 方法名复数化
npx codemod@latest @expressjs/explicit-request-params # req.param() → req.params/body/query
npx codemod@latest @expressjs/status-send-order # 响应状态码顺序
npx codemod@latest @expressjs/redirect-arg-order # redirect 参数顺序
npx codemod@latest @expressjs/back-redirect-deprecated # res.redirect('back')
npx codemod@latest @expressjs/camelcase-sendfile # res.sendfile() → res.sendFile()
codemod 工具会自动扫描代码并应用修复,大大减少手动迁移的工作量。建议在运行前先提交代码到版本控制系统,以便审查变更。
核心改进
Promise 支持(最重要改进)
Express 5 原生支持 Promise 错误处理,这是最期待的改进之一。
Express 4.x 的痛点:异步错误必须手动传递给 next()
// Express 4.x - 需要手动捕获错误
app.get('/user/: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 5.x 的改进:异步函数中的错误会自动传递
// Express 5.x - 错误自动传递
app.get('/user/:id', async (req, res) => {
const user = await User.findById(req.params.id);
// 如果 findById 抛出错误,会自动传递给错误处理中间件
if (!user) {
return res.status(404).json({ error: '用户不存在' });
}
res.json(user);
});
// 错误处理中间件
app.use((err, req, res, next) => {
console.error(err);
res.status(500).json({ error: '服务器错误' });
});
工作原理:
当路由处理器或中间件返回一个 rejected Promise 时,Express 5 会自动将拒绝原因作为错误传递给错误处理中间件,就像调用了 next(err) 一样。
注意事项:
这种支持只针对 rejected Promise。如果你在 async 函数中成功完成操作,仍需要手动调用 next() 或发送响应:
// 中间件示例
app.use(async (req, res, next) => {
req.locals.user = await getUser(req);
next(); // 成功时仍需调用 next()
});
// 错误会自动传递
app.use(async (req, res, next) => {
// 如果 getUser 抛出错误,会自动传递给错误处理中间件
req.locals.user = await getUser(req);
next();
});
路径匹配语法变化
Express 5 升级了 path-to-regexp 从 0.x 到 8.x,路径匹配语法有重大变化。
1. 通配符必须有名称
// Express 4.x
app.get('/*', (req, res) => {
res.send('匹配所有路径');
});
// Express 5.x
app.get('/*splat', (req, res) => {
// 通配符必须有名称
console.log(req.params.splat); // 捕获的路径部分
res.send('匹配所有路径');
});
// 匹配包括根路径在内的所有路径
app.get('/{*splat}', (req, res) => {
res.send('匹配所有路径包括根路径');
});
2. 可选参数使用新语法
// Express 4.x - 使用 ? 表示可选
app.get('/:file.:ext?', (req, res) => {
res.send('文件名可选扩展名');
});
// Express 5.x - 使用 {} 表示可选部分
app.get('/:file{.:ext}', (req, res) => {
// ext 是可选的
res.send(`文件: ${req.params.file}, 扩展名: ${req.params.ext || '无'}`);
});
// 更复杂的可选模式
app.get('/base{/:optional}/:required', (req, res) => {
// optional 部分是可选的
res.send('匹配成功');
});
3. 不再支持正则表达式
出于安全考虑(防止 ReDoS 攻击),Express 5 不再支持正则表达式语法:
// Express 4.x - 支持正则
app.get('/user/:id(\\d+)', (req, res) => {
res.send('ID 必须是数字');
});
// Express 5.x - 使用中间件验证
app.get('/user/:id', (req, res, next) => {
const id = req.params.id;
if (!/^\d+$/.test(id)) {
return res.status(400).json({ error: 'ID 必须是数字' });
}
res.send(`用户 ID: ${id}`);
});
// 或者使用验证库(推荐)
const { param, validationResult } = require('express-validator');
app.get('/user/:id',
param('id').isInt().withMessage('ID 必须是数字'),
(req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
res.send(`用户 ID: ${req.params.id}`);
}
);
4. 保留字符
以下字符在路径中现在是保留的,如果需要使用必须转义:
| 字符 | 说明 |
|---|---|
( ) | 分组 |
[ ] | 字符集 |
? | 可选 |
+ | 一个或多个 |
! | 否定 |
// 如果路径中包含这些字符,需要转义
app.get('/file\\?download', (req, res) => {
res.send('匹配 /file?download');
});
请求体解析变化
req.body 默认值变化
// Express 4.x - req.body 默认为 {}
app.post('/data', (req, res) => {
console.log(req.body); // {} (即使没有发送请求体)
});
// Express 5.x - req.body 默认为 undefined
app.post('/data', (req, res) => {
console.log(req.body); // undefined (如果没有发送请求体)
// 安全访问
const data = req.body || {};
res.json(data);
});
urlencoded 默认解析器变化
// Express 4.x - extended 默认为 true
app.use(express.urlencoded({ extended: true })); // 使用 qs 库
// Express 5.x - extended 默认为 false
app.use(express.urlencoded()); // 默认使用 querystring 库
// 如果需要解析复杂嵌套对象,显式设置 extended: true
app.use(express.urlencoded({ extended: true }));
Brotli 压缩支持
Express 5 内置支持 Brotli 压缩(如果客户端支持):
const compression = require('compression');
app.use(compression({
// 自动支持 br, gzip, deflate
// 客户端支持 Brotli 时优先使用
}));
// 响应头会根据客户端能力选择最佳压缩方式
// Accept-Encoding: br, gzip, deflate → Content-Encoding: br
移除的 API
以下 API 在 Express 5 中已被完全移除。
1. app.del() 方法
// Express 4.x
app.del('/user/:id', (req, res) => {
res.send('删除用户');
});
// Express 5.x
app.delete('/user/:id', (req, res) => {
res.send('删除用户');
});
2. req.param(name) 方法
这个方法因为安全隐患已被移除:
// Express 4.x - 不推荐
app.post('/user', (req, res) => {
const id = req.param('id'); // 从 params、body、query 查找
const name = req.param('name'); // 危险:可能来自不可信来源
});
// Express 5.x - 明确指定来源
app.post('/user', (req, res) => {
const id = req.params.id; // 路由参数
const name = req.body.name; // 请求体
const page = req.query.page; // 查询参数
});
3. 带状态码的响应方法签名
Express 5 统一了所有响应方法的签名,状态码必须通过 res.status() 设置:
// Express 4.x - 已废弃的签名
res.json(obj, 201);
res.jsonp(obj, 201);
res.send(obj, 200);
res.send(200);
res.redirect('/new', 301);
// Express 5.x - 统一的签名
res.status(201).json(obj);
res.status(201).jsonp(obj);
res.status(200).send(obj);
res.sendStatus(200); // 发送状态码和对应文本
res.redirect(301, '/new'); // 注意参数顺序
4. res.redirect('back') 魔法字符串
// Express 4.x
app.get('/back', (req, res) => {
res.redirect('back'); // 重定向到来源页
});
// Express 5.x - 显式指定
app.get('/back', (req, res) => {
res.redirect(req.get('Referrer') || '/');
});
// 或者创建工具函数
const redirectBack = (req, res) => {
res.redirect(req.get('Referrer') || '/');
};
5. res.sendfile() 方法
// Express 4.x
res.sendfile('/path/to/file');
// Express 5.x - 注意大写 F
res.sendFile('/path/to/file');
重要:Express 5 中 res.sendFile() 使用新的 MIME 类型检测库,部分文件类型的 Content-Type 有变化:
| 文件类型 | Express 4.x | Express 5.x |
|---|---|---|
| .js | application/javascript | text/javascript |
| .json | text/json | application/json |
| .css | text/plain | text/css |
| .xml | text/xml | application/xml |
| .woff | application/font-woff | font/woff |
| .svg | application/svg+xml | image/svg+xml |
6. 方法名复数化
// Express 4.x
req.acceptsCharset('utf-8');
req.acceptsEncoding('br');
req.acceptsLanguage('en');
// Express 5.x - 方法名复数化
req.acceptsCharsets('utf-8');
req.acceptsEncodings('br');
req.acceptsLanguages('en');
7. express.static.mime 属性
// Express 4.x
const mime = express.static.mime;
mime.lookup('json'); // 'application/json'
// Express 5.x - 使用 mime-types 包
const mime = require('mime-types');
mime.lookup('json'); // 'application/json'
行为变化
静态文件 dotfiles 处理
Express 5 出于安全考虑,默认不再提供以点开头的目录(如 .well-known):
// Express 4.x - dotfiles 默认可见
app.use(express.static('public'));
// /.well-known/assetlinks.json 可访问
// Express 5.x - dotfiles 默认不可见
app.use(express.static('public'));
// /.well-known/assetlinks.json 返回 404
// 解决方案:显式允许特定 dotfiles 目录
app.use('/.well-known', express.static('public/.well-known', {
dotfiles: 'allow'
}));
app.use(express.static('public'));
req.host 行为修正
在 Express 4 中,req.host 属性存在一个 bug:它会错误地移除端口号。Express 5 修复了这个问题,现在 req.host 正确保留端口号。
// 请求头: Host: example.com:3000
// Express 4.x - 端口号被错误地移除
req.host // 'example.com' (端口丢失,这是 bug)
// Express 5.x - 正确保留端口
req.host // 'example.com:3000' (已修复)
// 如果只需要主机名(不含端口),使用 req.hostname
req.hostname // 'example.com'
注意:req.host 和 req.hostname 是两个不同的属性:
req.host- 包含端口号的完整主机信息req.hostname- 仅主机名,不包含端口
req.params 详细变化
Express 5 中 req.params 有两个重要的行为变化:
1. 通配符参数现在是数组
当使用通配符捕获路径时,结果从字符串变为数组:
// Express 5.x
app.get('/*splat', (req, res) => {
// GET /foo/bar
console.dir(req.params);
// => [Object: null prototype] { splat: [ 'foo', 'bar' ] }
// 访问捕获的路径
const path = req.params.splat.join('/'); // 'foo/bar'
res.send(`捕获路径: ${path}`);
});
// Express 4.x - 返回字符串
// req.params['0'] = 'foo/bar'
2. 未匹配参数被省略
在 Express 4 中,未匹配的参数会存在于 req.params 中(值为空字符串或 undefined),Express 5 则完全省略:
// Express 4.x
app.get('/:file.:ext?', (req, res) => {
// GET /image (没有扩展名)
console.dir(req.params);
// => { file: 'image', ext: undefined }
});
// Express 5.x
app.get('/:file{.:ext}', (req, res) => {
// GET /image (没有扩展名)
console.dir(req.params);
// => [Object: null prototype] { file: 'image' }
// ext 完全不存在于 req.params 中
// 安全访问
const ext = req.params.ext || 'default';
});
3. 使用 null prototype 对象
当使用字符串路径时,req.params 现在是 null prototype 对象,更安全:
// Express 5.x
app.get('/user/:id', (req, res) => {
console.log(Object.getPrototypeOf(req.params)); // null
// 这意味着没有继承的属性,避免意外访问
});
req.query 默认解析器变化
// Express 4.x - 默认使用 extended 解析器(qs 库)
app.use(express.urlencoded()); // extended 默认为 true
// Express 5.x - 默认使用 simple 解析器(querystring 库)
app.use(express.urlencoded()); // extended 默认为 false
// 如果需要解析复杂嵌套对象,显式设置
app.use(express.urlencoded({ extended: true }));
同时,req.query 现在是只读的 getter:
// Express 4.x - 可修改
req.query = { custom: 'value' };
// Express 5.x - 只读 getter,修改无效
// 如需自定义,复制一份
const query = { ...req.query, custom: 'value' };
res.status() 验证
// Express 4.x - 接受任意数字
res.status(999);
// Express 5.x - 只接受 100-999 范围内的整数
res.status(200); // OK
res.status(999); // 报错:无效状态码
req.query 只读
// Express 4.x - 可修改
req.query = { custom: 'value' };
// Express 5.x - 只读 getter
req.query // 只读,修改无效
// 如需自定义查询对象,可以复制
const query = { ...req.query, custom: 'value' };
app.router 回归
app.router 在 Express 4 中被移除,现在又回来了:
// Express 5.x
const router = app.router; // 获取基础路由器引用
// 用途:直接操作基础路由器
app.router.use(someMiddleware);
app.listen 错误处理变化
Express 5 中 app.listen 的回调函数会接收错误参数:
// Express 4.x - 错误会被抛出
const server = app.listen(8080, () => {
console.log('Server started');
});
// 如果端口被占用,会抛出 EADDRINUSE 错误
// Express 5.x - 错误传递给回调
const server = app.listen(8080, '0.0.0.0', (error) => {
if (error) {
console.error('启动失败:', error.message);
// 可以选择重试或优雅退出
process.exit(1);
}
console.log(`服务运行在 ${JSON.stringify(server.address())}`);
});
这个变化让错误处理更加一致和可控。
res.clearCookie 变化
Express 5 中 res.clearCookie 会忽略 maxAge 和 expires 选项:
// Express 4.x - 可能会设置这些选项
res.clearCookie('name', { maxAge: 0, expires: new Date(0) });
// Express 5.x - 这些选项被忽略
res.clearCookie('name'); // 简化写法
res.clearCookie('name', { path: '/' }); // 只需指定 domain/path
res.vary 变化
缺少参数时会抛出错误而不是警告:
// Express 4.x - 缺少参数只警告
res.vary(); // 控制台警告,但不报错
// Express 5.x - 缺少参数抛出错误
res.vary(); // Error: field argument is required
res.vary('Accept-Encoding'); // 正确用法
res.render() 强制异步
// Express 5.x - res.render() 现在强制异步行为
app.get('/page', (req, res) => {
res.render('template', { data: 'value' }, (err, html) => {
if (err) {
return res.status(500).send('渲染错误');
}
res.send(html);
});
});
迁移清单
迁移前准备
- 确保 Node.js 版本 >= 18
- 运行现有测试确保全部通过
- 备份当前代码
自动迁移
# 运行所有 codemods
npx @expressjs/codemod upgrade
手动检查清单
路由相关:
- 检查通配符路由
/*改为/*splat或/{*splat} - 检查可选参数路由
/:param?改为{/:param} - 移除所有正则表达式路由,改用验证中间件
- 检查保留字符是否正确转义
响应方法:
-
app.del()改为app.delete() -
res.json(obj, status)改为res.status(status).json(obj) -
res.send(status)改为res.sendStatus(status) -
res.redirect('back')改为显式重定向 -
res.sendfile()改为res.sendFile()
请求处理:
-
req.param(name)改为req.params/req.body/req.query - 检查
req.body可能为undefined -
req.acceptsCharset等改为复数形式
静态文件:
- 检查 dotfiles 目录是否需要显式配置
- 检查 MIME 类型变化是否影响功能
错误处理:
- 简化 async 函数中的 try-catch(可选)
- 确保错误处理中间件正确配置
测试验证
# 运行测试
npm test
# 检查废弃 API 警告
DEBUG=express:* node app.js
# Express 5 调试日志命名空间变化
# Express 4: express:*
# Express 5: express:*,router,router:*
DEBUG=express:*,router,router:* node app.js
最佳实践
1. 使用 async/await 简化代码
Express 5 原生支持 Promise 错误处理,可以大幅简化代码:
// 推荐:简洁的 async/await 风格
app.get('/users/:id', async (req, res) => {
const user = await User.findById(req.params.id);
if (!user) {
return res.status(404).json({ error: '用户不存在' });
}
res.json(user);
});
// 错误处理中间件统一处理所有错误
app.use((err, req, res, next) => {
console.error(err);
res.status(err.status || 500).json({
error: process.env.NODE_ENV === 'production'
? '服务器错误'
: err.message
});
});
2. 使用验证库替代正则路由
const { param, body, validationResult } = require('express-validator');
// 验证路由参数
app.get('/users/:id',
param('id').isInt({ min: 1 }).withMessage('ID 必须是正整数'),
async (req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
const user = await User.findById(req.params.id);
res.json(user);
}
);
// 验证请求体
app.post('/users',
body('email').isEmail().normalizeEmail(),
body('password').isLength({ min: 6 }),
body('name').trim().notEmpty(),
async (req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
const user = await User.create(req.body);
res.status(201).json(user);
}
);
3. 安全处理请求体
// Express 5 中 req.body 可能为 undefined
app.post('/data', (req, res) => {
// 安全访问
const { name = '', email = '' } = req.body || {};
// 或者使用默认对象
const data = {
name: req.body?.name || 'default',
email: req.body?.email || ''
};
res.json(data);
});
4. 统一响应格式
// 响应工具类
const response = {
success: (res, data, message = '成功') => {
res.json({ success: true, message, data });
},
error: (res, message = '失败', status = 400) => {
res.status(status).json({ success: false, message });
},
created: (res, data, message = '创建成功') => {
res.status(201).json({ success: true, message, data });
},
noContent: (res) => {
res.status(204).send();
}
};
// 使用
app.post('/users', async (req, res) => {
const user = await User.create(req.body);
response.created(res, user);
});
app.delete('/users/:id', async (req, res) => {
await User.findByIdAndDelete(req.params.id);
response.noContent(res);
});
小结
Express 5 带来的主要改进:
- Promise 支持:异步错误自动传递,大幅简化 async/await 代码
- 安全增强:移除正则路由防止 ReDoS 攻击,dotfiles 默认不可见
- API 统一:移除废弃签名,方法命名更一致
- 现代化:要求 Node.js 18+,支持 Brotli 压缩
Express 5 的迁移相对平滑,大部分变化可以通过 codemod 工具自动完成。对于新项目,建议直接使用 Express 5.x。