为什么TextEncoder.encode()与File.arrayBuffer()生成结果不一致?
问题复现
以下代码可以稳定复现两个ArrayBuffer长度、内容不一致的问题:
<input type="file" id="upload">
document.querySelector('#upload').addEventListener('change', async (e) => { const initialFile = e.target.files[0]; const arrayBuffer = await initialFile.arrayBuffer(); const text = await initialFile.text(); const enc = new TextEncoder(); const arrayBufferFromText = enc.encode(text).buffer // 两个输出结果不一致 console.log(arrayBuffer.byteLength) console.log(arrayBufferFromText.byteLength); })
差异产生的根本原因
两个ArrayBuffer不一致的核心问题出在File.text()的解码逻辑上:
File.arrayBuffer()是直接读取文件的原始二进制字节,不会做任何转码处理,拿到的内容和磁盘上存储的文件完全一致。File.text()从设计上就是用来读UTF-8编码的文本文件的,它会把原始二进制字节按照UTF-8规则解码成JS字符串。拿它读图片、压缩包这类非文本二进制文件的时候,字节序列里有大量不符合UTF-8编码规则的片段,这些非法片段在解码时会被直接替换成Unicode替换字符U+FFFD(就是平时看到的乱码�),原始字节信息在这一步就已经永久丢失了。- 后面再调用
TextEncoder.encode(),处理的已经是被破坏过的字符串,每个U+FFFD字符会被编码成3个固定字节(0xEF 0xBF 0xBD),和原来的字节长度、内容根本对不上,最后得到的ArrayBuffer自然和原始数据不一样。
哪怕是纯文本文件,如果原文件编码不是UTF-8(比如GBK、Latin1编码),File.text()默认按UTF-8解码一样会出乱码,转码后也没法和原始ArrayBuffer对齐。
让两个缓冲区完全一致的方案
根据使用场景选对应方案即可:
- 不需要字符串中转的场景:直接使用
File.arrayBuffer()读取原始二进制数据,不要走text()转码流程,这是最简单也最安全的方案,没有任何转码损耗。 - 必须通过字符串中转二进制数据的场景:不要用UTF-8做编解码,改用单字节映射的Latin1(ISO-8859-1)字符集做转换,这个字符集的每个字符刚好对应0-0xFF范围的一个字节,不会出现非法字符替换的问题,可以做到二进制和字符串的无损互转,示例代码如下:
document.querySelector('#upload').addEventListener('change', async (e) => { const initialFile = e.target.files[0]; const originalArrayBuffer = await initialFile.arrayBuffer(); const uint8 = new Uint8Array(originalArrayBuffer); // 二进制无损转字符串 const binaryString = String.fromCharCode(...uint8); // 字符串无损转回ArrayBuffer const restoredUint8 = new Uint8Array(binaryString.length); for (let i = 0; i < binaryString.length; i++) { restoredUint8[i] = binaryString.charCodeAt(i); } const arrayBufferFromText = restoredUint8.buffer; // 此时两个ArrayBuffer长度、内容完全一致 console.log(originalArrayBuffer.byteLength === arrayBufferFromText.byteLength); // true })
注意:
TextEncoder只能输出UTF-8编码的内容,无法用于Latin1编码的字符串转二进制场景,不要用它处理无损二进制中转的逻辑。
内容的提问来源于stack exchange,提问作者Limon Monte
相关产品推荐
相关产品推荐

