Base64 与 UTF-8:排查中文、emoji 和替换字符

追踪 Unicode 文本到 UTF-8 字节,区分编码错误与规范化,并保留无法解释成文本的二进制。

Base64 处理中文后出现乱码,通常是字符与字节之间的边界出了问题。Base64 不知道输入代表中文还是 emoji,它只保存字节。应用负责决定文字如何产生字节,以及字节怎样还原成文字。

跟踪字节,而不只看文字

“中”的码点是 U+4E2D,UTF-8 字节为 E4 B8 AD,对应 Base64 为 5Lit。换一种字符编码,字节和 Base64 都可能改变。比较结果之前,应确认两端都使用 UTF-8。

const bytes = new TextEncoder().encode('中');
console.log([...bytes]); // [228, 184, 173]
console.log([...bytes].map(b => b.toString(16)).join(' ')); // e4 b8 ad

'😊'.length 在 JavaScript 中是两个 UTF-16 码元,而它的 UTF-8 需要四个字节。这些数值都不能直接当作任意文字的视觉字符数。传输和文件计算应使用字节长度。

让替换错误可见

� 可能表示此前某一步已经丢失了信息。未知字节被替换成这个字符后,再次编码通常无法恢复原值。继续转换前,应检查原始解码字节。

const decoder = new TextDecoder('utf-8', { fatal: true });
console.log(decoder.decode(Uint8Array.of(0xe4, 0xb8, 0xad))); // 中
try { decoder.decode(Uint8Array.of(0xff)); }
catch { console.log('非法 UTF-8,应保留原始字节。'); }

规范化是另外一种操作

外观相同的文字可能由不同 Unicode 序列组成。例如 é 可以是预组合字符,也可以是 e 加组合重音。UTF-8 与 Base64 会正确保存这种差异。如果应用需要统一形式,应在编码前明确执行某种规范化,不能把改变字节的操作称为无损修复。

结果不同时,也应检查不可见字符、换行风格和字节序标记。它们都是真实输入,对签名和校验和有影响。建议按“保存原文 → 检查字节 → 确认字符编码 → 明确处理解码错误 → 考虑规范化”的顺序排查。直接 btoa 处理 Unicode 的限制见 MDN: btoa();二进制文件则应完全绕过文本解码。