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。