URL 编码和解码 - 看这一篇就够了

在 Web 开发中,处理 URL 是一个常见且至关重要的任务。由于 URL 中可能含有特殊字符和非 ASCII 字符,确保 URL 的正确性和可传递性就成为了一个挑战。为了解决这个问题,JavaScript 提供了一些内置的方法来进行 URL 编码和解码,其中包括 encodeURI()decodeURI()encodeURIComponent()decodeURIComponent()。本文将深入探讨这些方法的使用、注意事项以及可能出现的异常情况。

URL 编码和解码方法简介

  • encodeURI(): 这个方法用于对整个 URL 进行编码,包括协议、域名、路径和查询参数。它将非 ASCII 字符和特殊字符转换为十六进制格式,确保它们可以在 URL 中安全传输。但需要注意,保留字符(如 :/?& 等)不会被编码,因为它们在 URL 中具有特殊含义。

  • decodeURI():encodeURI() 对应,decodeURI() 用于解码已经被编码的 URL。它将十六进制格式的字符恢复为原始字符,使 URL 可以正常解析。

  • encodeURIComponent(): 与前两者不同,encodeURIComponent() 主要用于编码查询参数等特定部分。除了非 ASCII 字符,它还会对保留字符进行编码,以确保不会被误解为 URL 的一部分。

  • decodeURIComponent(): decodeURIComponent() 用于解码使用 encodeURIComponent() 编码的部分。它将编码的字符还原为原始字符,使我们能够正确获取查询参数等信息。

注意事项和异常情况

注意事项

这些方法都是将需要转义的字符使用 %XX (X代表一个十六进制数字)进行替代(编码方法)或者反向进行替代(解码方法)。
xxxURIComponentxxxURI 的区别是处理的字符集范围不同。

encodeURI 不会转义的字符

A--Z a--z 0--9 - _ . ! ~ * ' ( )
; / ? : @ & = + $ , #

encodeURIComponent 不会转义的字符

A--Z a--z 0--9 - _ . ! ~ * ' ( )

js 复制代码
const set1 = ";/?:@&=+$,#"; // Reserved Characters
const set2 = "-.!~*'()"; // Unreserved Marks
const set3 = "ABC abc 123"; // Alphanumeric Characters + Space

console.log(encodeURI(set1)); // ;/?:@&=+$,#
console.log(encodeURI(set2)); // -.!~*'()
console.log(encodeURI(set3)); // ABC%20abc%20123 (the space gets encoded as %20)

console.log(encodeURIComponent(set1)); // %3B%2C%2F%3F%3A%40%26%3D%2B%24%23
console.log(encodeURIComponent(set2)); // -.!~*'()
console.log(encodeURIComponent(set3)); // ABC%20abc%20123 (the space gets encoded as %20)

encodeURI() & encodeURIComponent()

这两个方法抛出异常的场景相同,有以下两种情况:

  1. 超出字符串长度限制 VM348:1 Uncaught RangeError: Invalid string length
  2. 含有 lone surrogate 非法字符

Invalid string length

原因

这个异常严格上来说不是这两个方法直接导致的,而是字符串本来就有的长度限制。只不过由于编码后长度会增加,可能本来没有超长的字符串转化之后超长了。

长度被限制的原因为 v8 限制字符最大长度 ((1 << 29) - 24) = 536870888。这一点估计很多人不知道,算是个冷知识。

((1 << 29) - 24) = 536870888 = 512M (which is the current browser limit in Chrome too)

