URL 编码:那些 %E4%B8%AD 后面藏着的坑
"URL 编码就是把汉字变 % 加十六进制"——对,但每次写代码还是会卡在 encodeURI 还是 encodeURIComponent、用 + 还是 %20、Java 怎么传字符集这些细节上。这篇按真实踩坑场景列。
30 秒搞清楚原理
URL 的"地理位置"层(路径、查询字符串)只允许有限的 ASCII 字符。其他所有东西——中文、空格、emoji、保留字符——都得编码成 % + 两位十六进制。
# UTF-8 字节序列 → 每字节单独 % 编码
"中" → UTF-8 → E4 B8 AD → "%E4%B8%AD"
"你好 world" → "%E4%BD%A0%E5%A5%BD%20world"
所以 URL 里看到 %E4%B8%AD%E6%96%87 你心算就能知道这是 6 字节的 UTF-8——大概率是两个汉字。
encodeURI vs encodeURIComponent(最高频踩坑)
JS 里有两个编码函数,**99% 的场景应该用第二个**:
// encodeURI:编码整个 URL,保留 URL 结构字符
encodeURI("https://example.com/搜索?q=你好&p=1");
// "https://example.com/%E6%90%9C%E7%B4%A2?q=%E4%BD%A0%E5%A5%BD&p=1"
// 注意 :/?&= 都没动
// encodeURIComponent:编码一个值(query param 或路径片段)
encodeURIComponent("a&b=c");
// "a%26b%3Dc"
// 注意 &= 也编了,因为它们是"值"而不是"结构"
判断方法:你在编码整个 URL 还是 URL 中的一段值?后者用 Component。
// ✅ 正确用法
const url = "https://api.com/search?q=" + encodeURIComponent(userInput);
// ❌ 错误:会把 & 留着,userInput 含 & 时 URL 被截断
const url = "https://api.com/search?q=" + encodeURI(userInput);
真实事故:用户搜索 "javascript & jquery",代码用了 encodeURI(),结果 & 没被编码。后端把 URL 解析为两个参数 q=javascript 和 jquery=,搜索结果完全错了。改成 encodeURIComponent 立刻好。
+ 还是 %20?历史包袱问题
同样是"空格",两种编码都看见过:
| 编码 | 来源 | 用在哪 |
|---|---|---|
%20 | RFC 3986 (URL 标准) | URL path、JSON 里、所有正确场景 |
+ | HTML 表单 (application/x-www-form-urlencoded) | POST 表单 body、部分 query 实现 |
JS 的 encodeURIComponent 永远用 %20。但解码时两种都得识别:
// URLSearchParams 兼容两种
new URLSearchParams("q=hello+world").get("q"); // "hello world"
new URLSearchParams("q=hello%20world").get("q"); // "hello world"
// 但 decodeURIComponent 不会把 + 当空格
decodeURIComponent("hello+world"); // "hello+world" ← 注意
哪些字符必须编码(RFC 3986)
按规范分三类:
- 非保留字符(不编):
A-Z a-z 0-9 - _ . ~ - 保留字符(看场景):
: / ? # [ ] @ ! $ & ' ( ) * + , ; =
这些字符在 URL 不同位置有特殊含义,在"结构"位置不编,在"值"位置必须编 - 其他所有字符(必编):空格、中文、emoji、控制字符等
各语言的编解码 API
JavaScript
// 编
encodeURIComponent("你好 world");
// "%E4%BD%A0%E5%A5%BD%20world"
// 解
decodeURIComponent("%E4%BD%A0%E5%A5%BD%20world");
// "你好 world"
// 处理整个 URL 推荐用 URL/URLSearchParams 而不是手拼
const u = new URL("https://api.com/search");
u.searchParams.set("q", "你好 world");
console.log(u.toString());
// https://api.com/search?q=%E4%BD%A0%E5%A5%BD+world
Python
from urllib.parse import quote, unquote, quote_plus, urlencode
quote("你好 world") # '%E4%BD%A0%E5%A5%BD%20world' ← 空格变 %20
quote_plus("你好 world") # '%E4%BD%A0%E5%A5%BD+world' ← 空格变 +
unquote('%E4%BD%A0') # '你好'
# 构造完整 query string
urlencode({"q": "你好 world", "p": 1})
# 'q=%E4%BD%A0%E5%A5%BD+world&p=1'
Java
// Java URLEncoder 是 form 编码(空格变 +)
URLEncoder.encode("你好 world", "UTF-8");
// "%E4%BD%A0%E5%A5%BD+world"
// 真正的 URL 编码(RFC 3986)需要后处理
URLEncoder.encode("你好 world", "UTF-8")
.replace("+", "%20")
.replace("*", "%2A")
.replace("%7E", "~");
Go
import "net/url"
url.QueryEscape("你好 world") // "%E4%BD%A0%E5%A5%BD+world" (空格变 +)
url.PathEscape("你好/path") // "%E4%BD%A0%E5%A5%BD/path" (保留 /)
跨语言对接时不要假设对方用什么编码——加一行测试:让对方传一个含空格、中文、& 的字符串,看你能不能正确还原。
URL 编码 vs Base64 vs HTML 实体
三种都叫"编码",用途完全不同:
| 编码 | 解决什么 | 体积 | 典型场景 |
|---|---|---|---|
| URL 编码 | 让数据通过 URL 安全传输 | 中文 3x | query string、URL path |
| Base64 | 让二进制通过文本协议 | 1.33x | Data URL、JWT、邮件附件 |
| HTML 实体 | 让 HTML 里的特殊字符不被解析为标签 | 3-7x | HTML 文档中渲染 < > & |
常见疑问
为什么有时候 URL 里中文不用编码?
浏览器地址栏显示的是解码后的版本。复制粘贴或者通过 JS 拿到的是原始编码版本。试试在 Chrome 地址栏输入 https://example.com/中文,然后右键复制 URL——你会看到 %E4%B8%AD%E6%96%87。
对已经编码过的字符串再编码会怎样?
encodeURIComponent("%E4%BD%A0");
// "%25E4%25BD%25A0" ← % 本身被编码成 %25
// 解码两次才能拿回原字符串
decodeURIComponent(decodeURIComponent("%25E4%25BD%25A0"));
// "你"
所以:不要重复编码。某些后端会自动解码一次,前端再编码一次刚好,但如果链路里有多个组件都"贴心地"编码,就会出现 %2525 这种现象。
哈希(#)后面的内容需要编码吗?
需要。SPA 路由 /#/页面?q=值 里的中文必须编码,否则 history.pushState / location.hash 行为不一致。
需要快速编/解码?URL 编解码工具支持批量、双向转换,自动识别 UTF-8。