
最近折腾 JavaScript 里的 Base64 编码,踩了几个坑,特别是中文字符串直接用 btoa() 直接报错的体验实在太酸爽。这篇把 Base64 的常见坑和实用模式说清楚。
Base64 在 Web 开发里无处不在——JWT 令牌、图片 Data URL、API 载荷、HTTP Basic Auth 头都靠它。但看似简单的 API 其实有不少弯弯绕绕。
Base64 用 64 个可打印字符(A-Z、a-z、0-9、+、/)把二进制数据转成 ASCII 文本。每 3 字节输入变成 4 字符输出,编码结果比原始数据大约大 33%。
这种编码存在是因为很多协议(邮件、HTTP、JSON)设计之初只考虑文本而非二进制。Base64 填补了这个空白。
btoa() 的陷阱原生的 btoa() 函数处理 ASCII 完全没问题:
btoa("Hello World"); // "SGVsbG8gV29ybGQ="
atob("SGVsbG8gV29ybGQ="); // "Hello World"
但只要编码任何包含非 Latin1 字符的内容试试:
btoa("你好世界");
// Uncaught DOMException: Failed to execute 'btoa' on 'Window':
// The string to be encoded contains characters outside of the Latin1 range.
处理国际化内容时这是必踩的坑。解决方案取决于你的 JavaScript 运行环境。
经典方案——先转义 Unicode 字符,再编码:
function encodeBase64(str) { return btoa(encodeURIComponent(str).replace( /%([0-9A-F]{2})/g, (_, p1) => String.fromCharCode('0x' + p1) ));
} function decodeBase64(b64) { return decodeURIComponent( atob(b64).split('').map( c => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2) ).join('') );
} encodeBase64("你好世界"); // "5L2g5aW977yM5LiW55WM"
decodeBase64("5L2g5aW977yM5LiW55WM"); // "你好世界"
更干净、更易读:
function encodeBase64Unicode(str) { const bytes = new TextEncoder().encode(str); let binary = ''; bytes.forEach(b => binary += String.fromCharCode(b)); return btoa(binary);
} function decodeBase64Unicode(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);
}
两种方法都可行。我更倾向 TextEncoder,可读性更好。但 encodeURIComponent 方案在老旧浏览器里兼容性更广。
把小图片内嵌成 Base64 Data URL 可以减少 HTTP 请求:
function fileToDataUrl(file) { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = () => resolve(reader.result); reader.onerror = reject; reader.readAsDataURL(file); });
} // Usage: preview an uploaded image
input.addEventListener('change', async (e) => { const dataUrl = await fileToDataUrl(e.target.files[0]); previewImg.src = dataUrl; // data:image/jpeg;base64,/9j/4AAQ...
});
适用场景:4KB 以下的小图标、一次性图形、需要完全自包含的 HTML 文件。
不适用场景:大图片。一张 2MB 的照片转成字符串后约 2.6MB,会阻塞渲染并撑爆内存。
Node.js 原生支持 UTF-8,用 Buffer 处理即可:
// Encode
const encoded = Buffer.from('你好世界', 'utf-8').toString('base64');
// "5L2g5aW977yM5LiW55WM" // Decode
const decoded = Buffer.from(encoded, 'base64').toString('utf-8');
// "你好世界"
比浏览器端简单太多了。如果在服务端做 Base64 相关操作,Node.js 很省心。
JWT 令牌用 Base64URL 而不是标准 Base64。区别如下:
| 标准 Base64 | Base64URL |
|---|---|
+ |
- |
/ |
_ |
= 填充 |
无填充 |
// Convert standard Base64 to Base64URL
function toBase64Url(b64) { return b64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
} // Convert back
function fromBase64Url(b64url) { let b64 = b64url.replace(/-/g, '+').replace(/_/g, '/'); while (b64.length % 4) b64 += '='; return b64;
}
如果你在解析或生成 JWT,这个转换搞错了会导致静默认证失败,调试起来非常头秃。
这点值得反复强调:Base64 是编码,不是加密。任何人都能解码它。永远不要用它来"保护"密码、API 密钥或其他敏感数据。这类场景请用真正的加密算法(AES、RSA 等)。
一个 10MB 的视频文件转成 Base64 后约 13MB。在浏览器里,这个字符串作为 JavaScript 字符串存储(UTF-16),实际内存占用可能翻倍到 26MB。多个大文件这样处理会直接崩溃标签页。
解决方案:大文件用 Blob 和 URL.createObjectURL() 处理,别用 Base64。
同步编码大文件会冻住整个 UI。把处理逻辑搬到 Web Worker 里:
// worker.js
self.onmessage = (e) => { const reader = new FileReaderSync(); const result = reader.readAsDataURL(e.data.file); self.postMessage(result);
}; // main.js
const worker = new Worker('worker.js');
worker.postMessage({ file: largeFile });
worker.onmessage = (e) => { console.log('Done:', e.data.substring(0, 50) + '...');
};
// Browser: ASCII only
btoa("Hello"); // "SGVsbG8="
atob("SGVsbG8="); // "Hello" // Browser: UTF-8 (use TextEncoder)
const bytes = new TextEncoder().encode("你好");
btoa(String.fromCharCode(...bytes)); // "5L2g5aW9" // Node.js: UTF-8 native
Buffer.from('你好', 'utf-8').toString('base64'); // "5L2g5aW9" // Base64URL (for JWT)
btoa(str).replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, ''); // File to Data URL
new FileReader().readAsDataURL(file); // HTTP Basic Auth header
'Basic ' + btoa('user:pass');
调试的时候经常需要快速编解码 Base64 字符串而不想写代码。找个能正确处理 UTF-8 的在线 Base64 编码解码工具会方便很多,同时显示标准版和 URL 安全版——这在对比 JWT 载荷或调试 API 响应时特别有用。
核心要点:Base64 概念简单,但坑不少(UTF-8、URL 安全变种、内存占用),花时间理解底层机制比只会复制粘贴 btoa() 强得多。