Base64 编解码原理与应用场景:JavaScript 中如何正确处理中文与 Emoji

Base64 是什么

Base64 是一种将二进制数据用 64 个可打印 ASCII 字符表示的编码方案。这 64 个字符包括:

  • A-Z(26 个大写字母)
  • a-z(26 个小写字母)
  • 0-9(10 个数字)
  • +/(2 个特殊符号)

加上填充字符 =,构成了完整的 Base64 字符表。它的核心作用是让二进制数据能安全通过只支持文本的通道传输,例如电子邮件、JSON、URL、HTTP 头部等。

为什么需要 Base64

许多协议和历史系统设计为只处理 ASCII 文本。如果直接传输二进制字节,可能出现:

  • 字节流中的 0x00(空字符)被当作字符串结尾截断
  • 控制字符(如 0x0A 换行、0x0D 回车)破坏协议解析
  • 非 ASCII 字节在不同编码下被错误解释

Base64 把任意字节流转换为纯 ASCII 可见字符,绕开这些问题。代价是体积膨胀约 33%(每 3 字节编码为 4 字符)。

编码原理

Base64 将输入字节按 每 3 字节(24 位)一组 重新切分为 4 个 6 位块,每个 6 位块映射到字符表中的一个字符:

原始字节:  01000001 01000010 01000011  (A B C, 3 字节)
重新分组:  010000 010100 001001 000011
十进制:      16     20      9      3
字符表:       Q      U       J      D
结果:      "QUJD"

填充规则:当输入字节数不是 3 的倍数时:

  • 剩 1 字节:编码为 2 个字符 + ==
  • 剩 2 字节:编码为 3 个字符 + =

例如 "A"(1 字节)编码为 "QQ==""AB"(2 字节)编码为 "QUI="

JavaScript 中的 Base64 API

基础 API:btoa / atob

浏览器提供 btoa(binary to ASCII)和 atob(ASCII to binary)两个全局函数:

const encoded = btoa('Hello'); // "SGVsbG8="
const decoded = atob('SGVsbG8='); // "Hello"

中文陷阱:btoa 不能直接处理中文

btoa 只接受 Latin1 字符(码点 ≤ 255)。直接编码中文会抛错:

btoa('你好'); // Uncaught DOMException: Failed to execute 'btoa'

原因:JavaScript 字符串使用 UTF-16,中文字符码点超过 255,btoa 无法处理。

正确方案:TextEncoder + Uint8Array

TextEncoder 将字符串转为 UTF-8 字节序列,再逐字节转换为 Latin1 字符串后编码:

// 编码:支持中文、Emoji 等任意 Unicode 字符
function encodeBase64(str) {
  const bytes = new TextEncoder().encode(str);
  // 将 UTF-8 字节流按 Latin1 字符串处理,再交给 btoa
  let binary = '';
  for (const b of bytes) {
    binary += String.fromCharCode(b);
  }
  return btoa(binary);
}

// 解码:还原 UTF-8 字节流
function decodeBase64(b64) {
  const binary = atob(b64);
  const bytes = new Uint8Array(binary.length);
  for (let i = 0; i < binary.length; i++) {
    bytes[i] = binary.charCodeAt(i);
  }
  return new TextDecoder().decode(bytes);
}

encodeBase64('你好 🌍'); // "5L2g5aW9IPCfjoI="
decodeBase64('5L2g5aW9IPCfjoI='); // "你好 🌍"

大文本分块优化

String.fromCharCode.apply 在超长数组上会触发调用栈溢出。生产环境应分块处理:

function encodeBase64Large(str) {
  const bytes = new TextEncoder().encode(str);
  const CHUNK = 0x8000; // 32KB 一块
  let binary = '';
  for (let i = 0; i < bytes.length; i += CHUNK) {
    binary += String.fromCharCode.apply(null, bytes.subarray(i, i + CHUNK));
  }
  return btoa(binary);
}

URL 安全变体

标准 Base64 中的 +/ 在 URL 中有特殊含义(+ 表示空格,/ 是路径分隔符),会导致解析错误。URL 安全 Base64 做两个替换:

  • +-
  • /_
  • 通常去掉末尾的 =
function toUrlSafe(b64) {
  return b64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
}

function fromUrlSafe(b64) {
  // 补齐填充
  let str = b64.replace(/-/g, '+').replace(/_/g, '/');
  while (str.length % 4) str += '=';
  return str;
}

URL 安全变体常见于 JWT(JSON Web Token)、Data URL、文件名生成等场景。

Base64 的常见应用场景

1. 图片内联(Data URL)

小图标可以直接内联到 HTML/CSS 中,减少 HTTP 请求:

<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUg..." alt="图标" />
.icon {
  background-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxu...');
}

注意:仅适合几 KB 的小图。大图用 Base64 反而增大体积(+33%),且无法被浏览器缓存。

2. JWT(JSON Web Token)

JWT 的 Header 和 Payload 使用 URL 安全 Base64 编码:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.signature

3. 邮件附件(MIME)

SMTP 协议只支持 ASCII,邮件附件通过 Base64 编码后传输。

4. 在 JSON 中嵌入二进制

JSON 不支持原生二进制,文件上传、加密结果等需以 Base64 字符串形式携带。

5. Source Map 与字体文件

Webpack/Vite 的 source map、WOFF 字体嵌入都使用 Base64。

Base64 常见陷阱

陷阱说明解决方案
中文报错btoa 不支持 UTF-16 字符用 TextEncoder 转 UTF-8
体积膨胀编码后体积 +33%大文件避免使用
不可加密Base64 是编码不是加密敏感数据需额外加密
填充丢失= 后部分库无法解码解码前补齐 =
编码混淆看起来像乱码易误判为加密文档明确标注是 Base64

Base64 vs Hex vs Base32

编码字符集体积膨胀适用场景
Base6464 字符+33%通用,最大密度
Base3232 字符+60%大小写不敏感场景
Hex16 字符+100%哈希摘要、密钥展示

选择依据:密度优先选 Base64,兼容性优先选 Hex

总结

  • Base64 用于让二进制安全通过文本通道,不是加密
  • 编码原理:3 字节 → 4 字符,不足补 =
  • 处理中文必须用 TextEncoder 转 UTF-8,再交给 btoa
  • URL 场景使用 URL 安全变体(-_
  • 大文件慎用,体积膨胀 33% 且无法缓存

想要快速编解码 Base64?试试我们的 Base64 在线工具,支持中文、Emoji、URL 安全变体,所有数据在浏览器本地处理,零上传零追踪。