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 llfff[]=jjj%20ll(空格被转义,[] 保留)
  • iOS 里:fff[]=jjj llfff%5B%5D=jjj%20ll(空格和 [] 都被转义)

2. 为什么会出现二次序列化?

当一个 URL 在不同环境下被“来回处理”时:

  1. 浏览器环境里,[] 没有被编码
  2. 传给 iOS 处理时,iOS 认为 [] 必须编码 → %5B%5D
  3. 但如果开发者又手动调用了一次编码方法,就会对 %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

这样,就能避免再掉进这个双重编码的坑。

提示

功能待开通!


暂无评论~