请求响应
在 Express 应用中,每个路由处理函数都会接收两个核心对象:请求对象(req)和响应对象(res)。理解这两个对象的属性和方法,是构建 Web 应用的基础。
请求对象 (req)
请求对象代表了 HTTP 请求,包含了客户端发送的所有信息。它是 Node.js 原生 IncomingMessage 对象的扩展。
常用属性一览
| 属性 | 说明 | 示例 |
|---|---|---|
req.body | 请求体数据(需要解析中间件) | { name: 'John' } |
req.params | 路由参数 | { id: '123' } |
req.query | 查询字符串参数 | { page: '1', limit: '10' } |
req.headers | 请求头对象 | { 'content-type': 'application/json' } |
req.method | HTTP 方法 | 'GET', 'POST' |
req.url | 请求 URL 路径 | '/users?page=1' |
req.path | URL 路径部分(不含查询字符串) | '/users' |
req.protocol | 协议 | 'http' 或 'https' |
req.hostname | 主机名 | 'localhost' |
req.ip | 客户端 IP 地址 | '127.0.0.1' |
req.cookies | Cookie 对象(需要 cookie-parser) | { sessionId: 'abc123' } |
路由参数 (req.params)
路由参数是 URL 路径中的动态部分,通过冒号 : 定义:
// 定义带参数的路由
app.get('/users/:userId/books/:bookId', (req, res) => {
// req.params 包含所有路由参数
const { userId, bookId } = req.params;
res.json({
message: `用户 ${userId} 的书籍 ${bookId}`
});
});
// GET /users/123/books/456
// req.params = { userId: '123', bookId: '456' }
路由参数的值总是字符串类型。如果需要数字,必须手动转换:
app.get('/users/:id', (req, res) => {
const id = parseInt(req.params.id, 10); // 转换为数字
if (isNaN(id)) {
return res.status(400).json({ error: 'ID 必须是数字' });
}
// 使用 id 查询数据库...
});
查询参数 (req.query)
查询参数是 URL 中 ? 后面的部分:
app.get('/search', (req, res) => {
// req.query 包含所有查询参数
const { q, page = '1', limit = '10' } = req.query;
res.json({
keyword: q,
pagination: {
page: parseInt(page),
limit: parseInt(limit)
}
});
});
// GET /search?q=express&page=2&limit=20
// req.query = { q: 'express', page: '2', limit: '20' }
查询参数也总是字符串类型。如果参数有多个相同名称,会变成数组:
// GET /filter?category=books&category=music
app.get('/filter', (req, res) => {
console.log(req.query.category); // ['books', 'music']
});
请求体 (req.body)
req.body 包含请求体中的数据,但需要先使用解析中间件:
// 解析 JSON 格式的请求体
app.use(express.json());
// 解析 URL 编码的请求体(表单数据)
app.use(express.urlencoded({ extended: true }));
app.post('/users', (req, res) => {
// 现在可以访问 req.body
const { name, email, age } = req.body;
res.status(201).json({
message: '用户创建成功',
user: { name, email, age }
});
});
// POST /users
// Content-Type: application/json
// Body: { "name": "John", "email": "[email protected]", "age": 25 }
重要安全提示:req.body 的内容来自用户输入,所有值都不可信,使用前应该验证。
在 Express 5 中,如果请求没有请求体、Content-Type 不匹配或解析出错,req.body 会是 undefined。在 Express 4 中,默认是空对象 {}。这个变化要求你在访问 req.body 属性时更加谨慎:
// 不安全的写法
app.post('/data', (req, res) => {
// 如果 body 为空或 name 不存在,会出错
const name = req.body.name.toUpperCase();
});
// Express 5 安全的写法
app.post('/data', (req, res) => {
// 检查 body 是否存在
if (!req.body) {
return res.status(400).json({ error: '请求体不能为空' });
}
const name = req.body.name;
if (!name || typeof name !== 'string') {
return res.status(400).json({ error: 'name 字段必填且必须是字符串' });
}
const upperName = name.toUpperCase();
res.json({ name: upperName });
});
// 或使用可选链和空值合并
app.post('/data', (req, res) => {
const name = req.body?.name;
if (!name || typeof name !== 'string') {
return res.status(400).json({ error: 'name 字段必填且必须是字符串' });
}
res.json({ name: name.toUpperCase() });
});
请求头 (req.headers)
HTTP 请求头包含了请求的元信息:
app.get('/info', (req, res) => {
// 访问单个请求头(推荐方式)
const contentType = req.get('Content-Type');
const auth = req.get('Authorization');
// 访问所有请求头
console.log(req.headers);
res.json({
userAgent: req.headers['user-agent'],
contentType,
authorization: auth
});
});
常用请求头:
| 请求头 | 说明 |
|---|---|
content-type | 请求体的媒体类型 |
authorization | 认证信息(如 Bearer Token) |
user-agent | 客户端信息(浏览器、操作系统等) |
accept | 客户端接受的响应类型 |
host | 目标主机名 |
请求方法 (req.method)
获取 HTTP 请求方法:
app.use((req, res, next) => {
console.log(`${req.method} ${req.url}`);
next();
});
// GET /users → "GET /users"
// POST /users → "POST /users"
获取客户端信息
app.get('/client-info', (req, res) => {
res.json({
ip: req.ip, // 客户端 IP
ips: req.ips, // 代理链中的 IP 数组
hostname: req.hostname, // 主机名
protocol: req.protocol, // http 或 https
secure: req.secure, // 是否 HTTPS(等价于 req.protocol === 'https')
xhr: req.xhr // 是否 AJAX 请求
});
});
如果应用在反向代理(如 Nginx)后面,需要设置 trust proxy 才能获取真实 IP:
app.set('trust proxy', true);
app.get('/ip', (req, res) => {
res.json({ ip: req.ip });
});
Cookie (req.cookies)
需要 cookie-parser 中间件:
npm install cookie-parser
const cookieParser = require('cookie-parser');
app.use(cookieParser());
app.get('/read-cookie', (req, res) => {
// req.cookies 是一个对象
const sessionId = req.cookies.sessionId;
res.json({ sessionId });
});
文件上传 (req.file / req.files)
需要 multer 中间件处理文件上传:
npm install multer
const multer = require('multer');
const upload = multer({ dest: 'uploads/' });
// 单文件上传
app.post('/upload', upload.single('avatar'), (req, res) => {
// req.file 包含文件信息
console.log(req.file);
/*
{
fieldname: 'avatar',
originalname: 'photo.jpg',
encoding: '7bit',
mimetype: 'image/jpeg',
size: 102400,
destination: 'uploads/',
filename: 'abc123',
path: 'uploads/abc123'
}
*/
res.json({
message: '上传成功',
file: req.file
});
});
// 多文件上传(相同字段名)
app.post('/photos', upload.array('photos', 10), (req, res) => {
// req.files 是数组
res.json({ count: req.files.length });
});
// 多字段文件上传
app.post('/profile', upload.fields([
{ name: 'avatar', maxCount: 1 },
{ name: 'documents', maxCount: 5 }
]), (req, res) => {
// req.files 是对象
// { avatar: [...], documents: [...] }
res.json({ files: req.files });
});
响应对象 (res)
响应对象用于向客户端发送响应。它扩展了 Node.js 的 ServerResponse 对象。
发送响应的方法
res.send()
发送各种类型的响应,Express 会自动设置 Content-Type:
// 发送字符串
app.get('/text', (req, res) => {
res.send('Hello World');
// Content-Type: text/html
});
// 发送对象/数组(自动转 JSON)
app.get('/json', (req, res) => {
res.send({ name: 'Express', version: '4.x' });
// Content-Type: application/json
});
// 发送 Buffer
app.get('/buffer', (req, res) => {
res.send(Buffer.from('Hello'));
// Content-Type: application/octet-stream
});
res.json()
明确发送 JSON 响应:
app.get('/api/user', (req, res) => {
res.json({
success: true,
data: {
id: 1,
name: 'John'
}
});
});
res.json() 和 res.send() 发送对象时的区别:res.json() 会强制设置 Content-Type 为 application/json,而 res.send() 是根据内容类型自动推断。
res.status()
设置 HTTP 状态码,通常与其他响应方法链式调用:
// 创建成功
app.post('/users', (req, res) => {
res.status(201).json({ id: 1, name: 'New User' });
});
// 无内容
app.delete('/users/:id', (req, res) => {
res.status(204).send();
});
// 客户端错误
app.get('/error', (req, res) => {
res.status(400).json({ error: '参数错误' });
});
// 服务端错误
app.get('/server-error', (req, res) => {
res.status(500).json({ error: '服务器内部错误' });
});
常用 HTTP 状态码:
| 状态码 | 说明 | 使用场景 |
|---|---|---|
| 200 | 成功 | 一般成功响应 |
| 201 | 已创建 | POST 创建资源成功 |
| 204 | 无内容 | DELETE 成功或 PUT 更新成功 |
| 400 | 错误请求 | 参数验证失败 |
| 401 | 未认证 | 需要登录 |
| 403 | 禁止访问 | 权限不足 |
| 404 | 未找到 | 资源不存在 |
| 409 | 冲突 | 资源已存在 |
| 500 | 服务器错误 | 程序错误 |
设置响应头
app.get('/headers', (req, res) => {
// 设置单个响应头
res.set('X-Custom-Header', 'value');
// 设置多个响应头
res.set({
'Cache-Control': 'no-cache',
'X-Request-Id': '12345'
});
res.json({ message: '响应头已设置' });
});
Cookie 操作
// 设置 Cookie
app.get('/set-cookie', (req, res) => {
res.cookie('name', 'express', {
maxAge: 900000, // 有效期(毫秒)
httpOnly: true, // 仅服务器可访问
secure: true, // 仅 HTTPS 传输
sameSite: 'strict', // CSRF 防护
domain: 'example.com',
path: '/'
});
res.send('Cookie 已设置');
});
// 清除 Cookie
app.get('/clear-cookie', (req, res) => {
res.clearCookie('name');
res.send('Cookie 已清除');
});
重定向
// 重定向到相对路径
app.get('/old', (req, res) => {
res.redirect('/new');
});
// 重定向到外部 URL
app.get('/external', (req, res) => {
res.redirect('https://expressjs.com');
});
// 指定状态码(默认 302)
app.get('/permanent', (req, res) => {
res.redirect(301, '/new-permanent'); // 永久重定向
});
// 返回上一页(Express 4.x 写法)
// app.get('/back', (req, res) => {
// res.redirect('back'); // 重定向到 Referer 或 /
// });
// Express 5.x 写法:'back' 魔法字符串已移除
app.get('/back', (req, res) => {
res.redirect(req.get('Referrer') || '/'); // 显式指定来源页
});
文件操作
// 下载文件
app.get('/download', (req, res) => {
res.download('/files/report.pdf');
});
// 下载时指定文件名
app.get('/download-custom', (req, res) => {
res.download('/files/report.pdf', '月度报告.pdf', (err) => {
if (err) {
console.error('下载出错:', err);
}
});
});
// 发送文件(直接显示)
app.get('/file', (req, res) => {
// root 选项指定根目录
res.sendFile('index.html', { root: __dirname + '/public' });
});
// 发送文件作为附件下载
app.get('/attachment', (req, res) => {
res.attachment('/files/data.csv');
res.send('col1,col2\nval1,val2');
});
渲染模板
Express 支持多种模板引擎(如 EJS、Pug、Handlebars):
npm install ejs
// 设置模板引擎
app.set('view engine', 'ejs');
app.set('views', './views');
// 渲染模板
app.get('/profile', (req, res) => {
res.render('profile', {
title: '用户资料',
user: {
name: 'John',
email: '[email protected]'
}
});
});
实战模式
统一响应格式
在团队协作中,统一响应格式能提高代码可维护性:
// utils/response.js
class Response {
static success(res, data, message = '操作成功', code = 0) {
return res.json({
code,
message,
data
});
}
static error(res, message = '操作失败', code = 1, status = 400) {
return res.status(status).json({
code,
message,
data: null
});
}
static paginated(res, data, pagination) {
return res.json({
code: 0,
message: '查询成功',
data,
pagination
});
}
}
module.exports = Response;
使用示例:
const Response = require('../utils/response');
// 成功响应
app.get('/users', async (req, res) => {
const users = await User.find();
Response.success(res, users, '获取用户列表成功');
});
// 错误响应
app.post('/users', async (req, res) => {
if (!req.body.name) {
return Response.error(res, '姓名不能为空', 400, 400);
}
const user = await User.create(req.body);
Response.success(res, user, '创建成功', 201);
});
分页响应
app.get('/posts', async (req, res) => {
const { page = 1, limit = 10 } = req.query;
const skip = (parseInt(page) - 1) * parseInt(limit);
// 并行查询数据和总数
const [posts, total] = await Promise.all([
Post.find().skip(skip).limit(parseInt(limit)),
Post.countDocuments()
]);
res.json({
code: 0,
data: posts,
pagination: {
page: parseInt(page),
limit: parseInt(limit),
total,
pages: Math.ceil(total / parseInt(limit))
}
});
});
条件响应
根据请求类型返回不同格式:
app.get('/data', (req, res) => {
const data = { name: 'Express', version: '4.x' };
// 检查 Accept 头
const accept = req.get('Accept');
if (accept?.includes('application/json')) {
res.json(data);
} else if (accept?.includes('text/html')) {
res.render('data', { data });
} else {
res.send(`名称: ${data.name}, 版本: ${data.version}`);
}
});
流式响应
对于大文件或实时数据,使用流式响应:
const fs = require('fs');
app.get('/large-file', (req, res) => {
const stream = fs.createReadStream('./large-file.txt');
res.set('Content-Type', 'text/plain');
stream.pipe(res);
stream.on('error', (err) => {
console.error('流错误:', err);
res.status(500).end('服务器错误');
});
});
// Server-Sent Events
app.get('/events', (req, res) => {
res.set({
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive'
});
// 每秒发送一次时间
const interval = setInterval(() => {
res.write(`data: ${new Date().toISOString()}\n\n`);
}, 1000);
// 客户端断开连接时清理
req.on('close', () => {
clearInterval(interval);
});
});
请求响应完整示例
下面是一个完整的用户 API 示例,展示了请求处理和响应发送的各种场景:
const express = require('express');
const app = express();
// 中间件
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// 模拟数据库
let users = [
{ id: 1, name: 'Alice', email: '[email protected]' },
{ id: 2, name: 'Bob', email: '[email protected]' }
];
// GET /users - 获取用户列表(支持分页和搜索)
app.get('/users', (req, res) => {
const { page = 1, limit = 10, search = '' } = req.query;
// 过滤
let filtered = users;
if (search) {
filtered = users.filter(u =>
u.name.toLowerCase().includes(search.toLowerCase())
);
}
// 分页
const start = (parseInt(page) - 1) * parseInt(limit);
const paginated = filtered.slice(start, start + parseInt(limit));
res.json({
code: 0,
data: paginated,
pagination: {
page: parseInt(page),
limit: parseInt(limit),
total: filtered.length
}
});
});
// GET /users/:id - 获取单个用户
app.get('/users/:id', (req, res) => {
const id = parseInt(req.params.id);
const user = users.find(u => u.id === id);
if (!user) {
return res.status(404).json({
code: 404,
message: '用户不存在'
});
}
res.json({ code: 0, data: user });
});
// POST /users - 创建用户
app.post('/users', (req, res) => {
const { name, email } = req.body;
// 验证
if (!name || !email) {
return res.status(400).json({
code: 400,
message: '姓名和邮箱不能为空'
});
}
// 创建
const newUser = {
id: users.length + 1,
name,
email
};
users.push(newUser);
res.status(201).json({
code: 0,
message: '创建成功',
data: newUser
});
});
// PUT /users/:id - 更新用户
app.put('/users/: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({
code: 404,
message: '用户不存在'
});
}
users[index] = { ...users[index], ...req.body, id };
res.json({
code: 0,
message: '更新成功',
data: users[index]
});
});
// DELETE /users/:id - 删除用户
app.delete('/users/: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({
code: 404,
message: '用户不存在'
});
}
users.splice(index, 1);
res.status(204).send();
});
app.listen(3000, () => {
console.log('服务器运行在 http://localhost:3000');
});
小结
本章详细介绍了 Express 的请求和响应对象:
请求对象 (req):
req.params- 路由参数req.query- 查询字符串参数req.body- 请求体数据req.headers- 请求头req.method- HTTP 方法req.ip- 客户端 IPreq.cookies- Cookie
响应对象 (res):
res.send()- 发送响应res.json()- 发送 JSONres.status()- 设置状态码res.redirect()- 重定向res.download()- 文件下载res.sendFile()- 发送文件res.render()- 渲染模板res.cookie()/res.clearCookie()- Cookie 操作
理解这些属性和方法,是构建 Express 应用的基础。
练习
- 创建一个 API,返回请求的所有信息(方法、路径、参数、请求头等)
- 实现一个带分页和搜索功能的用户列表 API
- 创建一个文件上传接口,支持图片上传并返回访问 URL
- 实现统一响应格式的工具类,并在路由中使用