h 复制代码
class V8_EXPORT String : public Name {
 public:
  static constexpr int kMaxLength =
      internal::kApiSystemPointerSize == 4 ? (1 << 28) - 16 : (1 << 29) - 24;
js 复制代码
'1'.repeat(536870888).length // 536870888
'1'.repeat(536870889).length // Uncaught RangeError: Invalid string length
'我'.repeat(536870888).length // 536870888
'我'.repeat(536870889).length // Uncaught RangeError: Invalid string length
'😄'.repeat(268435444).length // 536870888
'😄'.repeat(268435445).length // Uncaught RangeError: Invalid string length
'👨‍👦'.repeat(107374177).length // 536870885
'👨‍👦'.repeat(107374178).length // Uncaught RangeError: Invalid string length
处理方法

这么长的字符串一般不太常遇到,而且也没有办处理,所以 try catch 一下就好了。

lone surrogate

原因

这种数据可能存在字符处理的过程中,直接输入基本不会输入这种数据,因为这不是一个能够正常展示的数据。

js 复制代码
// High-low pair OK
encodeURI("\uD800\uDFFF"); // "%F0%90%8F%BF"

// Lone high-surrogate code unit throws "URIError: malformed URI sequence"
encodeURI("\uD800");

// Lone low-surrogate code unit throws "URIError: malformed URI sequence"
encodeURI("\uDFFF");

// High-low pair OK
encodeURIComponent("\uD800\uDFFF"); // "%F0%90%8F%BF"

// Lone high-surrogate code unit throws "URIError: malformed URI sequence"
encodeURIComponent("\uD800");

// Lone high-surrogate code unit throws "URIError: malformed URI sequence"
encodeURIComponent("\uDFFF");

你可能会好奇怎么会有这种数据产生?下面是一些不小心产生了 lone surrogate 数据的操作

js 复制代码
"😄".split(""); // ['\ud83d', '\ude04']; splits into two lone surrogates

// "Backhand Index Pointing Right: Dark Skin Tone"
[..."👉🏿"]; // ['👉', '🏿']
// splits into the basic "Backhand Index Pointing Right" emoji and
// the "Dark skin tone" emoji

// "Family: Man, Boy"
[..."👨‍👦"]; // [ '👨', '‍', '👦' ]
// splits into the "Man" and "Boy" emoji, joined by a ZWJ

// The United Nations flag
[..."🇺🇳"]; // [ '🇺', '🇳' ]
// splits into two "region indicator" letters "U" and "N".
// All flag emojis are formed by joining two region indicator letters

You must be careful which level of characters you are iterating on. For example, split("") will split by UTF-16 code units and will separate surrogate pairs. String indexes also refer to the index of each UTF-16 code unit. On the other hand, @@iterator() iterates by Unicode code points. Iterating through grapheme clusters will require some custom code.

处理方法

那么这种情况有没有办法处理呢?在 chrome 111+ 提供了两个新的方法 toWellFormed() isWellFormed() 来专门处理这个问题。

js 复制代码
const strings = [
  // Lone leading surrogate
  "ab\uD800",
  "ab\uD800c",
  // Lone trailing surrogate
  "\uDFFFab",
  "c\uDFFFab",
  // Well-formed
  "abc",
  "ab\uD83D\uDE04c",
];

for (const str of strings) {
  console.log(str.toWellFormed());
}
// Logs:
// "ab�"
// "ab�c"
// "�ab"
// "c�ab"
// "abc"
// "ab😄c"

这样我们只需要先转化一下再调用就好了。

js 复制代码
const illFormed = "https://example.com/search?q=\uD800";

try {
  encodeURI(illFormed);
} catch (e) {
  console.log(e); // URIError: URI malformed
}

console.log(encodeURI(illFormed.toWellFormed())); // "https://example.com/search?q=%EF%BF%BD"

那如果需要兼容低版本浏览器怎么办呢?try catch 啊,你在想什么好事呢

decodeURI() & decodeURIComponent()

这两个方法抛出异常的场景也相同,有以下两种情况:

  1. 后面没有两个十六进制数字的%
  2. 转义序列未编码有效的 UTF-8 字符

简单来说就是 % 和后面跟着的两个字符组合起来没有编码有效的 UTF-8 字符。

未编码有效的 UTF-8 字符

这种情况会报错是因为这两个方法都是用来解码对应编码方法得到的字符串,所以遇到了 % 又无法进行解析就只能报错了。

js 复制代码
try {
  const a = decodeURI("%E0%A4%A");
} catch (e) {
  console.error(e);
}

// URIError: malformed URI sequence

try {
  const a = decodeURIComponent("%E0%A4%A");
} catch (e) {
  console.error(e);
}

// URIError: malformed URI sequence

处理方法

这种情况的字符串其实挺常见的,为了避免这种报错可以采取下面的措施:

  1. 不对未编码的字符串进行解码
  2. 使用 try catch 捕获异常

最佳实践

根据上面的分析,可以发现,无论是编码还是解码,在无法百分百确定入参可靠性的情况下,一定要在调用方法的时候使用 try catch 捕获异常并处理。

总结

理解和正确使用 encodeURI()decodeURI()encodeURIComponent()decodeURIComponent() 方法对于处理 URL 数据至关重要。这些方法可以确保数据正确传递和解析,但需要在实际使用中遵循注意事项,以保证代码的稳定性和可靠性。只有在遵循最佳实践的基础上,我们才能在 Web 开发中高效地处理 URL。无论是构建网页还是开发 Web 应用程序,对这些方法的理解都是不可或缺的。

参考资料

相关推荐
爱吃桃子的ICer1 小时前
[UVM]3.核心基类 uvm_object 域的自动化 copy() compare() print() pack unpack
开发语言·前端·ic设计
阿洵Rain2 小时前
【Linux】环境变量
android·linux·javascript
学地理的小胖砸3 小时前
【GEE的Python API】
大数据·开发语言·前端·python·遥感·地图学·地理信息科学
垦利不3 小时前
css总结
前端·css·html
八月的雨季 最後的冰吻4 小时前
C--字符串函数处理总结
c语言·前端·算法
6230_4 小时前
关于HTTP通讯流程知识点补充—常见状态码及常见请求方式
前端·javascript·网络·网络协议·学习·http·html
pan_junbiao5 小时前
Vue组件:使用$emit()方法监听子组件事件
前端·javascript·vue.js
hummhumm5 小时前
数据库系统 第46节 数据库版本控制
java·javascript·数据库·python·sql·json·database
正在绘制中6 小时前
如何部署Vue+Springboot项目
前端·vue.js·spring boot
Keep striving6 小时前
SpringMVC基于注解使用:国际化
java·前端·spring·servlet·tomcat·maven