Express.js 路由
1. 路由基本语法
路由的结构
一个基本的路由定义由三部分组成:app.METHOD(PATH, HANDLER)
:
app
:express
的一个实例。METHOD
: 一个小写的 HTTP 请求方法,如get
,post
,put
,delete
等。PATH
: 服务器上的路径(端点)。HANDLER
: 当路由匹配时执行的处理器函数。
const express = require('express');
const app = express();
app.get('/', (req, res) => {
res.send('Homepage!');
});
路由路径
字符串路径
这是最简单的形式,要求 URL 路径与定义的字符串完全匹配。
// 仅匹配 /about
app.get('/about', handler);
// 仅匹配 /random.text
app.get('/random.text', handler);
路径模式
Express 借助 path-to-regexp
库,允许在路径字符串中使用一些特殊字符来实现更灵活的匹配:
?
: 将其前面的字符标记为可选。/ab?cd
会匹配acd
和abcd
。+
: 匹配其前面字符的一次或多次重复。/ab+cd
会匹配abcd
,abbcd
,abbbcd
等。*
: 匹配其前面字符的任意次数(包括零次)重复,或作为通配符匹配任意字符序列。ab*cd
会匹配abcd
,abxcd
,abRANDOMcd
等。()
: 将一组字符标记为群组。例如,/fly(ing)?/
会匹配/fly
和/flying
。
// 匹配 /users 和 /user
app.get('/users?', handler);
// 匹配所有路径,通常用作 404 错误处理,必须放在最后
app.get('* ', (req, res) => {
res.status(404).send('Page Not Found');
});
正则表达式
为了实现最复杂的匹配逻辑,可以直接使用正则表达式作为路径。
// 匹配任何包含 "a" 的路径
app.get(/a/, handler);
// 匹配 butterfly 和 dragonfly,但不匹配 butterflyman, dragonflyman 等
app.get(/.*fly$/, handler);
路由顺序
Express 按顺序匹配路由。请求将由第一个匹配其路径和方法的规则来处理。因此,路由的定义顺序至关重要。
// 错误示范:通配符路由在前
// 所有 GET 请求都会被这个路由捕获,后面的 /about 永远不会被匹配
app.get('/:generic', (req, res) => {
res.send(`Generic page for: ${req.params.generic}`)
});
app.get('/about', (req, res) => {
res.send('About page'); // 这个路由永远不会执行
});
// 正确示范:将更具体的路由放在前面
app.get('/about', (req, res) => {
res.send('About page');
});
app.get('/:generic', (req, res) => {
res.send(`Generic page for: ${req.params.generic}`)
});
路由参数
路由参数是 URL 中用于捕获动态值的命名段。这些值会被填充到 req.params
对象中。通过在路径段前加上冒号 :
来定义路由参数。
// :userId 是一个路由参数
app.get('/users/:userId', (req, res) => {
// 如果 URL 是 /users/123, req.params 将是 { userId: '123' }
res.send(`User profile for user ID: ${req.params.userId}`);
});
一个路径中可以包含多个路由参数。
// 匹配 /users/123/books/456
app.get('/users/:userId/books/:bookId', (req, res) => {
// req.params 将是 { userId: '123', bookId: '456' }
const { userId, bookId } = req.params;
res.send(`User ${userId} requested book ${bookId}.`);
});
查询字符串
查询字符串是 URL 中 ?
之后的部分,用于传递可选的键值对。它们不属于路由路径的一部分,而是作为附加数据。
Express 会自动解析查询字符串,并将其填充到 req.query
对象中。
// 请求 URL: /search?q=express&limit=10
app.get('/search', (req, res) => {
// req.query 将是 { q: 'express', limit: '10' }
const { q, limit } = req.query;
res.send(`Searching for "${q}" with a limit of ${limit}.`);
});
如果同一个键在查询字符串中出现多次,Express 会将这些值收集到一个数组中。
// 请求 URL: /articles?sort=date&sort=likes
app.get('/articles', (req, res) => {
// req.query 将是 { sort: ['date', 'likes'] }
res.send(`Sorting by: ${req.query.sort.join(', ')}`);
});
2. 路由处理器
处理器是可以处理请求的函数。一个路由可以有一个或多个处理器。
多个处理器
我们可以为一个路由提供多个回调函数作为处理器。它们就像微型中间件,必须调用 next()
才能将控制权传递给下一个处理器。
const handler1 = (req, res, next) => {
console.log('First handler: validating request...');
req.isValid = true; // 可以向 req 对象添加属性
next();
}
const handler2 = (req, res) => {
console.log('Second handler: sending response...');
res.send(`Request is valid: ${req.isValid}`);
}
app.get('/chained', [handler1, handler2]);
app.route()
方法
这是一个非常有用的工具,可以为单个路由路径创建可链接的处理器。它避免了为同一个路径重复书写多次 app.get
, app.post
等。
app.route('/book')
.get((req, res) => {
res.send('Get a random book');
})
.post((req, res) => {
res.send('Add a book');
})
.put((req, res) => {
res.send('Update the book');
});
3. 模块化路由
引入
随着应用规模的增长,将所有路由都放在一个文件中会变得难以管理。express.Router
是一个可插拔的、迷你的 Express 应用,它允许我们将路由逻辑拆分到不同的文件中,实现模块化。这种架构方式有如下优点:
- 组织性: 将相关的路由(如所有与用户相关的路由)分组到同一个文件中。
- 可重用性: 可以在不同的主应用中挂载和重用路由模块。
- 关注点分离: 主文件
app.js
只负责配置和挂载,而路由文件只关心具体的路由逻辑。
创建和使用 Router
- 创建路由文件:
// routes/userRouter.js
const express = require('express');
const router = express.Router();
// 中间件,仅对该 router 生效
router.use((req, res, next) => {
console.log('Time: ', Date.now());
next();
});
// 定义用户相关的路由
// 注意:这里的路径是相对于挂载点的
router.get('/', (req, res) => {
res.send('Users homepage');
});
router.get('/:userId', (req, res) => {
res.send(`Profile for user ${req.params.userId}`);
});
module.exports = router;
- 在主文件中挂载 Router:
// app.js
const express = require('express');
const app = express();
const userRouter = require('./routes/userRouter');
// 将所有 /users 开头的请求都交给 userRouter 处理
app.use('/users', userRouter);
app.listen(3000);
挂载后,路由的最终路径是挂载点路径和路由文件内路径的组合。
app.use('/users', userRouter)
router.get('/:userId', ...)
… 两者结合起来,最终匹配的完整路径是 /users/:userId
。