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