尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Base64 遇上中文和 emoji:btoa 报错、解出一串 %E7、URL 里加号变空格,六个坑一次说清

Base64 遇上中文和 emoji:btoa 报错、解出一串 %E7、URL 里加号变空格,六个坑一次说清 前端处理 Base64英文字母怎么编都没事一碰中文和 emoji 就开始出状况要么btoa直接抛异常要么编出来的串后端解不开要么解回来是一堆ç§æ。这些问题的根子是同一个Base64 编的是字节不是字符。JS 字符串是 UTF-16而btoa/atob这对老 API 只认「每个字符一个字节」的 Latin-1 字符串。中间差的那一步 UTF-8 转换谁来做、在哪做决定了结果对不对。下面六个坑每个都附了能直接跑的代码和实际输出Node 25.8 实测浏览器里行为一致。坑 1btoa 直接吃中文抛 InvalidCharacterErrorconsts秋招 offer ;btoa(s);// InvalidCharacterErrorbtoa要求每个字符的码点都在 0–255 之间。「秋」是 U79CB超了直接抛错。这个坑至少会报错算友好的。坑 2用 encodeURIComponent「修」好了其实编的是另一个东西网上流传很广的一种写法constwrongbtoa(encodeURIComponent(s));console.log(wrong);// JUU3JUE3JThCJUU2JThCJTlCJTIwb2ZmZXIlMjAlRjAlOUYlOEUlODk 56 个字符不报错了前端自己再decodeURIComponent(atob(...))也能还原看起来一切正常。但把这个串交给后端Go、Java、Python 任何一种按标准 Base64 UTF-8 解码的语言拿到的是%E7%A7%8B%E6%8B%9B%20offer%20%F0%9F%8E%89它编码的是「百分号转义后的 ASCII 文本」不是原文的 UTF-8 字节。结果只有自己的前端认识而且体积还大了一截正确的编码只有 24 个字符见下面这里是 56 个。老代码里还有一个变体btoa(unescape(encodeURIComponent(s)))多了个unescape把%E7这种转义还原成单字节字符结果其实是对的。只是unescape/escape早就被标记为废弃新代码没必要再用。坑 3正确做法是先拿 UTF-8 字节functionencodeUtf8(str){constbytesnewTextEncoder().encode(str);// UTF-8 字节letbin;for(leti0;ibytes.length;i)binString.fromCharCode(bytes[i]);returnbtoa(bin);}functiondecodeUtf8(b64){constbinatob(b64);constbytesUint8Array.from(bin,cc.charCodeAt(0));returnnewTextDecoder().decode(bytes);}constokencodeUtf8(秋招 offer );console.log(ok);// 56eL5oubIG9mZmVyIPCfjokconsole.log(okBuffer.from(秋招 offer ).toString(base64));// trueconsole.log(decodeUtf8(ok));// 秋招 offer 思路就是把「字符 → 字节」这一步显式交给TextEncoderbtoa只负责「字节 → Base64」。和 Node 的Buffer结果一致意味着和各语言后端也一致。emoji 是 4 字节的 UTF-8同样没问题。坑 4解码忘了 TextDecoder得到一串 ç§æconsole.log(atob(56eL5oubIG9mZmVyIPCfjok));// ç§æ offer ð...atob返回的是「每个字符代表一个字节」的二进制字符串不是文本。直接显示就是把 UTF-8 字节当 Latin-1 读典型的乱码。看到ç、æ、ð开头的乱码基本可以断定是少了TextDecoder那一步。坑 5URL-safe Base64 和 URL 里的加号标准 Base64 字母表里有和/放进 URL 会出事conststdencodeUtf8(面试);// 6Z2i6KVnewURLSearchParams(tstd).get(t);// 6Z2i6K V 加号被当成了空格所以 JWT、很多签名参数用的是 URL-safe 变体换成-/换成_通常还去掉末尾的。问题是atob不认这套字母表consturlsafe6Z2i6K-V;atob(urlsafe);// InvalidCharacterErrorfunctionfromUrlSafe(b){bb.replace(/-/g,).replace(/_/g,/);while(b.length%4)b;returnb;}decodeUtf8(fromUrlSafe(urlsafe));// 面试排查时有个很快的判断串里出现-或_就是 URL-safe出现空格多半是某个环节把当成了空格要回头查是谁没做 URL 编码。顺带一提缺 padding 本身不一定报错atob(5bCPPw)在 Node 和 Chrome 里都能正常解出「小?」规范允许省略末尾的。真正报错的是字母表不对别把两件事混在一起。坑 6大文件 String.fromCharCode(…bytes) 爆栈处理图片、文件时很多人会图省事这样写constbignewUint8Array(1_000_000);String.fromCharCode(...big);// RangeError: Maximum call stack size exceeded展开运算符会把一百万个字节当成一百万个函数参数直接超出调用栈。改成分块functionbytesToB64(bytes){letbin;constCHUNK0x8000;for(leti0;ibytes.length;iCHUNK){binString.fromCharCode.apply(null,bytes.subarray(i,iCHUNK));}returnbtoa(bin);}console.log(bytesToB64(big).length);// 13333361MB 编完是 1333336 个字符符合 Base64「4/3 倍」的体积膨胀。如果只是要在页面上显示图片其实没必要转 Base64URL.createObjectURL(blob)更省内存。新 APIUint8Array.toBase64 / fromBase64较新的运行时已经有原生方法了上面那些手写的转换都可以省掉constbytesnewTextEncoder().encode(秋招 offer );bytes.toBase64();// 56eL5oubIG9mZmVyIPCfjoknewTextEncoder().encode(面试).toBase64({alphabet:base64url,omitPadding:true});// 6Z2i6K-VnewTextDecoder().decode(Uint8Array.fromBase64(6Z2i6K-V,{alphabet:base64url}));// 面试URL-safe 和 padding 都是参数不用自己替换字符。我在 Node 25.8 上跑通了。不过要兼容旧浏览器的项目暂时还得保留坑 3 那套写法做兜底上线前先查一下你的目标浏览器支不支持。小结一下排查顺序报InvalidCharacterError看是编码时有非 Latin-1 字符坑 1还是解码时混进了-、_、空格坑 5。解出来是%E7%A7...编码端用了btoa(encodeURIComponent())坑 2。解出来是ç§æ解码端少了TextDecoder坑 4。大文件卡死或爆栈别用展开运算符坑 6。平时临时核对一个串对不对我会直接丢到 福兮的 Base64 编解码工具forxi.cn里看一眼中文和 emoji 编出来的结果和上面TextEncoder的写法一致拿来和后端对数很方便。它按标准 Base64 处理遇到 URL-safe 的串要先把-、_换回、/再贴进去这点和atob一样。
返回列表