URL 编码(正式名称是"百分号编码")是开发者每天都会遇到却很少深究的东西,直到某天出了问题:文件名里的空格变成了 %20,中文字符变成了一堵百分号墙,查询参数里的 + 莫名其妙在服务端变成了空格。本文讲清楚百分号编码到底怎么工作、为什么存在,以及如何在不引入 bug 的前提下处理那些棘手场景。
百分号编码的工作原理
URL 只能包含有限的 ASCII 字符集:字母、数字以及少量特殊字符(-、_、.、~)。其他一切——空格、标点、非拉丁文字、emoji——在安全地出现在 URL 中之前都必须先编码。
机制很简单:
- 取出字符。
- 转换为 UTF-8 字节表示。
- 把每个字节替换为
%加两位大写十六进制值。
对于单字节 ASCII 字符,这会生成一个百分号编码三元组。对于多字节字符(如中文、日文或 emoji),你会得到多个三元组——每个字节一个。
为什么空格变成 %20(或 +)
空格字符(ASCII 0x20)是最常被编码的字符,它有一种容易让人混淆的双重表示:
%20—— 空格的标准百分号编码,在 URL 的任何位置(路径、查询、片段)都有效。+—— 一种遗留的空格编码,只在application/x-www-form-urlencoded数据中有效(即表单提交和查询字符串)。
# 在 URL 路径中,空格必须是 %20:
https://example.com/files/my%20document.pdf
# 在查询字符串中,两种都行(但在某些解析器中含义不同):
https://example.com/search?q=hello%20world # %20 = 空格
https://example.com/search?q=hello+world # + = 空格(表单编码)
混乱的根源在于:+ 在 URL 路径里是字面量加号,但在查询字符串里表示空格。如果你在构建一个接收查询参数的 API,两种都要处理。如果你在生成 URL,挑一种约定并保持一致。
编码中文字符(及其他多字节 UTF-8)
这里事情变得有趣。字符 中(U+4E2D)的 UTF-8 编码是三个字节:E4 B8 AD。所以它的百分号编码形式是:
中 → %E4%B8%AD
完整示例:
原文: 你好世界
UTF-8: E4 BD A0 E5 A5 BD E4 B8 96 E7 95 8C
编码后: %E4%BD%A0%E5%A5%BD%E4%B8%96%E7%95%8C
每个中文字符百分号编码后变成 9 个字符(3 字节 × 每字节 3 字符)。这就是为什么包含中文的 URL 会迅速变长。
JavaScript 中的做法:
// encodeURIComponent 处理 UTF-8 百分号编码
encodeURIComponent('中') // "%E4%B8%AD"
encodeURIComponent('你好') // "%E4%BD%A0%E5%A5%BD"
encodeURIComponent('hello 世界') // "hello%20%E4%B8%96%E7%95%8C"
// decodeURIComponent 反向解码
decodeURIComponent('%E4%B8%AD') // "中"
其他常见特殊字符
| 字符 | 编码 | 场景 |
|-----------|---------|---------|
| 空格 | %20 或 + | 路径用 %20,表单用 + |
| & | %26 | 查询值中必须编码(分隔参数) |
| = | %3D | 查询值中必须编码(分隔键值) |
| # | %23 | 必须编码(启动片段标识符) |
| ? | %3F | 路径中必须编码(启动查询字符串) |
| / | %2F | 路径段中必须编码(分隔路径段) |
| @ | %40 | 路径中编码以避免 userinfo 混淆 |
| € | %E2%82%AC | 3 字节 UTF-8 |
| 🎉 | %F0%9F%8E%89 | 4 字节 UTF-8(emoji) |
常见错误及规避方法
错误 1:双重编码
最常见的 bug。你编码了一个值,然后框架又编码了一次:
// 错误:双重编码
const param = encodeURIComponent('hello world'); // "hello%20world"
const url = `https://api.example.com/search?q=${encodeURIComponent(param)}`;
// 结果:q=hello%2520world(% 被编码成了 %25)
// 服务端收到:"hello%20world"(字面字符串,不是空格)
修复方法:在组装 URL 的那一层只编码一次。大多数 HTTP 库(axios、fetch 配 URLSearchParams)会替你处理编码:
// 正确:让 URLSearchParams 处理编码
const params = new URLSearchParams({ q: 'hello world', name: '张三' });
const url = `https://api.example.com/search?${params.toString()}`;
// 结果:q=hello+world&name=%E5%BC%A0%E4%B8%89
错误 2:混用 + 和 %20
如果后端把 + 解码为空格,而前端发的是 %20(或反过来),就会出现不匹配:
// 服务端期望表单编码(+ 表示空格)
// 但客户端发送:
fetch('/api?q=hello%20world') // 有的服务端能正确解码 %20,有的不行
// 最稳妥:用 URLSearchParams 产出一致的编码
const params = new URLSearchParams();
params.set('q', 'hello world');
// 始终产出:q=hello+world
错误 3:编码整个 URL
不要盲目编码完整 URL——否则会把本该作为结构的 ://、/、?、& 也编码掉:
// 错误:破坏了 URL 结构
encodeURIComponent('https://example.com/path?q=hello world')
// "https%3A%2F%2Fexample.com%2Fpath%3Fq%3Dhello%20world"
// 正确:只编码动态部分
const baseUrl = 'https://example.com/path';
const query = encodeURIComponent('hello world');
const url = `${baseUrl}?q=${query}`;
错误 4:假设只有 ASCII
如果你的应用要处理国际化输入(中文用户名、阿拉伯文文件名、日文搜索词),必须使用 UTF-8 百分号编码。JavaScript 中老的 escape() 函数用的是非标准编码——永远不要用它:
// 永远不要用 escape() —— 已废弃且非标准
escape('中') // "%u4E2D" —— 这不是合法的百分号编码
// 始终用 encodeURIComponent()
encodeURIComponent('中') // "%E4%B8%AD" —— 正确的 UTF-8 百分号编码
实战示例
构建包含混合内容的 URL
function buildSearchUrl(query, category, page) {
const params = new URLSearchParams({
q: query, // "北京 weather" → q=%E5%8C%97%E4%BA%AC+weather
category: category, // "food & drink" → category=food+%26+drink
page: page
});
return `https://example.com/search?${params.toString()}`;
}
buildSearchUrl('北京 weather', 'food & drink', 1);
// "https://example.com/search?q=%E5%8C%97%E4%BA%AC+weather&category=food+%26+drink&page=1"
安全地解码 URL 参数
function getUrlParams(url) {
const { searchParams } = new URL(url);
const params = {};
for (const [key, value] of searchParams) {
params[key] = value; // URL API 已经解码好了
}
return params;
}
getUrlParams('https://example.com/search?q=%E4%BD%A0%E5%A5%BD&tag=a%2Bb');
// { q: "你好", tag: "a+b" }
// 注意:%2B 解码为字面量 +,而 + 解码为空格
处理带特殊字符的文件名
// 下载一个中文名文件
const filename = '年度报告 2026.pdf';
const encodedFilename = encodeURIComponent(filename);
// "%E5%B9%B4%E5%BA%A6%E6%8A%A5%E5%91%8A%202026.pdf"
const downloadUrl = `/api/files/${encodedFilename}`;
// 服务端必须解码路径段才能找到实际文件
速查表:哪里需要编码什么
| URL 组成部分 | 需要编码的字符 |
|---------------|-------------------------------|
| 路径段 | /、?、#、[、]、@、!、$、&、'、(、)、*、+、,、;、=、空格、非 ASCII |
| 查询参数值 | &、=、+、#、空格、非 ASCII |
| 片段 | 空格、非 ASCII(浏览器在这里比较宽松)|
最简单的规则:如果你要把一个动态值插入 URL,用 encodeURIComponent()(适用于路径段和查询值)或 URLSearchParams(适用于查询字符串)。剩下的细节交给平台处理。
需要快速编解码 URL 组件?DevToolkit Pro 的 URL 编解码器 工具完全在浏览器中处理百分号编码——粘贴字符串,立即得到编码结果,数据不会发送到任何服务器。它支持多字节 UTF-8 字符、+ 与 %20 的差异处理,以及完整 URL 编码,方便你在上线前验证输出。