跳到主要内容

路由处理

路由是指确定应用程序如何响应客户端对特定端点的请求。每个路由可以有一个或多个处理函数,当匹配到请求时执行。本章将详细介绍 Express 路由系统的工作原理和使用方法。

路由基础

什么是路由?

在 Express 中,一个路由由三部分组成:

  1. HTTP 方法:如 GET、POST、PUT、DELETE 等
  2. 路径(URL 模式):如 /users/users/:id
  3. 处理函数:一个或多个回调函数

当服务器收到一个请求时,Express 会根据请求的 HTTP 方法和 URL 路径来匹配路由,找到匹配的路由后执行对应的处理函数。

基本语法

app.METHOD(PATH, HANDLER)
  • app:Express 应用实例
  • METHOD:HTTP 方法(小写),如 getpostputdelete
  • PATH: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.idreq.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 路由系统的核心概念:

  1. 路由基础:HTTP 方法、路径、处理函数的组合
  2. 路径模式:字符串、字符串模式、正则表达式
  3. 路由参数:动态 URL 参数的捕获和验证
  4. 查询参数:可选的 URL 查询字符串处理
  5. 路由处理器:单个、多个、数组形式的处理函数
  6. next('route'):条件跳过当前路由
  7. 模块化路由:使用 express.Router 组织代码
  8. RESTful 设计:遵循约定的 API 设计风格

路由是 Express 应用的骨架,合理组织路由结构能让代码更清晰、更易维护。

练习

  1. 创建一个 RESTful API,实现对"文章"资源的 CRUD 操作
  2. 使用 express.Router 将用户路由和文章路由分离到不同文件
  3. 实现一个路由参数验证,确保用户 ID 是有效的 MongoDB ObjectId 格式
  4. 使用 next('route') 实现管理员和普通用户访问同一路由时返回不同数据