[JS] JavaScript 中的 Base64 —— atob、btoa 與 Node Buffer
JavaScript 中 Base64 的實務指南。何時該用 btoa/atob、為何它們在 Unicode 上出錯、Buffer 在 Node.js 中的運作方式,以及跨執行環境的 base64url 輔助函式。
// 整體概況
JavaScript 與 Base64 的故事因歷史因素而四分五裂。瀏覽器從 1990 年代的 Netscape 繼承了 btoa() / atob() —— 這兩個函式的名字字面上就是 binary-to-ASCII 與 ASCII-to-binary。它們只能處理 Latin-1 字元,這就是為什麼把 emoji 或中文文字複製貼上丟進 btoa() 會拋出例外。
Node.js 則透過 Buffer 類別走出自己的路。它理解你會用到的所有編碼(utf8、base64、base64url、hex、latin1、ascii),原生處理二進位資料,是伺服器端程式碼的明顯選擇。
現代執行環境(Node 18+、Deno、Bun、現代瀏覽器)也都提供 TextEncoder / TextDecoder —— 這是在做 Base64 編碼之前把 Unicode 字串轉成位元組的標準做法。知道該伸手拿哪個工具,勝負就已經分了一半。
// 瀏覽器:BTOA / ATOB —— 正確用法
btoa(str) 接受一個字串,其中每個字元都必須能塞進 1 個位元組(Latin-1 範圍,U+0000 到 U+00FF),並回傳 Base64 字串。atob(b64) 做反向處理。它們是同步、快速、從古至今每個瀏覽器都內建的函式。
它們的正確用途是:對已經是位元組性質的資料做 Base64。從 FileReader 讀出的原始位元組、從二進位 XMLHttpRequest(responseType: 'arraybuffer')讀出的位元組,或是 Uint8Array,只要每個位元組的數值落在 0–255 之間即可。
// Encode a Latin-1 string
const latin1 = 'Hello, world!';
const encoded = btoa(latin1); // 'SGVsbG8sIHdvcmxkIQ=='
// Decode it back
const decoded = atob(encoded); // 'Hello, world!'
// Encode a binary Uint8Array (the common real-world case)
const bytes = new Uint8Array([0xFF, 0xD8, 0xFF, 0xE0]); // JPEG magic
const binStr = String.fromCharCode(...bytes); // 'ÿØÿà' (Latin-1)
const b64 = btoa(binStr); // '/9j/4A=='
// 為什麼 btoa() 遇到 UNICODE 會拋例外
在瀏覽器主控台試試 btoa('héllo')。你會得到 InvalidCharacterError: The string to be encoded contains characters outside of the Latin1 range。其中 é(U+00E9)還算可以,但 btoa('日本語') 就一定會失敗,因為 日(U+65E5)落在 Latin-1 之外。這個函式沒有 UTF-8 的概念;它把每個 JavaScript String 的程式碼單元當成一個位元組來讀。
解決方法是先把 Unicode 字串轉成 UTF-8 位元組,再對這些位元組做 Base64。每個現代瀏覽器都提供 TextEncoder 正是為了這件事。
// ❌ broken
// btoa('日本語') → throws InvalidCharacterError
// ✅ correct: encode to UTF-8 first
function b64encodeUtf8(str) {
const bytes = new TextEncoder().encode(str);
let bin = '';
for (const byte of bytes) bin += String.fromCharCode(byte);
return btoa(bin);
}
b64encodeUtf8('日本語'); // '5pel5pys6Kqe'
// ✅ round-trip decode
function b64decodeUtf8(b64) {
const bin = atob(b64);
const bytes = Uint8Array.from(bin, c => c.charCodeAt(0));
return new TextDecoder().decode(bytes);
}
b64decodeUtf8('5pel5pys6Kqe'); // '日本語'
// NODE.JS:BUFFER 的做法
在 Node 中,請伸手拿 Buffer。它預設理解 UTF-8、支援世上所有想像得到的編碼,並能乾淨地處理二進位。Node 16+ 也把 'base64url' 視為一個編碼名稱,所以你不再需要另寫一個 URL 安全輔助函式。
// String → Base64
const encoded = Buffer.from('héllo 世界').toString('base64');
// 'aMOpbGxvIOS4lueVjA=='
// Base64 → string (UTF-8 by default)
const decoded = Buffer.from(encoded, 'base64').toString();
// 'héllo 世界'
// URL-safe (Node 16+): +/- omitted padding
const safe = Buffer.from('héllo 世界').toString('base64url');
// 'aMOpbGxvIOS4lueVjA' ← no = padding, uses -_
// Binary file → Base64
const fs = require('node:fs');
const imgB64 = fs.readFileSync('logo.png').toString('base64');
// Base64 → binary file
fs.writeFileSync('out.png', Buffer.from(imgB64, 'base64'));
// BUFFER 與 BTOA/ATOB —— 快速比較
- > Unicode:Buffer 原生處理 UTF-8;btoa/atob 只能處理 Latin-1 位元組。
- > 二進位:Buffer 是 Node 中的原生二進位型別;瀏覽器則必須搭配 Uint8Array + String.fromCharCode。
-
>
URL 安全:Buffer 將
'base64url'視為一等公民的編碼(Node 16+);瀏覽器需要自訂正規表示式替換。 - > 效能:Buffer 以 C++ 實作,在大型輸入上比 atob/btoa 快約 3–5 倍。
- > 串流:Buffer 可搭配 transform stream 處理大型檔案;btoa/atob 無法串流。
- > 可用性:btoa/atob 在 Node 16+ 也以全域函式存在,但有選擇時請優先用 Buffer —— 它能處理各種邊界情境。
// 在瀏覽器中編解碼二進位檔案
瀏覽器中把檔案選擇器的結果轉成 Base64 字串的典型做法是 FileReader.readAsDataURL(),它會回傳一個可直接使用的 data URI。若你只要原始 Base64 內容,就把 data:...;base64, 前綴剝掉即可。
對非常大的檔案,請用 readAsArrayBuffer() 讀取並以分塊方式編碼,以避免 String.fromCharCode(...bytes) 在陣列超過約 65,000 個元素時會遇到「maximum call stack size exceeded」的錯誤。
// ▸ Simple: file → data URI → strip prefix
async function fileToBase64(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => {
const dataUri = reader.result; // 'data:image/png;base64,iVBOR...'
resolve(dataUri.split(',')[1]); // raw Base64 only
};
reader.onerror = reject;
reader.readAsDataURL(file);
});
}
// ▸ Large files: chunked btoa() to avoid stack overflow
async function arrayBufferToBase64(buffer) {
const bytes = new Uint8Array(buffer);
const chunkSize = 0x8000; // 32 KB chunks
let binary = '';
for (let i = 0; i < bytes.length; i += chunkSize) {
binary += String.fromCharCode.apply(null,
bytes.subarray(i, i + chunkSize));
}
return btoa(binary);
}
// Usage
const file = inputEl.files[0];
const buf = await file.arrayBuffer();
const b64 = await arrayBufferToBase64(buf);
// 在瀏覽器中把 BASE64 解回位元組
// ▸ Base64 → Uint8Array (binary bytes)
function base64ToBytes(b64) {
const bin = atob(b64);
const bytes = new Uint8Array(bin.length);
for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
return bytes;
}
// ▸ Base64 → Blob → downloadable
function base64ToBlob(b64, mime = 'application/octet-stream') {
const bytes = base64ToBytes(b64);
return new Blob([bytes], { type: mime });
}
// Trigger a download
const blob = base64ToBlob(b64, 'image/png');
const url = URL.createObjectURL(blob);
Object.assign(document.createElement('a'), { href: url, download: 'out.png' }).click();
URL.revokeObjectURL(url);
// JAVASCRIPT 中的 URL 安全 BASE64 (BASE64URL)
標準 Base64 使用 + 與 /,兩者在 URL 中都是保留字元。它結尾還會有一或兩個 =,會被百分比編碼成 %3D,浪費空間。RFC 4648 §5 定義了 URL 安全變體(base64url):把 + 換成 -、把 / 換成 _,並去掉填充。
在 Node 16+ 中,直接用 Buffer.from(x).toString('base64url')。在瀏覽器中,則把它實作成 btoa() 的後處理步驟。
// ▸ Browser: standard Base64 → URL-safe Base64
function toBase64Url(b64) {
return b64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
}
// ▸ URL-safe Base64 → standard Base64 (before feeding atob)
function fromBase64Url(b64u) {
let b64 = b64u.replace(/-/g, '+').replace(/_/g, '/');
while (b64.length % 4) b64 += '='; // re-add padding
return b64;
}
// Round-trip
const token = toBase64Url(btoa('hello+world/'));
// 'aGVsbG8rd29ybGQv'
atob(fromBase64Url(token)); // 'hello+world/'
// 常見陷阱與如何捕捉
-
>
btoa() 丟出 InvalidCharacterError —— 你的輸入含有 Latin-1 以外的字元。修正:先用
TextEncoder編成 UTF-8。 -
>
atob() 丟出 InvalidCharacterError —— 你的輸入不是有效的 Base64。通常是缺少填充(對 base64url 而言)或包含空白。先去除空白、補回
=填充。 -
>
解碼後 Unicode 亂碼 —— 你直接用
atob()解碼而沒有經過TextDecoder。UTF-8 的多位元組序列會以亂碼呈現。 -
>
'Maximum call stack size exceeded' —— 你在超過約 65,000 個位元組的陣列上使用了
String.fromCharCode(...bytes)。修正:分塊處理(參考前面 32 KB 的迴圈範例)。 - > 瀏覽器與 Node 輸出不同 —— 通常代表其中一邊是 URL 安全,另一邊是標準。在介面邊界統一成同一種變體。
-
>
重複編碼的內容 —— 資料被 Base64 兩次。檢查:
atob(input)看起來是不是又像 Base64?那就再解一次。 - > JSON 中的 Base64 被意外再 JSON 編碼 —— 你把 Base64 字串用 JSON.stringify 包起來,接收端解析 JSON 後得到的字串不變,但你的伺服器程式碼卻誤以為需要再編一次。檢查傳輸線上的實際內容。
// 跨執行環境輔助函式(瀏覽器 + NODE + DENO + BUN)
// A portable Base64/base64url helper. Uses Buffer when available,
// falls back to btoa/atob + TextEncoder in the browser / Deno.
const hasBuffer = typeof Buffer !== 'undefined';
export function encode(str, { urlSafe = false } = {}) {
if (hasBuffer) {
return Buffer.from(str, 'utf8').toString(urlSafe ? 'base64url' : 'base64');
}
const bytes = new TextEncoder().encode(str);
let bin = '';
for (const b of bytes) bin += String.fromCharCode(b);
const b64 = btoa(bin);
return urlSafe
? b64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '')
: b64;
}
export function decode(b64, { urlSafe = false } = {}) {
if (hasBuffer) {
return Buffer.from(b64, urlSafe ? 'base64url' : 'base64').toString('utf8');
}
if (urlSafe) {
b64 = b64.replace(/-/g, '+').replace(/_/g, '/');
while (b64.length % 4) b64 += '=';
}
const bin = atob(b64);
const bytes = Uint8Array.from(bin, c => c.charCodeAt(0));
return new TextDecoder().decode(bytes);
}
// Usage
encode('héllo 世界'); // 'aMOpbGxvIOS4lueVjA=='
encode('héllo 世界', { urlSafe: true }); // 'aMOpbGxvIOS4lueVjA'
decode('aMOpbGxvIOS4lueVjA=='); // 'héllo 世界'
// 效能筆記
- > Buffer 最快 —— 在 Node 中它以 C++ 執行,可輕鬆處理 MB 等級的輸入而不會卡頓。
- > btoa/atob 在小資料上有競爭力(< 100 KB)——它們是同步的,在大型輸入上會卡住主執行緒。
-
>
瀏覽器中對 > 10 MB 的輸入,請用 Worker 讓編碼在主執行緒之外執行,或使用內部已優化的
Blob.text()+FileReader.readAsDataURL()。 - > 避免反覆的編碼/解碼週期 —— 每一次往返在時間與記憶體上都是 O(n)。若要多次使用,請把 Base64 形式快取起來。
-
>
串流 —— 對真正的巨量資料(100 MB+),請使用 Node 的
stream.Transform或瀏覽器的TransformStreamAPI,搭配分塊式的 Base64 編碼器。
// 延伸閱讀
• 什麼是 Base64?它是如何運作的? —— 從頭說清楚 6-bit 分組技巧。
• Base64 與 UTF-8、Unicode —— 深入探討 TextEncoder 的操作。
• URL 安全版與標準版 Base64 —— 何時需要 -_ 而不是 +/。
• 試用即時 Base64 編碼器 —— 貼上文字、查看編碼、複製結果。
若你經常要呼叫 btoa(),請把本頁加入書籤——十次中有九次,你正要 Google 的問題都在這裡。