上传文件
  1. base64hub
  2. 在代码中使用 Base64
  3. Node.js
代码

Node.js 中的 Base64

Node.js 通过 Buffer 处理 Base64,内置了 base64 和 base64url 两种编码。下面的每个输出都来自 Node.js 24。

编码和解码字符串

Buffer.from(string, "utf8") 把文本转成 UTF-8 字节,.toString("base64") 再对其编码。解码就是把这两个调用反过来。

const enc = Buffer.from("héllo wörld", "utf8").toString("base64");
console.log(enc);                                         // aMOpbGxvIHfDtnJsZA==

console.log(Buffer.from(enc, "base64").toString("utf8")); // héllo wörld

两个调用的默认值都是 "utf8",但显式写出来意图更清楚。Buffer 是 Uint8Array 的子类,所以普通的 Uint8Array 需要先包装一下:u8.toString("base64") 会忽略参数并返回 "104,105",而 Buffer.from(u8).toString("base64") 返回 aGk=。

URL 安全的 Base64(base64url)

Node 为 URL 安全字母表提供了专门的编码,它从不写出填充:

Buffer.from("<<???>>").toString("base64");     // PDw/Pz8+Pg==
Buffer.from("<<???>>").toString("base64url");  // PDw_Pz8-Pg
Buffer.from("PDw_Pz8-Pg", "base64url").toString();  // <<???>>

如果接收方需要带填充的 URL 安全 Base64,就自己补上:s + "=".repeat((4 - s.length % 4) % 4)。

在解码这一侧,Node 很宽容:"base64" 和 "base64url" 都接受任一字母表,也不在乎有没有填充。

Buffer.from("PDw_Pz8-Pg==", "base64").toString();   // <<???>>
Buffer.from("PDw/Pz8+Pg==", "base64url").toString(); // <<???>>

这对 JWT 和各种令牌来说很方便(可以试试 JWT 解码器),但请看下文的校验部分。两种字母表的背景知识见 Base64 与 Base64URL 的区别。

编码和解码文件

fs 接受 "base64" 作为编码参数,两个方向都能省一步:

import { readFileSync, writeFileSync } from "node:fs";

readFileSync("hello.txt").toString("base64");  // aGVsbG8K
readFileSync("hello.txt", "base64");           // aGVsbG8K  (same thing)

writeFileSync("copy.txt", Buffer.from("aGVsbG8K", "base64"));
writeFileSync("copy2.txt", "aGVsbG8K", "base64");  // decodes while writing

要把文件嵌入 HTML 或 CSS,给结果加上前缀即可:`data:image/png;base64,${b64}`。Data URI 生成器 可以在浏览器中生成同样的字符串。

流式处理大文件

为了编码而把一个 2 GB 的文件整个读进内存,不是个好主意。Transform 流可以逐块编码,前提是每次只编码完整的 3 字节组,并把余下的字节带到下一块:

import { createReadStream, createWriteStream } from "node:fs";
import { pipeline } from "node:stream/promises";
import { Transform } from "node:stream";

class Base64Encode extends Transform {
  constructor() { super(); this.rest = Buffer.alloc(0); }
  _transform(chunk, _enc, cb) {
    const buf = Buffer.concat([this.rest, chunk]);
    const cut = buf.length - (buf.length % 3);
    this.rest = buf.subarray(cut);
    cb(null, buf.subarray(0, cut).toString("base64"));
  }
  _flush(cb) { cb(null, this.rest.toString("base64")); }
}

await pipeline(createReadStream("big.bin"), new Base64Encode(),
               createWriteStream("big.b64"));

在一个 1 MB 的随机文件上,输出与 readFileSync("big.bin").toString("base64") 完全一致。如果不做这种进位处理,每个不是 3 的倍数的块边界都会在输出中间插入 = 填充。原因见 Base64 的工作原理。

想不解码就知道解码后的大小,可以用 Buffer.byteLength("aGVsbG8K", "base64"),它返回 6。

校验输入

Buffer.from(s, "base64") 从不因为内容有误而报错。它会丢弃两种字母表中都没有的字符,然后返回剩下的部分:

Buffer.from("aGk=!!", "base64");             // <Buffer 68 69>
Buffer.from("not base64 at all", "base64");  // <Buffer 9e 8b 5b 6a c7 ba e1 ab 5a 96>
Buffer.from("aGk", "base64").toString();     // hi  (missing padding is fine)

对于不可信的输入,先检查字母表和长度,再重新编码并比较。这种往返检查可以发现 aGl= 这样填充位不为零的情况,它同样解码为 hi,但并不规范。

function isBase64(s) {
  return /^[A-Za-z0-9+/]*={0,2}$/.test(s) && s.length % 4 === 0 &&
         Buffer.from(s, "base64").toString("base64") === s;
}

isBase64("aGk=");        // true
isBase64("aGk");         // false (no padding)
isBase64("aGl=");        // false (non-canonical)
isBase64("not base64");  // false

对于 base64url,把正则改成 [A-Za-z0-9_-] 并去掉长度检查。Base64 校验工具 以交互方式提供同样的检查。

常见陷阱与报错信息

  • 参数类型错误:Buffer.from(123, "base64") 会抛出 TypeError [ERR_INVALID_ARG_TYPE]: The first argument must be of type string or an instance of Buffer, ArrayBuffer, or Array or an Array-like Object. Received type number (123)。通常是从 JSON 里混进了一个 undefined 或数字。
  • new Buffer():出于安全原因,这个构造函数已被废弃(DEP0005)。请用 Buffer.from 或 Buffer.alloc。
  • 把二进制数据当文本解码:对图片字节调用 .toString("utf8") 会产生替换字符(U+FFFD)而不报错,写回文件后文件就损坏了。二进制数据请保留为 Buffer。
  • Data URI 前缀:Buffer.from("data:image/png;base64,iVBOR...", "base64") 不会失败,它会把前缀当成垃圾数据一起解码。请先在逗号处拆分。
  • 换行符:Node 解码时会忽略 \n 和 \r,所以按 MIME 换行的输入没有问题,但编码器从不换行。需要 MIME 分行 时,用 b64.match(/.{1,76}/g).join("\r\n")。

版本说明

  • base64url 编码:Node.js 15.7.0 和 14.18.0。
  • 全局函数 btoa 和 atob:Node.js 16.0,被标记为遗留 API。
  • Uint8Array.prototype.toBase64 和 Uint8Array.fromBase64 在 Node.js 24 中不可用(typeof 返回 "undefined")。它们会随更新的 V8 版本到来;在那之前,请使用 Buffer。
  • Buffer.from(string, "base64") 在所有版本中都会忽略空白和无效字符,所以不要依赖它来做校验。

常见问题

Buffer.from(str, 'base64') 遇到无效输入会报错吗?

不会。Node 会跳过字母表之外的字符,并在第一个 = 处停止,所以乱七八糟的输入也会解码出一些字节,而且没有任何报错。如果输入不可信,请自己校验字符串。

哪个 Node.js 版本加入了 base64url?

base64url 编码在 Node.js 15.7.0 中加入,并向后移植到了 14.18.0。在任何当前版本上都可以放心使用。

在 Node.js 中应该用 btoa 吗?

Node 把 btoa 和 atob 标记为遗留 API,推荐使用 Buffer。它们是为了兼容浏览器而存在的,同样有 JavaScript 中的 Base64 里提到的 Latin-1 限制。

继续阅读