路由处理
路由是指确定应用程序如何响应客户端对特定端点的请求。每个路由可以有一个或多个处理函数,当匹配到请求时执行。本章将详细介绍 Express 路由系统的工作原理和使用方法。
路由基础
什么是路由?
在 Express 中,一个路由由三部分组成:
- HTTP 方法:如 GET、POST、PUT、DELETE 等
- 路径(URL 模式):如
/users、/users/:id - 处理函数:一个或多个回调函数
当服务器收到一个请求时,Express 会根据请求的 HTTP 方法和 URL 路径来匹配路由,找到匹配的路由后执行对应的处理函数。
基本语法
app.METHOD(PATH, HANDLER)
app:Express 应用实例METHOD:HTTP 方法(小写),如get、post、put、deletePATH:URL 路径HANDLER:处理函数
const express = require('express');
const app = express();
// GET 请求
app.get('/', (req, res) => {
res.send('这是首页');
});
// POST 请求
app.post('/users', (req, res) => {
res.send('创建用户');
});
// PUT 请求
app.put('/users/:id', (req, res) => {
res.send('更新用户');
});
// DELETE 请求
app.delete('/users/:id', (req, res) => {
res.send('删除用户');
});
app.all() 方法
app.all() 可以匹配所有 HTTP 方法,通常用于中间件性质的逻辑:
// 对 /secret 路径的所有请求都会经过这里
app.all('/secret', (req, res, next) => {
console.log('访问了 secret...');
next(); // 继续匹配下一个路由
});
// 需要认证的 API 前置检查
app.all('/api/*', (req, res, next) => {
if (!req.user) {
return res.status(401).json({ error: '请先登录' });
}
next();
});
路由路径
路由路径定义了请求的 URL 匹配模式。Express 支持三种类型的路径:字符串、字符串模式和正则表达式。
字符串路径
最简单的路径形式,精确匹配:
// 匹配根路径 /
app.get('/', (req, res) => {
res.send('根路径');
});
// 匹配 /about
app.get('/about', (req, res) => {
res.send('关于页面');
});
// 匹配 /random.text(包含点的路径)
app.get('/random.text', (req, res) => {
res.send('random.text');
});
字符串模式
Express 4 支持在字符串路径中使用特殊字符:
// ? 表示前面的字符可选
// 匹配 /acd 和 /abcd
app.get('/ab?cd', (req, res) => {
res.send('ab?cd 匹配成功');
});
// + 表示前面的字符可以出现一次或多次
// 匹配 /abcd、/abbcd、/abbbcd 等
app.get('/ab+cd', (req, res) => {
res.send('ab+cd 匹配成功');
});
// * 表示任意字符
// 匹配 /abcd、/abxcd、/abRANDOMcd 等
app.get('/ab*cd', (req, res) => {
res.send('ab*cd 匹配成功');
});
// () 表示分组,可以与 ? 组合
// 匹配 /abe 和 /abcde
app.get('/ab(cd)?e', (req, res) => {
res.send('ab(cd)?e 匹配成功');
});
Express 5 的变化:Express 5 不再支持字符串模式中的 ?、+、*、() 字符,这是出于安全考虑(防止 ReDoS 攻击)。你需要使用以下替代方案:
// Express 5 中使用新语法
// 可选部分使用大括号
app.get('/:file{.:ext}', (req, res) => {
// ext 是可选的,匹配 /file.txt 或 /file
res.send(`文件: ${req.params.file}, 扩展名: ${req.params.ext || '无'}`);
});
// 通配符必须有名称
app.get('/*splat', (req, res) => {
// 不匹配根路径 /,但匹配 /foo、/foo/bar 等
const path = req.params.splat.join('/');
res.send(`路径: ${path}`);
});
// 匹配所有路径包括根路径
app.get('/{*splat}', (req, res) => {
const path = req.params.splat?.join('/') || '';
res.send(`完整路径: /${path}`);
});
// 多个路径使用数组
app.get(['/discussion/:slug', '/page/:slug'], (req, res) => {
res.send(`页面: ${req.params.slug}`);
});
正则表达式路径
当需要更灵活的匹配时,可以使用正则表达式:
// 匹配任何包含 "a" 的路径
app.get(/a/, (req, res) => {
res.send('路径包含 a');
});
// /a → 匹配
// /banana → 匹配
// /xyz → 不匹配
// 匹配以 "fly" 结尾的路径
app.get(/.*fly$/, (req, res) => {
res.send('路径以 fly 结尾');
});
// /butterfly → 匹配
// /dragonfly → 匹配
// /butterflyman → 不匹配
// 匹配数字 ID
app.get(/^\/users\/(\d+)$/, (req, res) => {
const id = req.params[0]; // 注意:正则捕获组从 0 开始
res.send(`用户ID: ${id}`);
});
路由参数
路由参数是 URL 中的动态部分,用于捕获特定位置的值。
基本用法
// 定义路由参数 :userId 和 :bookId
app.get('/users/:userId/books/:bookId', (req, res) => {
// 通过 req.params 获取参数
res.json({
userId: req.params.userId,
bookId: req.params.bookId
});
});
// 请求: GET /users/34/books/8989
// 响应: { "userId": "34", "bookId": "8989" }
参数名称必须由"单词字符"组成([A-Za-z0-9_])。连字符(-)和点(.)会被按字面意思解析,可以用于构建有意义的 URL:
// 使用连字符分隔参数
app.get('/flights/:from-:to', (req, res) => {
res.json({
from: req.params.from,
to: req.params.to
});
});
// 请求: GET /flights/LAX-SFO
// 响应: { "from": "LAX", "to": "SFO" }
// 使用点分隔参数
app.get('/plantae/:genus.:species', (req, res) => {
res.json({
genus: req.params.genus,
species: req.params.species
});
});
// 请求: GET /plantae/Prunus.persica
// 响应: { "genus": "Prunus", "species": "persica" }
参数验证
可以在参数后添加正则表达式来限制匹配范围:
// 只匹配数字 ID
app.get('/users/:id(\\d+)', (req, res) => {
res.send(`用户ID必须是数字: ${req.params.id}`);
});
// GET /users/123 → 匹配
// GET /users/abc → 不匹配,返回 404
// 只匹配特定格式的用户名
app.get('/profile/:username([a-z]+)', (req, res) => {
res.send(`用户名: ${req.params.username}`);
});
// GET /profile/john → 匹配
// GET /profile/John123 → 不匹配
// 匹配 UUID 格式
app.get('/items/:uuid([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})', (req, res) => {
res.send(`UUID: ${req.params.uuid}`);
});
注意:在字符串中使用正则时,反斜杠需要双重转义,如 \\d+。
可选参数
使用 ? 可以让路由参数变为可选:
// 可选参数
app.get('/users/:id?', (req, res) => {
const id = req.params.id || 'default';
res.send(`用户ID: ${id}`);
});
// GET /users/123 → 用户ID: 123
// GET /users → 用户ID: default
查询参数
查询参数是 URL 中 ? 后面的部分,通过 req.query 访问:
app.get('/search', (req, res) => {
// 解构查询参数,提供默认值
const { q, page = 1, limit = 10, sort = 'desc' } = req.query;
res.json({
query: q,
pagination: {
page: parseInt(page),
limit: parseInt(limit)
},
sort
});
});
// 请求: GET /search?q=express&page=2&limit=20
// 响应: { "query": "express", "pagination": { "page": 2, "limit": 20 }, "sort": "desc" }
查询参数与路由参数的区别:
| 特性 | 路由参数 (req.params) | 查询参数 (req.query) |
|---|---|---|
| 位置 | URL 路径中 | ? 后面 |
| 必需性 | 必须匹配定义的模式 | 可选 |
| 示例 | /users/:id → /users/123 | /search?q=test |
| 访问方式 | req.params.id | req.query.q |
路由处理器
路由处理器可以是单个函数、多个函数或函数数组。
单个处理函数
app.get('/hello', (req, res) => {
res.send('Hello World');
});
多个处理函数
可以指定多个处理函数,它们会依次执行:
app.get('/example',
(req, res, next) => {
console.log('第一个处理函数');
next(); // 必须调用 next() 才会继续
},
(req, res, next) => {
console.log('第二个处理函数');
next();
},
(req, res) => {
console.log('第三个处理函数,发送响应');
res.send('处理完成');
}
);
// 控制台输出:
// 第一个处理函数
// 第二个处理函数
// 第三个处理函数,发送响应
函数数组
const validateUser = (req, res, next) => {
if (!req.body.name) {
return res.status(400).json({ error: '姓名必填' });
}
next();
};
const logRequest = (req, res, next) => {
console.log(`创建用户: ${req.body.name}`);
next();
};
const createUser = (req, res) => {
res.status(201).json({ name: req.body.name });
};
// 使用数组形式
app.post('/users', [validateUser, logRequest, createUser]);
// 也可以混合使用
app.post('/users2', validateUser, [logRequest, createUser]);
next('route') 跳过当前路由
在路由中间件中调用 next('route') 可以跳过当前路由的剩余处理函数:
app.get('/user/:id',
(req, res, next) => {
// 如果 id 是 '0',跳过后续处理
if (req.params.id === '0') {
return next('route');
}
next();
},
(req, res) => {
// 这个函数只有在 id 不是 '0' 时执行
res.send('普通用户: ' + req.params.id);
}
);
// 另一个匹配 /user/:id 的路由
app.get('/user/:id', (req, res) => {
// 当 id 是 '0' 时,前面的路由跳过了,会执行这里
res.send('特殊用户(ID 为 0)');
});
// GET /user/5 → "普通用户: 5"
// GET /user/0 → "特殊用户(ID 为 0)"
这种模式非常适合条件处理:
// 管理员和普通用户看到不同的页面
app.get('/dashboard',
(req, res, next) => {
if (req.user?.role === 'admin') {
return next('route'); // 管理员跳到下一个路由
}
next(); // 普通用户继续当前路由
},
(req, res) => {
res.render('user-dashboard'); // 普通用户页面
}
);
app.get('/dashboard', (req, res) => {
res.render('admin-dashboard'); // 管理员页面
});
app.route() 链式路由
app.route() 可以为同一个路径创建链式路由处理器,减少代码重复:
// 不使用链式路由(重复写路径)
app.get('/articles', listArticles);
app.post('/articles', createArticle);
app.put('/articles', updateArticle);
app.delete('/articles', deleteArticle);
// 使用链式路由(路径只写一次)
app.route('/articles')
.get((req, res) => {
res.send('获取文章列表');
})
.post((req, res) => {
res.send('创建文章');
})
.put((req, res) => {
res.send('更新文章');
})
.delete((req, res) => {
res.send('删除所有文章');
});
express.Router 模块化路由
express.Router 是一个完整的中间件和路由系统,常被称为"迷你应用"。使用它可以更好地组织路由代码。
创建路由模块
// routes/users.js
const express = require('express');
const router = express.Router();
// 路由级中间件(只对这个路由器生效)
router.use((req, res, next) => {
console.log('用户路由请求:', Date.now());
next();
});
// 定义路由
router.get('/', (req, res) => {
res.send('用户列表');
});
router.get('/:id', (req, res) => {
res.send(`用户详情: ${req.params.id}`);
});
router.post('/', (req, res) => {
res.send('创建用户');
});
module.exports = router;
挂载路由模块
// app.js
const express = require('express');
const usersRouter = require('./routes/users');
const productsRouter = require('./routes/products');
const app = express();
// 挂载路由器到特定路径
app.use('/users', usersRouter);
app.use('/products', productsRouter);
// 现在可以访问:
// GET /users → 用户列表
// GET /users/123 → 用户详情
// GET /products → 产品列表
mergeParams 选项
默认情况下,父路由的参数不会传递给子路由。如果需要在子路由中访问父路由参数,需要启用 mergeParams:
// routes/userPosts.js
const router = express.Router({ mergeParams: true });
router.get('/', (req, res) => {
// 可以访问父路由的 userId 参数
const { userId } = req.params;
res.send(`用户 ${userId} 的文章列表`);
});
module.exports = router;
// app.js
const userPostsRouter = require('./routes/userPosts');
// 父路由有 :userId 参数
app.use('/users/:userId/posts', userPostsRouter);
// GET /users/123/posts
// 响应: "用户 123 的文章列表"
如果不设置 mergeParams: true,子路由中的 req.params.userId 会是 undefined。
响应方法
响应对象 res 有多种方法来结束请求-响应周期:
| 方法 | 描述 |
|---|---|
res.send() | 发送各种类型的响应(字符串、对象、Buffer) |
res.json() | 发送 JSON 响应 |
res.jsonp() | 发送 JSONP 响应 |
res.status() | 设置响应状态码 |
res.redirect() | 重定向请求 |
res.render() | 渲染视图模板 |
res.sendfile() | 发送文件 |
res.download() | 提示下载文件 |
res.end() | 结束响应 |
// 发送文本
app.get('/text', (req, res) => {
res.send('文本响应');
});
// 发送 JSON
app.get('/json', (req, res) => {
res.json({ message: 'JSON 响应', status: 'success' });
});
// 设置状态码
app.get('/created', (req, res) => {
res.status(201).json({ id: 1, created: true });
});
// 重定向
app.get('/old', (req, res) => {
res.redirect('/new');
});
app.get('/external', (req, res) => {
res.redirect('https://expressjs.com');
});
// 文件下载
app.get('/download', (req, res) => {
res.download('/files/report.pdf');
});
// 发送文件
app.get('/file', (req, res) => {
res.sendFile('/files/index.html', { root: __dirname });
});
路由组织最佳实践
按功能模块组织
推荐的项目结构:
routes/
├── index.js # 路由入口,汇总所有路由
├── users.js # 用户相关路由
├── products.js # 产品相关路由
├── orders.js # 订单相关路由
└── auth.js # 认证相关路由
// routes/index.js
const express = require('express');
const router = express.Router();
const usersRouter = require('./users');
const productsRouter = require('./products');
const authRouter = require('./auth');
router.use('/auth', authRouter);
router.use('/users', usersRouter);
router.use('/products', productsRouter);
module.exports = router;
// app.js
const routes = require('./routes');
app.use('/api', routes);
// API 路径结构:
// /api/auth/login
// /api/users
// /api/products
RESTful API 设计
RESTful 是一种 API 设计风格,遵循约定可以简化开发:
| HTTP 方法 | 路径 | 描述 |
|---|---|---|
| GET | /users | 获取用户列表 |
| GET | /users/:id | 获取单个用户 |
| POST | /users | 创建用户 |
| PUT | /users/:id | 完整更新用户 |
| PATCH | /users/:id | 部分更新用户 |
| DELETE | /users/:id | 删除用户 |
// RESTful 用户 API
const router = express.Router();
// 获取列表
router.get('/', async (req, res) => {
const users = await User.find();
res.json(users);
});
// 获取单个
router.get('/:id', async (req, res) => {
const user = await User.findById(req.params.id);
if (!user) {
return res.status(404).json({ error: '用户不存在' });
}
res.json(user);
});
// 创建
router.post('/', async (req, res) => {
const user = await User.create(req.body);
res.status(201).json(user);
});
// 完整更新
router.put('/:id', async (req, res) => {
const user = await User.findByIdAndUpdate(
req.params.id,
req.body,
{ new: true, overwrite: true }
);
res.json(user);
});
// 部分更新
router.patch('/:id', async (req, res) => {
const user = await User.findByIdAndUpdate(
req.params.id,
{ $set: req.body },
{ new: true }
);
res.json(user);
});
// 删除
router.delete('/:id', async (req, res) => {
await User.findByIdAndDelete(req.params.id);
res.status(204).send();
});
版本控制
对于 API,版本控制是常见需求:
// 方式一:路径版本
app.use('/api/v1', v1Routes);
app.use('/api/v2', v2Routes);
// 方式二:头部版本
app.use((req, res, next) => {
const version = req.headers['accept-version'] || 'v1';
req.apiVersion = version;
next();
});
小结
本章学习了 Express 路由系统的核心概念:
- 路由基础:HTTP 方法、路径、处理函数的组合
- 路径模式:字符串、字符串模式、正则表达式
- 路由参数:动态 URL 参数的捕获和验证
- 查询参数:可选的 URL 查询字符串处理
- 路由处理器:单个、多个、数组形式的处理函数
- next('route'):条件跳过当前路由
- 模块化路由:使用 express.Router 组织代码
- RESTful 设计:遵循约定的 API 设计风格
路由是 Express 应用的骨架,合理组织路由结构能让代码更清晰、更易维护。
练习
- 创建一个 RESTful API,实现对"文章"资源的 CRUD 操作
- 使用
express.Router将用户路由和文章路由分离到不同文件 - 实现一个路由参数验证,确保用户 ID 是有效的 MongoDB ObjectId 格式
- 使用
next('route')实现管理员和普通用户访问同一路由时返回不同数据