一、核心前提
express.urlencoded 仅解析 Content-Type: application/x-www-form-urlencoded 格式的请求体,因此用 fetch 发送请求时,必须满足两个条件:
- 请求体需构造为「URL 编码的键值对字符串」(如
username=zhangsan&age=20),而非 JSON 对象。 - 手动设置请求头
Content-Type: application/x-www-form-urlencoded(fetch不会自动为该格式设置请求头)。
二、步骤1:后端 Express 配置
首先搭建支持 application/x-www-form-urlencoded 解析的后端服务,核心是注册 express.urlencoded 中间件。
完整后端代码
javascript
// server.js
const express = require('express');
const app = express();
// 1. 注册 urlencoded 中间件(关键:extended: true 支持嵌套数据)
app.use(express.urlencoded({ extended: true }));
// 2. 解决跨域(前端 fetch 大概率存在跨域,需配置)
app.use((req, res, next) => {
res.setHeader('Access-Control-Allow-Origin', '*'); // 允许所有来源(开发环境)
res.setHeader('Access-Control-Allow-Methods', 'POST, GET, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
next();
});
// 3. 定义接收表单数据的接口
app.post('/api/user', (req, res) => {
console.log('解析后的请求体:', req.body);
// req.body 已挂载解析后的数据
const { username, age, hobby } = req.body;
// 返回响应
res.json({
code: 200,
message: '数据接收成功',
data: { username, age, hobby }
});
});
// 启动服务
const port = 3000;
app.listen(port, () => {
console.log(`后端服务运行在 http://localhost:${port}`);
});
运行后端
- 初始化项目:
npm init -y - 安装 Express:
npm install express - 启动服务:
node server.js
三、步骤2:前端 fetch 发送请求
前端需手动构造 URL 编码字符串,并设置正确的请求头,以下提供 3 种常用写法。
写法1:手动拼接键值对(简单场景)
适合数据量少、无嵌套的场景,直接拼接字符串作为请求体。
html
<!-- index.html -->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>fetch + express.urlencoded</title>
</head>
<body>
<button onclick="submitData()">提交数据</button>
<script>
// 提交数据函数
async function submitData() {
try {
// 1. 准备要发送的数据
const username = '张三';
const age = 22;
const hobby = '篮球';
// 2. 手动拼接为 URL 编码字符串(核心:key=value&key=value 格式)
const requestBody = `username=${encodeURIComponent(username)}&age=${age}&hobby=${encodeURIComponent(hobby)}`;
// 注意:使用 encodeURIComponent 编码特殊字符(如中文、空格、@ 等),避免解析异常
// 3. 发送 fetch 请求
const response = await fetch('http://localhost:3000/api/user', {
method: 'POST', // 必须指定 POST 方法(GET 无请求体)
headers: {
// 关键:设置 Content-Type 为 application/x-www-form-urlencoded
'Content-Type': 'application/x-www-form-urlencoded'
},
body: requestBody // 传入拼接好的 URL 编码字符串
});
// 4. 解析响应
const result = await response.json();
console.log('后端响应:', result);
alert(`成功:${JSON.stringify(result.data)}`);
} catch (error) {
console.error('请求失败:', error);
alert('请求失败');
}
}
</script>
</body>
</html>
写法2:使用 URLSearchParams 构造(推荐,简洁高效)
URLSearchParams 是浏览器内置 API,可自动将对象转为 URL 编码字符串,无需手动拼接,还能自动处理特殊字符编码。
javascript
async function submitData() {
try {
// 1. 准备原始数据对象
const formData = {
username: '李四',
age: 25,
hobby: '音乐',
// 支持嵌套对象(后端需开启 extended: true 才能解析)
address: {
city: '北京',
area: '朝阳'
}
};
// 2. 用 URLSearchParams 构造请求体
const searchParams = new URLSearchParams();
// 普通键值对
searchParams.append('username', formData.username);
searchParams.append('age', formData.age);
searchParams.append('hobby', formData.hobby);
// 嵌套对象(需按 user[city] 格式 append)
searchParams.append('address[city]', formData.address.city);
searchParams.append('address[area]', formData.address.area);
// 3. 发送请求(searchParams 可直接作为 body 参数)
const response = await fetch('http://localhost:3000/api/user', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: searchParams // 自动转为 URL 编码字符串
});
const result = await response.json();
console.log('后端响应:', result);
alert(`成功:${JSON.stringify(result.data)}`);
} catch (error) {
console.error('请求失败:', error);
}
}
简化写法(适用于单层对象)
若数据无嵌套,可直接传入对象(部分浏览器支持,兼容性略差,推荐用 append 写法):
javascript
const formData = { username: '王五', age: 30 };
const searchParams = new URLSearchParams(formData); // 自动转为 username=王五&age=30
写法3:处理数组数据(后端 extended: true 生效)
express.urlencoded({ extended: true }) 支持解析数组,前端需按 hobby[0]、hobby[1] 格式构造。
javascript
async function submitData() {
try {
const formData = {
username: '赵六',
age: 28,
hobby: ['游戏', '读书', '运动'] // 数组数据
};
const searchParams = new URLSearchParams();
searchParams.append('username', formData.username);
searchParams.append('age', formData.age);
// 数组处理:循环 append
formData.hobby.forEach((item, index) => {
searchParams.append(`hobby[${index}]`, item);
});
const response = await fetch('http://localhost:3000/api/user', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: searchParams
});
const result = await response.json();
console.log('后端响应:', result);
// 后端 req.body.hobby 会是 ['游戏', '读书', '运动']
} catch (error) {
console.error('请求失败:', error);
}
}
四、完整前后端联动效果
- 前端点击按钮发送请求,请求体格式为
username=张三&age=22&hobby=篮球。 - 后端
express.urlencoded解析后,req.body为{ username: '张三', age: '22', hobby: '篮球' }。 - 后端返回 JSON 响应,前端解析后弹出提示并在控制台打印结果。
五、常见坑点与解决方案
坑点1:req.body 为 undefined
- 原因:
- 前端未设置
Content-Type: application/x-www-form-urlencoded。 - 后端未注册
express.urlencoded中间件。 - 后端中间件注册在路由之后(执行顺序问题)。
- 前端未设置
- 解决方案:
- 前端 fetch 中明确设置
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }。 - 后端先注册
app.use(express.urlencoded({ extended: true })),再定义路由。
- 前端 fetch 中明确设置
坑点2:中文/特殊字符乱码
- 原因:未对特殊字符进行 URL 编码,导致解析异常。
- 解决方案:
- 手动拼接时使用
encodeURIComponent()编码每个值。 - 优先使用
URLSearchParams,自动处理编码问题。
- 手动拼接时使用
坑点3:嵌套对象/数组解析失败
- 原因:后端
extended配置为false(默认旧版 Express),querystring库不支持嵌套解析。 - 解决方案:后端必须设置
app.use(express.urlencoded({ extended: true }))。
坑点4:fetch 发送 GET 请求带请求体
- 原因:GET 请求默认没有请求体,即使传入
body参数也会被忽略。 - 解决方案:
- 表单数据用 POST 方法发送(推荐)。
- 若需用 GET,将数据拼接到 URL 后(如
http://localhost:3000/api/user?username=张三&age=22)。
六、与 fetch 发送 JSON 格式的区别
为了避免混淆,这里对比两者的差异:
| 对比项 | 发送 application/x-www-form-urlencoded(本文) | 发送 application/json |
|---|---|---|
| 后端中间件 | express.urlencoded({ extended: true }) |
express.json() |
| 前端 Content-Type | application/x-www-form-urlencoded |
application/json |
| 前端请求体格式 | URL 编码字符串(如 a=1&b=2) |
JSON 字符串(如 JSON.stringify({a:1,b:2})) |
| 适用场景 | 兼容传统表单、简单数据提交 | 现代接口、复杂嵌套数据提交 |
总结
- 核心要求:前端 fetch 需设置
Content-Type: application/x-www-form-urlencoded,并将请求体转为 URL 编码字符串。 - 推荐用法:使用
URLSearchParams构造请求体,简洁且能自动处理特殊字符编码,后端需配置express.urlencoded({ extended: true })。 - 避坑要点:中间件注册在路由之前、使用 POST 方法、嵌套/数组数据需开启
extended: true、特殊字符需编码。 - 跨域处理:后端需配置 CORS 响应头,否则前端 fetch 会出现跨域错误。