在 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代表一个十六进制数字)进行替代(编码方法)或者反向进行替代(解码方法)。
xxxURIComponent
和 xxxURI
的区别是处理的字符集范围不同。
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()
这两个方法抛出异常的场景相同,有以下两种情况:
- 超出字符串长度限制
VM348:1 Uncaught RangeError: Invalid string length
- 含有
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()
这两个方法抛出异常的场景也相同,有以下两种情况:
- 后面没有两个十六进制数字的%
- 转义序列未编码有效的 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
处理方法
这种情况的字符串其实挺常见的,为了避免这种报错可以采取下面的措施:
- 不对未编码的字符串进行解码
- 使用
try catch
捕获异常
最佳实践
根据上面的分析,可以发现,无论是编码还是解码,在无法百分百确定入参可靠性的情况下,一定要在调用方法的时候使用 try catch
捕获异常并处理。
总结
理解和正确使用 encodeURI()
、decodeURI()
、encodeURIComponent()
和 decodeURIComponent()
方法对于处理 URL 数据至关重要。这些方法可以确保数据正确传递和解析,但需要在实际使用中遵循注意事项,以保证代码的稳定性和可靠性。只有在遵循最佳实践的基础上,我们才能在 Web 开发中高效地处理 URL。无论是构建网页还是开发 Web 应用程序,对这些方法的理解都是不可或缺的。