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 编解码 最直接:支持整段编码与逐字节查看,中文不会被误判。

日常写代码时记住三条就够了:

  1. 编「值」用 encodeURIComponent,编「整条 URL」用 encodeURI。
  2. 路径里的空格写 %20,别写 +。
  3. 看到 %25 就是双编了。

URL 编码属于「规则五分钟、事故五小时」那类知识。真正记住的不是百分号怎么写,而是「这个字符在这个位置是不是分隔符」。