3. 路由与请求处理
在 Express 中,路由是连接 HTTP 请求与业务处理逻辑的桥梁。一个完整的路由定义由三部分组成:HTTP 方法、路径模式和处理函数。理解这三者的协作方式,以及请求对象(req)和响应对象(res)各自承担的角色,是编写可维护 Web 服务的基础。
HTTP 方法与路由匹配
Express 为每种标准 HTTP 方法提供了对应的方法名:app.get()、app.post()、app.put()、app.patch()、app.delete() 等。方法名决定了该路由只响应特定类型的请求。例如,app.get('/users', handler) 只处理 GET 请求,而 app.post('/users', handler) 只处理 POST 请求。如果客户端用 POST 访问一个只注册了 GET 的路径,Express 会返回 404,而不是 405,这是初学者容易忽略的细节。
实际开发中,同一路径往往需要注册多个方法。例如用户资源通常需要:GET /users 获取列表、POST /users 创建用户、GET /users/:id 获取单个用户、PUT /users/:id 全量更新、PATCH /users/:id 部分更新、DELETE /users/:id 删除。这些路由可以分别注册,也可以使用 app.route('/users/:id') 链式写法集中管理。
路径参数:从 URL 中提取动态值
路径参数用于捕获 URL 中位置固定的动态片段。定义方式是在路径中使用冒号前缀,例如 /users/:id。当请求 /users/42 到达时,Express 会将 42 存入 req.params.id。路径参数的值始终是字符串类型,如果需要数字,必须显式转换。
一个路径中可以包含多个参数,例如 /users/:userId/posts/:postId,此时 req.params 会包含 { userId: '...', postId: '...' }。路径参数适合表达资源层级关系,但不适合传递可选配置或过滤条件,因为路径结构本身要求位置固定。
查询字符串:灵活的可选参数
查询字符串位于 URL 的 ? 之后,以键值对形式出现,例如 /search?keyword=express&page=2&limit=10。Express 会自动解析查询字符串并挂载到 req.query 上,得到 { keyword: 'express', page: '2', limit: '10' }。与路径参数一样,查询值默认是字符串,需要根据业务逻辑进行类型转换和默认值处理。
查询字符串的优势在于参数可选、顺序无关、数量灵活,非常适合分页、排序、过滤、搜索等场景。例如实现分页时,可以这样处理:
app.get('/articles', (req, res) => {
const page = parseInt(req.query.page, 10) || 1;
const limit = Math.min(parseInt(req.query.limit, 10) || 10, 50);
const offset = (page - 1) * limit;
// 使用 offset 和 limit 查询数据库
res.json({ page, limit, offset });
});
这里 parseInt 的第二个参数 10 明确指定十进制,避免以 0 开头的字符串被误解析为八进制。Math.min 用于限制最大每页条数,防止客户端请求过大的数据量。
请求对象 req 的核心属性
请求对象封装了客户端发来的全部信息。除了 req.params 和 req.query,最常用的还有:
req.body:包含解析后的请求体数据,需要配合express.json()或express.urlencoded()中间件才能使用。req.method:当前请求的 HTTP 方法,如'GET'、'POST'。req.path:URL 中不包含查询字符串的路径部分。req.headers:请求头对象,常用于读取content-type、authorization等。req.ip:客户端 IP 地址,在日志和限流中很有用。
需要注意的是,req.body 在没有配置 body 解析中间件时是 undefined,直接访问其属性会抛出异常。因此,在 POST 和 PUT 路由中,务必先确认中间件已正确挂载。
响应对象 res 的核心方法
响应对象负责向客户端发送数据并结束请求-响应周期。最常用的方法包括:
res.send():发送字符串、Buffer 或对象,Express 会根据内容自动设置 Content-Type。res.json():发送 JSON 响应,等价于res.send(JSON.stringify(obj))但更高效且自动设置正确的内容类型。res.status():设置 HTTP 状态码,通常与res.json()或res.send()链式调用。res.redirect():发送 302 重定向,也可指定 301 永久重定向。res.sendFile():发送文件,需要绝对路径。
一个常见的错误是在同一个处理函数中多次调用响应方法。例如先 res.send('ok') 再 res.json({}),第二次调用会抛出 "Cannot set headers after they are sent" 错误。正确的做法是确保每个请求路径只调用一次结束响应的方法,或者在条件分支中提前 return。
路由处理中的参数校验与错误响应
路径参数和查询字符串都来自客户端,不能直接信任。以 /users/:id 为例,如果 id 应该是数字,就需要校验:
app.get('/users/:id', (req, res) => {
const id = Number(req.params.id);
if (!Number.isInteger(id) || id <= 0) {
return res.status(400).json({ error: '无效的用户 ID' });
}
// 继续查询用户
res.json({ id, name: '示例用户' });
});
这里使用 Number.isInteger 而不是 isNaN,因为 Number('12abc') 返回 NaN,而 Number('12.5') 返回 12.5,只有整数才符合 ID 的语义。校验失败时返回 400 状态码和结构化错误信息,便于客户端定位问题。
请求-响应生命周期中的关键节点
下图展示了从请求进入 Express 到响应返回客户端的核心流程,以及路径参数、查询字符串、请求体在其中的位置:
图中清晰地区分了正常路径和校验失败分支。正常路径中,请求先经过路由匹配,再解析参数和查询字符串,然后进入处理函数,最终通过响应对象返回数据。校验失败时,处理函数提前返回 400 响应,不再执行后续查询逻辑。这种“先校验、后处理”的模式能有效避免无效数据进入业务层。
路径参数与查询字符串的选择原则
两者都能传递数据,但适用场景不同。路径参数用于标识资源本身,例如 /users/:id 中的 id 是资源的唯一标识,缺少它就无法定位资源。查询字符串用于修饰资源的呈现方式,例如 /users?role=admin&sort=name 中的 role 和 sort 是过滤和排序条件,即使省略,请求仍然有意义。
一个实用的判断标准是:如果参数是“必须的、用于定位资源的”,用路径参数;如果是“可选的、用于过滤或分页的”,用查询字符串。遵循这个原则,API 的语义会更清晰,客户端也更容易理解和使用。
* 不再直接使用,需要改为命名通配符 /*splat。如果项目从 Express 4 升级,需要检查所有使用通配符的路由定义。
综合示例:一个完整的用户查询路由
下面这个例子综合运用了本节的核心知识点,包括路径参数校验、查询字符串默认值、请求体读取和多种响应方法:
const express = require('express');
const app = express();
app.use(express.json());
app.get('/users/:id', (req, res) => {
const id = Number(req.params.id);
if (!Number.isInteger(id) || id <= 0) {
return res.status(400).json({ error: '用户 ID 必须为正整数' });
}
const includePosts = req.query.include === 'posts';
const user = { id, name: '示例用户', email: 'user@example.com' };
if (includePosts) {
user.posts = [{ id: 1, title: 'Express 路由入门' }];
}
res.json(user);
});
app.post('/users', (req, res) => {
const { name, email } = req.body;
if (!name || !email) {
return res.status(422).json({ error: 'name 和 email 为必填项' });
}
res.status(201).json({ id: 100, name, email });
});
app.listen(3000);
运行后,访问 GET /users/42?include=posts 会返回包含 posts 数组的用户对象;访问 GET /users/abc 会返回 400 和错误信息;向 POST /users 发送缺少 email 的 JSON 体会返回 422。这些行为都可以通过 curl 或 Postman 直接验证,是理解请求对象和响应对象协作方式的有效手段。
req.body 只有在请求头包含 Content-Type: application/json 且已挂载 express.json() 时才会被正确解析。如果客户端发送表单数据但未设置正确的 Content-Type,req.body 会是空对象,导致校验逻辑误判。
掌握 HTTP 方法、路径参数、查询字符串以及 req/res 的核心用法后,就可以开始构建结构清晰、行为可预测的 RESTful 接口。下一阶段需要关注的是中间件机制,它决定了请求在到达路由处理函数之前会经历哪些处理步骤。