Skip to content
从 req 到 res:请求-响应循环中的两个核心对象
在 Express 应用中,每个 HTTP 请求到达时,框架会实例化两个对象:req(请求对象)和 res(响应对象)。这两个对象贯穿整个请求-响应循环,作为中间件和路由处理器操作的核心接口。req 封装了客户端发送的请求信息,包括查询参数、路径参数、请求头等;res 提供了构建和发送响应的各种方法,如设置状态码、发送不同格式的内容、触发重定向等。
读取查询参数:req.query
查询字符串是 URL 中 ? 之后的部分,格式为 key=value 键值对,多个键值对用 & 分隔。req.query 是一个对象,Express 使用解析库对查询字符串进行解码并填充该对象。当查询字符串中没有内容时,req.query 是空对象 {}。
基本用法
typescript
import express from 'express';
const app = express();
app.get('/search', (req, res) => {
console.log(req.query);
res.send(`查询关键词: ${req.query.q}`);
});
app.listen(3000);访问 /search?q=express,终端输出 { q: 'express' },客户端收到 查询关键词: express。
如果查询字符串包含数组格式的键,例如 ?tags=node&tags=typescript,则 req.query.tags 会被解析为数组 ['node', 'typescript']。嵌套对象格式 ?user[name]=Alice 会被解析为 { user: { name: 'Alice' } }。该行为取决于 query parser 的配置,默认使用 qs 库支持复杂解析。
注意点
- 查询参数始终是可选的。路由
/search可以接收不带?q的请求,此时req.query.q为undefined,业务逻辑中需要判断是否存在。 req.query中的所有值都是字符串或字符串数组(除非配置了自定义解析器)。与请求体不同,查询参数不会自动转换为数字或布尔值。例如?count=5,req.query.count是字符串'5',需要手动转换类型。
捕获路径参数:路由占位符与 req.params
路径参数是 URL 路径中的一部分,用于标识特定资源。在路由定义中,使用 : 前缀表示占位符,占位符与其后的内容被捕获为命名参数,挂载到 req.params 对象上。
定义路由占位符
typescript
app.get('/user/:id', (req, res) => {
res.send(`用户ID: ${req.params.id}`);
});请求 /user/101 时,req.params.id 的值为字符串 '101'。可以连续定义多个占位符:
typescript
app.get('/post/:year/:month/:slug', (req, res) => {
// 访问 /post/2024/12/express-guide
console.log(req.params.year); // '2024'
console.log(req.params.month); // '12'
console.log(req.params.slug); // 'express-guide'
});路径参数与查询参数的区别
路径参数是路由的一部分,用于匹配该路由的 URL 结构;查询参数则附加在 URL 后面,不影响路由匹配。例如 /user/101?show=profile 中,101 是路径参数,show=profile 是查询参数,两者分别通过 req.params 和 req.query 获取。
注意点
- 路径参数总是字符串。如果需要在应用中使用数字,需要显式转换,例如
Number(req.params.id)。无效的值转换后为NaN,需要处理。 - 多个路由可能匹配同一个 URL,但路径参数定义会接受任何非空内容。对于可选参数,Express 不直接支持,通常通过定义多个路由实现,或者使用中间件处理。
读取请求头:req.headers 与 req.get()
请求头以键值对形式携带客户端环境、认证凭证、内容类型等信息。Express 提供了两种读取请求头的方式:req.headers 和 req.get()。
req.headers
req.headers 直接继承自 Node.js 的 http.IncomingMessage,是一个普通对象,包含所有请求头。Node.js 会把请求头名称统一转为小写,因此直接使用 req.headers['content-type']、req.headers['user-agent'] 等键名访问是可靠的。
typescript
app.get('/headers', (req, res) => {
console.log(req.headers['content-type']);
console.log(req.headers['user-agent']);
res.send('查看控制台');
});多值请求头(如 Set-Cookie)可能以字符串或数组形式出现,具体取决于 Node.js 版本和解析器的行为。HTTP/2 的伪头部(如 :method)不会直接出现在 req.headers 中。
req.get()
req.get(field) 是 Express 提供的辅助方法,不区分大小写地检索请求头的值。对于需要忽略大小写差异的场景,它比直接访问 req.headers 更方便。
typescript
app.get('/headers', (req, res) => {
const contentType = req.get('Content-Type'); // 大小写不敏感
const auth = req.get('Authorization');
res.json({ contentType, auth });
});如果请求头不存在,req.get() 返回 undefined。对于具有多个值的头部,它仅返回第一个值,这与 req.headers 直接访问可能得到数组的行为不同。
实际使用中,req.get('Referer') 和 req.headers['referer'] 都能正确获取值,Node.js 已完成名称的小写规范化,不存在因拼写变体导致取值缺失的问题。两者的区别仅在于大小写敏感性:req.get() 可以接受 Referer、referer、REFERER 等任意大小写形式。
发送响应内容:res.send() 的自动行为
res.send() 是 Express 中最常用的响应发送方法,根据传入内容的类型自动设置 Content-Type 头部并处理数据。
基本行为
typescript
app.get('/text', (req, res) => {
res.send('一段纯文本');
});当参数是字符串时,Express 设置 Content-Type 为 text/html 并发送。如果希望发送纯文本,可以先调用 res.type('text/plain') 再发送。
res.send() 还会自动计算并设置 Content-Length 头部。
发送对象或数组时,Express 会调用 JSON.stringify() 将数据序列化,并将 Content-Type 设置为 application/json。
typescript
app.get('/data', (req, res) => {
res.send({ status: 'ok' });
});状态码控制
res.send() 默认状态码为 200。如果需要不同的状态码,可以与 res.status() 链式调用:
typescript
app.post('/resource', (req, res) => {
res.status(201).send('资源已创建');
});注意点
res.send()是一个终结方法,调用后响应结束,后续的write或send会触发错误Cannot set headers after they are sent to the client。- 如果传入数值,Express 会将其识别为状态码,但
res.send(404)发送的是字符串'404'而非按状态码解释。要设置状态码并发送消息,需要分两步:res.status(404).send('未找到')。 - 发送 Buffer 时,
Content-Type设置为application/octet-stream。
返回 JSON 数据:res.json()
res.json() 专门用于发送 JSON 格式的响应。它与 res.send() 处理对象的行为相似,但内部实现有差异:res.json() 通过 JSON.stringify() 转换参数,并在配置了 json replacer 或 json spaces 时应用这些设置(通过 app.set('json spaces', 2) 等配置)。
基本用法
typescript
app.get('/api/users', (req, res) => {
res.json([{ id: 1, name: 'Alice' }]);
});该方法强制将 Content-Type 设置为 application/json,无论参数是对象、数组还是 null。
与 res.send() 的区别
res.json()只接受可 JSON 序列化的值,并且总是返回 JSON。如果传入字符串,res.json('hello')会发送 JSON 字符串"hello"(带双引号),而res.send('hello')发送不带引号的普通字符串。对于 API 端点,res.json()更能保证响应格式的一致性。res.json()会使用json replacer和json spaces配置(如果设置),res.send()不会应用这些配置。
状态码
与 res.send() 一样,可以链式调用 res.status() 设置状态码:
typescript
app.post('/api/users', (req, res) => {
res.status(201).json({ id: 3, name: 'Created' });
});设置状态码:res.status() 与链式调用
res.status() 方法用于设置 HTTP 响应状态码,返回自身的 res 对象,支持链式调用。
基本用法
typescript
app.get('/not-found', (req, res) => {
res.status(404);
res.send('资源不存在');
});
// 等价于链式写法:
app.get('/not-found', (req, res) => {
res.status(404).send('资源不存在');
});链式调用将响应头设置和内容发送写在一起,减少中间状态出错的概率。
HTTP 状态码的意义
状态码用于告知客户端请求的处理结果。常见分类:
- 2xx 成功,如
200 OK、201 Created、204 No Content。 - 3xx 重定向,如
301 Moved Permanently、302 Found。 - 4xx 客户端错误,如
400 Bad Request、401 Unauthorized、403 Forbidden、404 Not Found。 - 5xx 服务端错误,如
500 Internal Server Error。
res.status() 可以设置任意有效的 HTTP 状态码,框架不会校验其是否合理。在 RESTful API 设计中,应根据操作结果返回适当的状态码。
快捷方式
Express 提供了 res.sendStatus() 快捷方法。例如 res.sendStatus(404) 会发送状态码并把描述字符串作为响应体(Not Found),等价于 res.status(404).send('Not Found'),但无法自定义消息。
实现重定向:res.redirect()
res.redirect() 方法向客户端发送重定向响应,默认状态码为 302 Found。客户端收到后会根据状态码和 Location 头部发起新的请求。
基本用法
typescript
app.get('/old-path', (req, res) => {
res.redirect('/new-path');
});浏览器收到 302 后跳转到 /new-path。可以指定完全限定 URL:
typescript
app.get('/external', (req, res) => {
res.redirect('https://example.com');
});指定状态码
通过第一个参数指定重定向类型:
typescript
// 永久重定向
app.get('/permanent', (req, res) => {
res.redirect(301, '/new-location');
});内部实现
res.redirect() 内部会设置 Location 头部,并自动构造完整的 URL(根据请求协议和主机)。如果传入的是相对路径,Express 会基于当前请求的 URL 进行解析。在使用反向代理时,协议和主机的识别可能受影响,可以通过 app.set('trust proxy', true) 让 Express 信任代理传递的 X-Forwarded-Proto 和 X-Forwarded-Host 头部,以生成正确的重定向 URL。
注意点
- 重定向必须在发送任何响应体之前调用,否则会抛出头部已发送的错误。
- 在 AJAX 请求的响应中,重定向会被 XMLHttpRequest 自动跟随,客户端无法直接感知。通常需要服务端返回特定状态码(如
401)而非重定向来处理身份验证失败等情况。
实战串联:构建一个 Echo 接口
Echo 接口接收客户端的查询参数、路径参数、请求头,并将这些信息汇总后以 JSON 格式返回。
Express 4.x 中,路由参数的可选写法 /:resource? 在 app.all 中并不生效。准确的实现方式是分别定义有参数和无参数的路由,或者将 echo 逻辑提取为函数通过中间件处理。
typescript
import express from 'express';
const app = express();
function buildEchoData(req) {
return {
query: req.query,
params: req.params,
headers: {
'user-agent': req.get('User-Agent'),
'content-type': req.get('Content-Type'),
'authorization': req.get('Authorization'),
},
method: req.method,
url: req.originalUrl,
};
}
app.all('/echo/:resource', (req, res) => {
res.json(buildEchoData(req));
});
app.all('/echo', (req, res) => {
res.json(buildEchoData(req));
});
app.listen(3000, () => {
console.log('Echo server running on port 3000');
});请求 /echo/book?sort=desc 时,返回类似以下 JSON:
json
{
"query": { "sort": "desc" },
"params": { "resource": "book" },
"headers": {
"user-agent": "curl/7.68.0",
"content-type": null,
"authorization": null
},
"method": "GET",
"url": "/echo/book?sort=desc"
}如果请求路径不包含路径参数,例如 /echo?q=test,req.params 为空对象 {}。
关键方法速览与注意点
req 对象常用属性/方法
| 属性/方法 | 说明 |
|---|---|
req.query | 解析后的查询字符串对象,默认 {} |
req.params | 路由占位符映射对象,例如 { id: '101' } |
req.headers | 请求头对象,键名小写 |
req.get(field) | 获取指定请求头,不区分大小写,返回字符串或 undefined |
req.method | HTTP 方法字符串,如 'GET'、'POST' 等 |
req.path | 去掉查询字符串后的路径部分 |
req.originalUrl | 原始请求 URL(包含查询字符串),与在中间件中可能被修改的 req.url 不同 |
res 对象常用方法
| 方法 | 说明 |
|---|---|
res.send(body) | 发送响应,自动设置 Content-Type,终结响应 |
res.json(body) | 发送 JSON 响应,设置 Content-Type 为 application/json |
res.status(code) | 设置响应状态码,返回 res 自身以支持链式调用 |
res.redirect([status,] path) | 重定向请求,默认 302,可指定状态码 |
res.type(type) | 设置 Content-Type,例如 res.type('text/plain') |
res.end([data]) | 结束响应过程,可选输出数据,用于快速结束不发送内容的情况 |
注意点
- 一次请求只能发送一次响应。在调用
res.send()、res.json()或res.redirect()后,必须确保后续代码不会再次触发响应发送,否则引发ERR_HTTP_HEADERS_SENT错误。在多分支条件中,使用return避免继续执行是常见的防御性编程手法。 - 请求头设置后不可修改。状态码和响应头一旦发送到客户端,后续任何尝试修改都会报错。可以通过
res.headersSent属性检查头部是否已发送。 - 在处理中间件链时,如果当前中间件不打算终结请求,必须调用
next()。读取req和res的方法在所有中间件中一致。 - 对
req.query的解析默认使用qs库,支持嵌套对象和数组格式。如果不需要,可以用app.set('query parser', 'simple')切换为 Node.js 内置的querystring模块,从而禁用嵌套对象解析。 req.params的值针对每个路由独立生成。如果一个请求通过多个路径匹配的中间件,req.params会在不同中间件中反映各自定义的占位符,而不是全局不变。通常仅在路由处理器中使用。
小结
本篇梳理了 Express 请求对象 req 和响应对象 res 的核心操作:从 req.query、req.params、req.get() 读取请求数据,到使用 res.send()、res.json()、res.status()、res.redirect() 构建和发送响应。通过一个 Echo 接口将这些方法串联起来,展示了它们在单个请求处理中的配合方式。
下一篇聚焦 Express 的内置中间件和 Router:如何使用 express.json()、express.urlencoded() 解析请求体,如何用 express.static() 提供静态文件服务,以及如何利用 express.Router 将路由模块化,构建更清晰的应用结构。
