URL 参数为什么被序列化了两次?一次 IOS 与浏览器标准差异的排查
常见技术问题 刘宇帅 2天前 阅读量: 33
在我们 iOS 客户端的开发中,有一个长期困扰的问题:部分链接打开后,URL 里的参数被序列化了两次。
比如一个原始链接是这样的:
/path?fff[]=jjj ll
结果在 iOS 里打开后,变成了:
/path?fff%255B%255D=jjj%2520ll
注意看:
%5B
又变成了%255B
%20
又变成了%2520
也就是说,参数被二次序列化了。这个问题追了很久,直到最近才找到真正的原因。
1. 背景差异:iOS vs 浏览器
-
在 iOS 标准库 中,
[]
被认为是 需要编码的特殊字符。 所以fff[]
会被序列化成fff%5B%5D
。 - 而在 浏览器标准 (WHATWG URL) 里,查询串部分允许保留
[]
不编码,因为很多 Web 框架(例如 PHP)习惯用ids[]
这种形式传数组。 所以<a>
标签或document.createElement("a")
得到的结果里,[]
会原样保留。
这就造成了一个差异:
- 浏览器里:
fff[]=jjj ll
→fff[]=jjj%20ll
(空格被转义,[]
保留) - iOS 里:
fff[]=jjj ll
→fff%5B%5D=jjj%20ll
(空格和[]
都被转义)
2. 为什么会出现二次序列化?
当一个 URL 在不同环境下被“来回处理”时:
- 浏览器环境里,
[]
没有被编码 - 传给 iOS 处理时,iOS 认为
[]
必须编码 →%5B%5D
- 但如果开发者又手动调用了一次编码方法,就会对
%5B%5D
再做一次序列化 →%255B%255D
这样,参数就被双重编码了。
3. 复现代码对比
在浏览器里:
var a = document.createElement("a");
a.href = "/path?fff[]=jjj ll";
console.log(a.search);
// 输出: ?fff[]=jjj%20ll
在 iOS(Swift)里:
let url = URL(string: "/path?fff[]=jjj ll", relativeTo: baseURL)!
print(url.query ?? "")
// 输出: fff%5B%5D=jjj%20ll
可以清楚看到差异。
4. 解决方案
最小改动方案
在前端解析函数里,强制把 []
也转义,保证跟 iOS 保持一致:
query: a.search
.replace(/ /g, "%20")
.replace(/\[/g, "%5B")
.replace(/\]/g, "%5D")
这样就不会出现“浏览器保留 []
,iOS 又去二次编码”的情况。
推荐方案
使用标准化的 URL
+ URLSearchParams
,而不是自己手写 split:
const u = new URL("/path?fff[]=jjj ll", location.origin);
console.log(u.search); // "?fff%5B%5D=jjj+ll"
console.log(u.searchParams.get("fff[]")); // "jjj ll"
这能确保在所有环境下,参数都被一致地编码。
5. 总结
这个问题折腾了我们很久,直到深入比对 iOS 与浏览器的标准才找到原因:
- 浏览器 a 标签 对
[]
不强制序列化 - iOS 标准库 对
[]
强制序列化
于是跨环境传递 URL 时,很容易出现“二次序列化”的问题。
✅ 最佳实践:
- 要么在生成 URL 时统一转义规则,显式 encode
[]
- 要么在前后端约定好统一的参数序列化方案(推荐用
URLSearchParams
)
这样,就能避免再掉进这个双重编码的坑。