URL 编码:%20、加号与双重编码的坑
开发与 Web发布于 2026-10-31
URL 里能直接出现的字符是有限的。空格、中文、&、#、+ 这些一旦原样出现,就会被解析成别的东西 —— 轻则参数错位,重则整条链接失效。
编码规则本身很简单:把字符按 UTF-8 转成字节,每个字节写成 % 加两位十六进制。「中」是 E4 B8 AD,编码后就是 %E4%B8%AD。
坑都在细节里。
哪些字符必须编码
RFC 3986 把字符分三类:
| 类别 | 字符 | 处理 |
|---|---|---|
| 非保留字符 | A-Z a-z 0-9 - . _ ~ |
永远不用编码 |
| 保留字符 | : / ? # [ ] @ ! $ & ' ( ) * + , ; = |
取决于位置,作为分隔符时不能编码 |
| 其它 | 空格、中文、" < > \ ^ { ` |
}` 等 |
「取决于位置」是第一个坑。/ 在路径里是分隔符,编码成 %2F 就等于告诉服务器「这是一个叫 a/b 的文件名」——有的服务器会因此返回 404,有的会直接 400。
空格:%20 还是 +
两种都合法,但含义不同:
%20是「这个位置有一个空格」+是application/x-www-form-urlencoded规定的「空格简写」,只在查询串或表单体里有这个含义
在路径 /a+b/c 里,+ 就是加号本身,不是空格。所以:
| 位置 | 空格写成 | 备注 |
|---|---|---|
| 路径 | %20 |
+ 会被当成字面加号 |
| 查询串 | %20 或 + |
两者等价 |
| 表单 POST body | + |
标准写法,但 %20 也被接受 |
事故场景:把路径里的空格替换成 +,图片加载 404,而 URL 看起来「挺正常」。
encodeURI 和 encodeURIComponent
JavaScript 里两个函数的行为完全不同,这是最常被记混的一对:
encodeURI('https://a.com/搜索?q=hello world')
// https://a.com/%E6%90%9C%E7%B4%A2?q=hello%20world
// 冒号、斜杠、问号、等号都保留 —— 它假设你在编码一整个 URL
encodeURIComponent('https://a.com/搜索?q=hello world')
// https%3A%2F%2Fa.com%2F%E6%90%9C%E7%B4%A2%3Fq%3Dhello%20world
// 全编码,包括分隔符 —— 它假设你在编码一个「值」
选哪个只看一件事:你编的是整条 URL,还是 URL 里的某一段。
// 错:拼接时用 encodeURI,中文参数里的 & 没被编码
`https://api.com/search?q=${encodeURI(keyword)}`
// 对:参数值用 encodeURIComponent
`https://api.com/search?q=${encodeURIComponent(keyword)}`
参数里带 & 时差别立刻显形:keyword = "a&b",用 encodeURI 得到 ?q=a&b,服务端会读到两个参数 q=a 和 b=。
双重编码:看起来一样,服务器不认
「先编码一次不够,再编一次」是新手常见的过度操作。% 本身会被编码成 %25,于是 中 变成 %25E4%25B8%25AD。
症状很有辨识度:浏览器地址栏里出现 %25。看到它基本就是双编了。
另一个来源是框架自动编码。你在代码里手编一次,HTTP 客户端拼 URL 时又编一次 —— 因为客户端认为「传进来的字符串已经是原始值」。判断方法:打印最终发出的完整 URL,看 %25 在不在。
中文为什么是三个百分号
UTF-8 里常用汉字占 3 个字节,所以编码后是 3 组 %XX。生僻字(扩展 B 区以上)占 4 个字节,就是 4 组。
这也解释了一个老问题:同一段中文在 GBK 页面里编码出来是 2 组 %XX,在 UTF-8 页面里是 3 组。编码结果依赖字符集,跨系统对不上时先确认两边都是 UTF-8。
签名对不上,八成是编码不一致
对接支付、开放平台时最常见的报错是「签名错误」,而参数肉眼完全相同。
原因通常是签名串的拼接规则和发送时的编码规则不一致:
| 环节 | 常见分歧 |
|---|---|
| 参与签名的参数 | 空值参加不参加、sign 字段排除没有 |
| 排序 | 按 key 字典序,还是按参数原始顺序 |
| 编码 | 签名前编一次,发送时又编一次 |
| 空格 | 一边写 %20,一边写 + |
| 大小写 | %2f 和 %2F(多数平台要求大写) |
排查办法:把「参与签名的原始串」和「实际发出的 URL」都打印出来,逐字符比对。这一步能省掉几小时的猜。
什么时候该用 Base64 而不是 URL 编码
如果把一段数据塞进 URL 又不想被 URL 规则影响(比如带 &、=、中文的 token),Base64URL 是更好的选择:它用 - 和 _ 代替 + 和 /,去掉 = 填充,产出的字符串在 URL 里完全安全,长度也可控。
两者的区别本质是:URL 编码解决「字符能不能出现在 URL 里」,Base64 解决「二进制怎么变成纯文本」。JWT 用的就是 Base64URL —— 详见Base64 不是加密和JWT 那篇。
手边工具
临时要编一段中文、看某个字符的编码、或者反过来解一段乱糟糟的 %E4%B8%AD,用 URL 编解码 最直接:支持整段编码与逐字节查看,中文不会被误判。
日常写代码时记住三条就够了:
- 编「值」用
encodeURIComponent,编「整条 URL」用encodeURI。 - 路径里的空格写
%20,别写+。 - 看到
%25就是双编了。
URL 编码属于「规则五分钟、事故五小时」那类知识。真正记住的不是百分号怎么写,而是「这个字符在这个位置是不是分隔符」。