[譯]JavaScript中Base64編碼字符串的細(xì)節(jié)
當(dāng)前位置:點(diǎn)晴教程→知識(shí)管理交流
→『 技術(shù)文檔交流 』
本文作者為 360 奇舞團(tuán)前端開(kāi)發(fā)工程師 本文為翻譯 原文標(biāo)題:The nuances of base64 encoding strings in Javascript 原文作者:Matt Joseph 原文鏈接:https://web.dev/articles/base64-encoding Base64編碼和解碼是一種常見(jiàn)的將二進(jìn)制內(nèi)容轉(zhuǎn)換為適合Web的文本的形式。它通常用于data URLs,比如內(nèi)嵌圖片。 當(dāng)你在Javascript中對(duì)字符串應(yīng)用base64編碼和解碼時(shí)會(huì)發(fā)生什么?這篇文章探討了這些細(xì)節(jié)和需要避免的常見(jiàn)陷阱。 btoa() 和 atob() 函數(shù) Javascript中進(jìn)行base64編碼和解碼的核心函數(shù)是btoa()和atob()。btoa()用于將字符串轉(zhuǎn)換為base64編碼的字符串,而atob()則用于解碼。 下面是一個(gè)快速示例: // 一個(gè)非常簡(jiǎn)單的字符串,僅包含低于128的代碼點(diǎn)。 const asciiString = 'hello';
// 這將會(huì)成功,它將打?。?/span> // 編碼后的字符串: [aGVsbG8=] const asciiStringEncoded = btoa(asciiString); console.log(`Encoded string: [${asciiStringEncoded}]`);
// 這也將會(huì)成功,它將打?。?/span> // 解碼后的字符串: [hello] const asciiStringDecoded = atob(asciiStringEncoded); console.log(`Decoded string: [${asciiStringDecoded}]`); 不幸的是,正如MDN文檔所指出的,這只適用于包含ASCII字符的字符串,即可以用單個(gè)字節(jié)表示的字符。換句話說(shuō),這對(duì)于Unicode來(lái)說(shuō)不起作用。 要理解發(fā)生了什么,請(qǐng)嘗試以下代碼: // 示例字符串表示了小、中、大代碼點(diǎn)的組合。 // 這個(gè)示例字符串是有效的UTF-16。 // 'hello' 的代碼點(diǎn)都低于128。 // '⛳' 是一個(gè)16位代碼單元。 // '❤️' 是兩個(gè)16位代碼單元,U+2764 和 U+FE0F(一個(gè)心形和一個(gè)變體)。 // '🧀' 是一個(gè)32位代碼點(diǎn)(U+1F9C0),也可以表示為兩個(gè)16位代碼單元的替代對(duì) '\ud83e\uddc0'。 const validUTF16String = 'hello⛳❤️🧀';
// 這將不會(huì)成功。它將打印: // DOMException: Failed to execute 'btoa' on 'Window': The string to be encoded contains characters outside of the Latin1 range. try { const validUTF16StringEncoded = btoa(validUTF16String); console.log(`Encoded string: [${validUTF16StringEncoded}]`); } catch (error) { console.log(error); } 字符串中的任何一個(gè)表情符號(hào)都會(huì)導(dǎo)致錯(cuò)誤。為什么Unicode會(huì)引起這個(gè)問(wèn)題? 為了理解,讓我們先退后一步,深入了解計(jì)算機(jī)科學(xué)和Javascript中的字符串。 Unicode和Javascript中的字符串 Unicode是當(dāng)前的全球字符編碼標(biāo)準(zhǔn),它是將數(shù)字分配給特定字符的實(shí)踐,以便在計(jì)算機(jī)系統(tǒng)中使用。有關(guān)Unicode的更深入了解,請(qǐng)?jiān)L問(wèn)W3C的文章。 h - 104 ñ - 241 ❤ - 2764 ❤️ - 2764 帶有一個(gè)隱藏的修改編號(hào)65039 ⛳ - 9971 🧀 - 129472 表示每個(gè)字符的數(shù)字被稱為“代碼點(diǎn)”。您可以將“代碼點(diǎn)”視為每個(gè)字符的地址。在紅心表情符號(hào)中,實(shí)際上有兩個(gè)代碼點(diǎn):一個(gè)用于心形,另一個(gè)用于“變化”顏色并使其始終為紅色。 深入了解變體選擇器的概念。 Unicode有兩種常見(jiàn)的方法將這些代碼點(diǎn)轉(zhuǎn)換為計(jì)算機(jī)可以一致解釋的字節(jié)序列:UTF-8和UTF-16。 一個(gè)過(guò)于簡(jiǎn)化的視角是: 在UTF-8中,一個(gè)代碼點(diǎn)可以使用一到四個(gè)字節(jié)(每個(gè)字節(jié)8位)。 在UTF-16中,一個(gè)代碼點(diǎn)始終是兩個(gè)字節(jié)(16位)。 重要的是,Javascript處理字符串時(shí)使用的是UTF-16。這破壞了像btoa()這樣的函數(shù),這些函數(shù)實(shí)際上是基于這樣一個(gè)假設(shè):字符串中的每個(gè)字符映射到一個(gè)單字節(jié)。MDN上明確說(shuō)明了這一點(diǎn): The btoa() method creates a Base64-encoded ASCII string from a binary string (i.e., a string in which each character in the string is treated as a byte of binary data). 現(xiàn)在您知道Javascript中的字符通常需要不止一個(gè)字節(jié),下一部分將演示如何處理這種情況下的base64編碼和解碼。 btoa()和atob()與Unicode 正如您現(xiàn)在所知,拋出的錯(cuò)誤是由于我們的字符串包含位于單個(gè)字節(jié)之外的UTF-16字符。 幸運(yùn)的是,MDN關(guān)于base64的文章包含了一些有用的示例代碼來(lái)解決這個(gè)“Unicode問(wèn)題”。您可以修改這些代碼以適應(yīng)前面的示例: // 來(lái)自https://developer.mozilla.org/en-US/docs/Glossary/Base64#the_unicode_problem。 function base64ToBytes(base64) { const binString = atob(base64); return Uint8Array.from(binString, (m) => m.codePointAt(0)); }
// 來(lái)自https://developer.mozilla.org/en-US/docs/Glossary/Base64#the_unicode_problem。 function bytesToBase64(bytes) { const binString = String.fromCodePoint(...bytes); return btoa(binString); }
// 示例字符串表示了小、中、大代碼點(diǎn)的組合。 // 這個(gè)示例字符串是有效的UTF-16。 // 'hello' 的代碼點(diǎn)都低于128。 // '⛳' 是一個(gè)16位代碼單元。 // '❤️' 是兩個(gè)16位代碼單元,U+2764 和 U+FE0F(一個(gè)心形和一個(gè)變體)。 // '🧀' 是一個(gè)32位代碼點(diǎn)(U+1F9C0),也可以表示為兩個(gè)16位代碼單元的替代對(duì) '\ud83e\uddc0'。 const validUTF16String = 'hello⛳❤️🧀';
// 這將會(huì)成功。它將打?。?/span> // 編碼后的字符串: [aGVsbG/im7PinaTvuI/wn6eA] const validUTF16StringEncoded = bytesToBase64(new TextEncoder().encode(validUTF16String)); console.log(`Encoded string: [${validUTF16StringEncoded}]`);
// 這將會(huì)成功。它將打印: // 解碼后的字符串: [hello⛳❤️🧀] const validUTF16StringDecoded = new TextDecoder().decode(base64ToBytes(validUTF16StringEncoded)); console.log(`Decoded string: [${validUTF16StringDecoded}]`);The following steps explain what this code does to encode the string: 使用TextEncoder接口將UTF-16編碼的Javascript字符串轉(zhuǎn)換為UTF-8編碼的字節(jié)流,可通過(guò)TextEncoder.encode()實(shí)現(xiàn)。 這將返回一個(gè)Uint8Array,這是Javascript中較少使用的數(shù)據(jù)類型,是TypedArray的子類。 將這個(gè)Uint8Array提供給bytesToBase64()函數(shù),該函數(shù)使用String.fromCodePoint()將Uint8Array中的每個(gè)字節(jié)作為代碼點(diǎn)處理,并從中創(chuàng)建一個(gè)字符串,其結(jié)果為一個(gè)可以全部用單個(gè)字節(jié)表示的代碼點(diǎn)的字符串。 使用btoa()對(duì)該字符串進(jìn)行base64編碼。 解碼過(guò)程與此相同,但順序相反。 這有效的原因是,Uint8Array和字符串之間的步驟保證了雖然Javascript中的字符串是以UTF-16的兩字節(jié)編碼表示的,但每?jī)蓚€(gè)字節(jié)代表的代碼點(diǎn)始終小于128。 這段代碼在大多數(shù)情況下都工作良好,但在其他情況下會(huì)悄悄地失敗。 靜默失敗的案例 使用相同的代碼,但使用不同的字符串: // 來(lái)自https://developer.mozilla.org/en-US/docs/Glossary/Base64#the_unicode_problem。 function base64ToBytes(base64) { const binString = atob(base64); return Uint8Array.from(binString, (m) => m.codePointAt(0)); }
// 來(lái)自https://developer.mozilla.org/en-US/docs/Glossary/Base64#the_unicode_problem。 function bytesToBase64(bytes) { const binString = String.fromCodePoint(...bytes); return btoa(binString); }
// 示例字符串表示了小、中、大代碼點(diǎn)的組合。 // 這個(gè)示例字符串是無(wú)效的UTF-16。 // 'hello' 的代碼點(diǎn)都低于128。 // '⛳' 是一個(gè)16位代碼單元。 // '❤️' 是兩個(gè)16位代碼單元,U+2764 和 U+FE0F(一個(gè)心形和一個(gè)變體)。 // '🧀' 是一個(gè)32位代碼點(diǎn)(U+1F9C0),也可以表示為兩個(gè)16位代碼單元的替代對(duì) '\ud83e\uddc0'。 // '\uDE75' 是代理對(duì)中的一半。 const partiallyInvalidUTF16String = 'hello⛳❤️🧀\uDE75';
// 這將會(huì)成功。它將打?。?/span> // 編碼后的字符串: [aGVsbG/im7PinaTvuI/wn6eA77+9] const partiallyInvalidUTF16StringEncoded = bytesToBase64(new TextEncoder().encode(partiallyInvalidUTF16String)); console.log(`Encoded string: [${partiallyInvalidUTF16StringEncoded}]`);
// 這也將會(huì)成功。它將打印: // 解碼后的字符串: [hello⛳❤️🧀�] const partiallyInvalidUTF16StringDecoded = new TextDecoder().decode(base64ToBytes(partiallyInvalidUTF16StringEncoded)); console.log(`Decoded string: [${partiallyInvalidUTF16StringDecoded}]`); 如果您查看解碼后的最后一個(gè)字符(�)的十六進(jìn)制值,您會(huì)發(fā)現(xiàn)它是\uFFFD而不是原來(lái)的\uDE75。它沒(méi)有失敗或拋出錯(cuò)誤,但輸入和輸出數(shù)據(jù)已經(jīng)悄悄地改變了。為什么會(huì)這樣? Javascript API中的字符串變化 如前所述,Javascript將字符串處理為UTF-16。但是UTF-16字符串有一個(gè)獨(dú)特的屬性。 以奶酪表情為例。這個(gè)表情(🧀)的Unicode代碼點(diǎn)是129472。不幸的是,16位數(shù)的最大值是65535!那么UTF-16是如何表示這個(gè)更高的數(shù)字的呢? UTF-16有一個(gè)稱為代理對(duì)的概念。您可以這樣想: 對(duì)中的第一個(gè)數(shù)字指定要搜索的“書(shū)籍”。這被稱為 "surrogate"。 對(duì)中的第二個(gè)數(shù)字是“書(shū)籍”中的條目。 您可以想象,有時(shí)僅擁有代表書(shū)籍的數(shù)字而沒(méi)有實(shí)際書(shū)籍中的條目可能是有問(wèn)題的。在UTF-16中,這被稱為 lone surrogate。 這在Javascript中尤其具有挑戰(zhàn)性,因?yàn)橐恍〢PI盡管存在單獨(dú)代理也能工作,而其他API則會(huì)失敗。 在前面的例子中,您在從base64解碼回來(lái)時(shí)使用了TextDecoder。特別是,TextDecoder的默認(rèn)設(shè)置指定了以下內(nèi)容: 它默認(rèn)為false,這意味著解碼器用替代字符替換格式錯(cuò)誤的數(shù)據(jù)。 您之前觀察到的那個(gè)�字符,用十六進(jìn)制表示為\uFFFD,就是那個(gè)替代字符。在UTF-16中,帶有單獨(dú)代理的字符串被視為“格式錯(cuò)誤的”或“不規(guī)范的”。 有各種Web標(biāo)準(zhǔn)(示例1, 2, 3, 4)準(zhǔn)確指定了格式錯(cuò)誤的字符串何時(shí)影響API行為,但值得注意的是TextDecoder是這些API之一。在進(jìn)行文本處理之前確保字符串格式規(guī)范是一個(gè)好習(xí)慣 檢查格式良好的字符串 最近版本的瀏覽器現(xiàn)在具有用于此目的的函數(shù):isWellFormed(). 瀏覽器支持: isWellFormed(). 您可以通過(guò)使用encodeURIComponent()來(lái)實(shí)現(xiàn)類似的結(jié)果,如果字符串包含單獨(dú)代理,則會(huì)拋出URIError錯(cuò)誤。 以下函數(shù)在可用時(shí)使用isWellFormed(),如果不可用則使用encodeURIComponent()。類似的代碼可用于創(chuàng)建isWellFormed()的polyfill。 // 由于舊版瀏覽器不支持isWellFormed(),可以快速創(chuàng)建polyfill。 // encodeURIComponent()對(duì)于單獨(dú)代理會(huì)拋出錯(cuò)誤,這本質(zhì)上是相同的。 function isWellFormed(str) { if (typeof(str.isWellFormed)!="undefined") { // 使用更新的isWellFormed()功能。 return str.isWellFormed(); } else { // 使用較老的encodeURIComponent()。 try { encodeURIComponent(str); return true; } catch (error) { return false; } } } 將所有內(nèi)容整合在一起 現(xiàn)在您已經(jīng)知道如何處理Unicode和單獨(dú)代理,您可以將所有內(nèi)容整合在一起,創(chuàng)建能夠處理所有情況并且不會(huì)進(jìn)行靜默文本替換的代碼。 // 來(lái)自https://developer.mozilla.org/en-US/docs/Glossary/Base64#the_unicode_problem. function base64ToBytes(base64) { const binString = atob(base64); return Uint8Array.from(binString, (m) => m.codePointAt(0)); }
// 來(lái)自https://developer.mozilla.org/en-US/docs/Glossary/Base64#the_unicode_problem. function bytesToBase64(bytes) { const binString = String.fromCodePoint(...bytes); return btoa(binString); }
// 由于舊版瀏覽器不支持isWellFormed(),可以快速創(chuàng)建polyfill。 // encodeURIComponent()對(duì)于單獨(dú)代理會(huì)拋出錯(cuò)誤,這本質(zhì)上是相同的。 function isWellFormed(str) { if (typeof(str.isWellFormed)!="undefined") { // Use the newer isWellFormed() feature. return str.isWellFormed(); } else { // Use the older encodeURIComponent(). try { encodeURIComponent(str); return true; } catch (error) { return false; } } }
const validUTF16String = 'hello⛳❤️🧀'; const partiallyInvalidUTF16String = 'hello⛳❤️🧀\uDE75';
if (isWellFormed(validUTF16String)) { // 這將會(huì)成功。它將打?。?/span> // 編碼后的字符串: [aGVsbG/im7PinaTvuI/wn6eA] const validUTF16StringEncoded = bytesToBase64(new TextEncoder().encode(validUTF16String)); console.log(`Encoded string: [${validUTF16StringEncoded}]`);
// 這將會(huì)成功。它將打?。?/span> // 解碼后的字符串: [hello⛳❤️🧀] const validUTF16StringDecoded = new TextDecoder().decode(base64ToBytes(validUTF16StringEncoded)); console.log(`Decoded string: [${validUTF16StringDecoded}]`); } else { // 忽略 }
if (isWellFormed(partiallyInvalidUTF16String)) { // 忽略 } else { // 這不是一個(gè)格式良好的字符串,因此我們要處理這種情況。 console.log(`Cannot process a string with lone surrogates: [${partiallyInvalidUTF16String}]`); } 這段代碼可以進(jìn)行許多優(yōu)化,比如將其泛化為一個(gè)polyfill,將TextDecoder的參數(shù)更改為在單獨(dú)代理處拋出而不是默默替換,以及其他。有了這些知識(shí)和代碼,您還可以明確決定如何處理格式不正確的字符串,比如拒絕數(shù)據(jù)或明確啟用數(shù)據(jù)替換,或者為以后分析而拋出錯(cuò)誤。除了作為base64編碼和解碼的一個(gè)有價(jià)值的例子外,本文還提供了一個(gè)例子,說(shuō)明仔細(xì)處理文本數(shù)據(jù)尤其重要,特別是當(dāng)文本數(shù)據(jù)來(lái)自用戶生成或外部來(lái)源時(shí)。 - END - ———————————————— 版權(quán)聲明:本文為CSDN博主「奇舞周刊」的原創(chuàng)文章,遵循CC 4.0 BY-SA版權(quán)協(xié)議,轉(zhuǎn)載請(qǐng)附上原文出處鏈接及本聲明。 原文鏈接:https://blog.csdn.net/qiwoo_weekly/article/details/134522065 該文章在 2023/11/27 16:04:38 編輯過(guò) |
關(guān)鍵字查詢
相關(guān)文章
正在查詢... |