路由是 Web 框架的核心功能之一,它决定了应用程序如何响应客户端对特定端点(URI)的请求。Express 提供了强大而灵活的路由系统,能够轻松处理各种 HTTP 方法和复杂的 URL 模式。本章将深入讲解路由的定义、参数获取、模块化组织等基础知识。
路由由三部分组成:HTTP 方法(如 GET、POST)、路径(如 /users)和处理函数(也称为控制器)。当客户端发送一个请求,Express 会根据请求的方法和路径匹配到对应的处理函数来生成响应。
在 Express 中,路由的基本形式如下:
app.METHOD(PATH, HANDLER);
其中:
app 是 Express 应用实例。METHOD 是小写的 HTTP 方法,例如 get、post、put、delete 等。PATH 是服务器上的路径(可以是字符串、字符串模式或正则表达式)。HANDLER 是匹配到路由时执行的函数。Express 为常见的 HTTP 方法提供了对应的方法,最常用的包括:
// GET 请求 - 获取资源
app.get('/users', (req, res) => {
res.send('获取用户列表');
});
// POST 请求 - 创建资源
app.post('/users', (req, res) => {
res.send('创建新用户');
});
// PUT 请求 - 更新资源(全量替换)
app.put('/users/:id', (req, res) => {
res.send(`更新用户 ${req.params.id}`);
});
// DELETE 请求 - 删除资源
app.delete('/users/:id', (req, res) => {
res.send(`删除用户 ${req.params.id}`);
});
// PATCH 请求 - 部分更新资源
app.patch('/users/:id', (req, res) => {
res.send(`部分更新用户 ${req.params.id}`);
});
// 还有 app.all() 用于匹配所有 HTTP 方法
app.all('/secret', (req, res) => {
res.send('所有方法都会响应');
});
路由路径定义了请求的 URL 匹配规则。Express 支持三种方式定义路径:字符串、字符串模式和正则表达式。
最简单的形式,直接使用字符串匹配完整路径。
// 匹配根路径
app.get('/', (req, res) => { res.send('root'); });
// 匹配 /about
app.get('/about', (req, res) => { res.send('about'); });
路径字符串中可以包含一些特殊字符,如 ?、+、* 和 (),它们基于正则表达式的子集。
// 匹配 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'); });
可以使用正则表达式进行更复杂的匹配,例如:
// 匹配任何路径中包含 "fly" 的请求
app.get(/fly/, (req, res) => { res.send('包含 fly'); });
// 匹配 /user/12345 或 /user/abcde(4个或更多数字/字母?)
app.get(/^\/user\/[a-zA-Z0-9]{4,}$/, (req, res) => { res.send('匹配用户模式'); });
路由参数是 URL 路径中的命名段,用于捕获在 URL 中的动态值。捕获的值会填充到 req.params 对象中,参数名作为其键。
定义路由参数时,在路径中使用 : 加参数名:
app.get('/users/:userId', (req, res) => {
res.send(`用户 ID: ${req.params.userId}`);
});
// 多个参数
app.get('/users/:userId/books/:bookId', (req, res) => {
res.json(req.params); // 例如 { userId: '123', bookId: '456' }
});
路由参数只能由字母、数字、下划线组成,它们的值只能是字符串。如果需要更精确的类型限制,可以在处理函数中进行类型转换或验证。
此外,连字符 - 和点 . 也可以被解释为普通字符,所以参数可以这样使用:
app.get('/flights/:from-:to', (req, res) => {
res.send(`从 ${req.params.from} 到 ${req.params.to}`);
});
查询参数是 URL 中 ? 后面的键值对,Express 会自动解析它们并放入 req.query 对象中。
// 请求 GET /search?q=express&page=2
app.get('/search', (req, res) => {
console.log(req.query); // { q: 'express', page: '2' }
res.send(`搜索: ${req.query.q}, 页码: ${req.query.page}`);
});
如果没有提供查询参数,req.query 是一个空对象 {}。查询参数的值始终是字符串,如果需要数字需要手动转换。
使用 app.route() 可以为同一个路径定义多个请求方法的处理,避免重复写路径,使代码更简洁。
app.route('/users')
.get((req, res) => {
res.send('获取用户列表');
})
.post((req, res) => {
res.send('创建用户');
})
.put((req, res) => {
res.send('更新用户');
});
当应用变得复杂时,将所有路由定义在同一个文件中会变得难以维护。Express 提供了 express.Router 类来创建模块化、可挂载的路由处理程序,类似于“迷你应用”。
例如,创建一个 routes/users.js 文件:
const express = require('express');
const router = express.Router();
// 该路由级别的中间件,仅在 /users 下生效
router.use((req, res, next) => {
console.log('Time: ', Date.now());
next();
});
// 定义路由
router.get('/', (req, res) => {
res.send('用户列表');
});
router.post('/', (req, res) => {
res.send('创建用户');
});
router.get('/:id', (req, res) => {
res.send(`用户详情: ${req.params.id}`);
});
module.exports = router;
在主文件(如 app.js)中引入并使用该路由模块:
const userRouter = require('./routes/users');
app.use('/users', userRouter); // 将路由挂载到 /users 路径
现在,访问 /users/ 就会由 userRouter 处理。这种模式让代码结构清晰,便于团队协作和维护。
路由处理函数可以接受第三个参数 next,用于将控制权传递给下一个中间件或路由。这在需要多个处理函数处理同一个路由时非常有用。
const cb0 = (req, res, next) => {
console.log('第一个处理函数');
next(); // 调用下一个
};
const cb1 = (req, res, next) => {
console.log('第二个处理函数');
next();
};
app.get('/example', [cb0, cb1], (req, res) => {
res.send('最后响应');
});
也可以将多个处理函数作为数组传递,或者混合数组和函数。
由于 Express 会按照定义顺序匹配路由,如果没有找到任何匹配的路由,应该返回 404。通常在所有路由之后添加一个中间件来处理 404:
// 放在所有路由之后
app.use((req, res, next) => {
res.status(404).send('Sorry, cannot find that!');
});
下面是一个结合了上述知识点的完整示例,包含路由参数、查询参数、链式路由和模块化。
app.js
const express = require('express');
const app = express();
const userRouter = require('./routes/users');
app.use(express.json()); // 解析 JSON 请求体
app.use('/users', userRouter); // 挂载用户路由
// 根路由
app.get('/', (req, res) => {
res.send('Home Page');
});
// 404 处理
app.use((req, res) => {
res.status(404).send('404 Not Found');
});
app.listen(3000, () => console.log('Server running on port 3000'));
routes/users.js
const express = require('express');
const router = express.Router();
// 模拟用户数据
let users = [
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' }
];
// 获取所有用户,支持查询参数过滤(例如 ?name=Alice)
router.get('/', (req, res) => {
const { name } = req.query;
if (name) {
const filtered = users.filter(u => u.name.includes(name));
res.json(filtered);
} else {
res.json(users);
}
});
// 获取单个用户
router.get('/:id', (req, res) => {
const id = parseInt(req.params.id);
const user = users.find(u => u.id === id);
if (user) {
res.json(user);
} else {
res.status(404).json({ error: 'User not found' });
}
});
// 创建用户
router.post('/', (req, res) => {
const newUser = {
id: users.length + 1,
name: req.body.name
};
users.push(newUser);
res.status(201).json(newUser);
});
// 更新用户(全量更新)
router.put('/:id', (req, res) => {
const id = parseInt(req.params.id);
const user = users.find(u => u.id === id);
if (!user) return res.status(404).json({ error: 'User not found' });
user.name = req.body.name;
res.json(user);
});
// 删除用户
router.delete('/:id', (req, res) => {
const id = parseInt(req.params.id);
const index = users.findIndex(u => u.id === id);
if (index === -1) return res.status(404).json({ error: 'User not found' });
users.splice(index, 1);
res.status(204).send();
});
module.exports = router;