编码和解码字符串
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 限制。