开发者工具 · URL 编码器和解码器
escape() 与encodeURIComponent:JavaScript 的 URL 编码如何演变
· 背景
JavaScript url 编码 历史
JavaScript 已经有了三代 URL 编码函数,最古老的仍然潜伏在生产代码中。这篇文章解释了 escape() 做错了什么,为什么 ES3 添加了 URI 函数以及它们为什么保留! * ' ( )。
旧日志中的 %u20AC — escape() 的明确无误的指纹及其导致的解码失败
旧版 JavaScript 文件包含使用已弃用的 escape() 函数的 URL 编码调用。日志文件或错误消息中的输出包括序列 %u20AC — 已弃用的 escape() 函数的明确无误的指纹,没有其他人使用。此序列与任何标准 URL 编码都不匹配,基于 RFC 3986 或 WHATWG 规则构建的解码器将无法识别它。数据无法通过现代工具往返。它是 ES3 之前的常见代码符号,自 20 世纪 90 年代以来就没有更新过。
escape() 函数是在 Netscape 时代设计的,当时 JavaScript 还没有标准或正式的 URL 编码规则。它使用 %uXXXX 表示法对大多数非 ASCII 字符进行编码,这是一种没有其他人使用且没有任何标准定义的四位十六进制代码。这对于浏览器中的一次性使用来说是有意义的,但它破坏了与 URL 标准的兼容性,并使数据无法在其他地方解码。
escape() 和 unescape():Netscape 时代的设计 — Latin-1 假设、%uXXXX 发明以及为什么它从未符合任何标准
escape() 和 unescape() 假定输入是 Latin-1 (ISO 8859-1),这是早于 UTF-8 和 Unicode 的字符编码。它们将每个字符转换为十六进制代码,使用 %XX 表示高位拉丁语-1 字符,使用 %uXXXX 表示拉丁语-1 之外的所有字符。像表情符号这样的非拉丁语 1 字符根本无法表示。这些函数既简单又快速,但对于任何现代用例来说它们也是完全错误的。
这两个函数都是在标准存在之前添加到 JavaScript 中的。 ES3 在 1999 中引入正确的 URL 编码后,它们立即被弃用。它们保留在 JavaScript 中是为了向后兼容——删除它们会破坏古老的代码。但任何新代码都不应该使用它们。它们是遗产。
ES3 (1999) 添加encodeURI 和encodeURIComponent — UTF-8 百分比编码与 RFC 2396 一致
ES3 引入了两个函数:encodeURI 和encodeURIComponent。两者都执行 UTF-8 百分比编码:将非 ASCII 字符转换为 UTF-8 字节,然后将每个字节写入 %HH。两者均符合当时有效的 RFC 2396。 RFC 3986 后来出现,并没有改变编码行为。这些功能至今仍然是标准并且应该被使用。
encodeURI 用于编码完整的 URI; encodeURIComponent 用于对 URI 内的组件进行编码,例如查询值或路径段。这种差异绝对是至关重要的并且很容易被误解。 encodeURI 保留结构字符,例如 : / ? #@=& 和 ;。 encodeURIComponent 对所有这些进行编码,使它们可以安全地嵌入到更大的 URI 中。
为什么! * ' ( ) 仍然未编码 — RFC 2396 “标记”字符在 RFC 3986 移动后被冻结到语言中
这两个函数都将这些字符保留为未编码:字母、数字、连字符 (-)、下划线 (_)、句点 (.)、波形符 (~) 和五个标点符号! * ' ( )。这些标记来自 RFC 2396,其中将它们列为非保留的“标记”字符。 RFC 3986 在 2005 中出现,并将这五个移动到不同的类别,但 JavaScript 已经在 1999 中冻结了encodeURI 和encodeURIComponent。改变他们留下的字符会破坏现有的代码,所以他们留下来。
为了向后兼容而保留这五个标记未编码的决定意味着 JavaScript 的编码并不完全符合 RFC 3986 或 WHATWG 标准。它已经足够接近实际使用了,现在改变它是完全不可能的。这是 API 稳定性的一个教训:一旦冻结行为,即使标准不断发展,也无法更改它。
工作示例:通过转义、encodeURI 和encodeURIComponent 的相同字符串 — 比较三个输出
采用字符串“R&D(研究)=咖啡馆”。通过 escape()、encodeURI 和encodeURIComponent 运行它。 escape() 生成“R%26D%20(research)%20%3D%20caf%E9's”,将未编码的括号和撇号与百分比编码的“&”号和等号混合在一起。 encodeURI 生成“R&D%20(research)%20=%20caf%C3%A9's”,保留与号和等号,因为它们是结构性的。 encodeURIComponent 生成“R%26D%20%28research%29%20%3D%20caf%C3%A9%27s”,对包括括号和撇号在内的所有内容进行编码。
将相同的字符串粘贴到URL编码器和解码器中,并在encodeURI和encodeURIComponent之间切换以查看差异。然后检查 escape() 会产生什么(您可以在浏览器控制台中调用它,尽管它会警告您)。您立即会发现这三个函数产生三个完全不同的结果。
迁移脱离 escape() — 将旧调用映射到正确的现代函数并处理存储的 %uXXXX 数据
使用 escape() 的旧代码必须更新。如果 escape() 用于编码 URI 组件,请将其替换为encodeURIComponent。如果它用于编码完整的 URI,请使用encodeURI。对于包含 %uXXXX 序列的存储数据,您需要一个自定义解码器:将每个 %uXXXX 转换为 Unicode 代码点,然后将代码点收集到字符串中。 JavaScript 的内置 unescape() 将读取 %uXXXX,但结果可能不正确 UTF-8。
替换 escape() 后,使用包含非 ASCII 字符、标点符号和特殊字符的字符串测试代码。现在的输出应该符合现代工具和标准的预期。如果您的代码明显早于 ES3,它也可能使用其他过时的模式;全面的审计是值得付出努力的。
这不包括的内容 — URL 和 URLSearchParams API,它们将单独介绍
URL 和 URLSearchParams API(后来添加)为 URL 构造和组件编码提供了更高级别的接口。它们自动处理所有转义并完全匹配 WHATWG URL 标准。它们是在现代 JavaScript 中以编程方式构建 URL 的首选方法。
这篇文章仅涵盖编码函数,而不涉及那些更高级别的 API。 URL 和 URLSearchParams 解析结构,选择组件规则并序列化结果,而encodeURIComponent 转换提供的一个字符串,而不知道它将被放置在哪里。这种区别就是边界:根据旧的 escape() 调用是处理值还是地址来迁移旧的 escape() 调用,然后考虑用结构化 API 替换周围的手动连接作为单独的重构。
要点:三个函数,一对幸存的函数 — URL 编码器和解码器如何并排显示现代的encodeURI 和encodeURIComponent 行为
现代 JavaScript 开发应该使用encodeURI 或encodeURIComponent,而不是escape()。这些函数在 1999 中标准化,从那时起就没有改变。它们将非 ASCII 字符编码为 UTF-8 字节并正确处理标准保留字符。 URL 编码器和解码器工具实现了这两种功能,并让您可以并排查看它们的行为,从而轻松地为您的组件选择正确的功能。
如果您在旧日志或存储的数据中遇到 %u 序列,它们是 escape() 输出,应该迁移。一旦确定了模式,迁移就很简单。现代代码不应该产生它们。