最近项目需要用到 Base64, 想到npm中找个好用的, 很可惜没一个符合我的预期, 于自己捣鼓一个, 感觉还不错所以分享出来;
其实我的需求其实很简单:
- 浏览器可用; (利用 Buffer 的方法出局);
- 支持字符串 (现有的库都支持, 只有
btoa
,atob
只支持 Latin1) - javascript 字符串无损转换 (因为这一点, 现有库全军覆没), 稍后做说明;
- 能用上
Tree-shaking
, 因为项目一般只用了Base64的一半功能(encode
或decode
), 我可不想copy代码;
为什么不能做到javascript字符串无损转换
看一个例子:
var s = '\ud800';
var b64 = Buffer.from(s).toString('base64');
var _s = Buffer.from(b64, 'base64').toString();
console.log(s == _s); //false
为什么最后是 false
?
首先要知道, 字符串编码为base64之前要先转成字节数组, 字节数组再进行 base64 编码, 解码反之, 我们一般都用’utf8’编码字符串;
再看 U+d800
是一个空码, 从 utf16
来看, 是一个4字节字符的一半, 然而javascript是ucs2编码, 所以它在javascript中是一个正常的字符串;
Buffer
默认也是以’utf8’编码字符串 , 所以过程是这样 字符串>Unicode>utf8
, 字符串 到 Unicode 过和程对于空码, 替换成了一个占位符 ‘�’, 之后的转换过程就跟着错了, 还原回来当然就错了;
反正就认准一点,我一个字符串 出了个门回来就变了 这是不可以的.
怎么解决:
方案1:
直接用ucs2
编码; 省去 字符串 到 Unicode 的过程, 自然不会出问题了;
var s = '\ud800';
var b64 = Buffer.from(s,'ucs2').toString('base64');
var _s = Buffer.from(b64, 'base64').toString('ucs2');
console.log(s == _s); //true
注意: 上面代码里的 ‘ucs2’ 其实是 ‘utf16le’ 的别名. 但有一个问题, 单字符串中 英文居多时 , 编码后的Base64会比用 “utf8” 的情况长很多;
方案2:
字符串>Unicode>utf8
的过程不把空码替换, 解码也一样, 这样也可以保证字符串的一至性;
我写的这个Base64库用的也是这个方案;
其它
除了上面说的5点, 我还做了一些顺手的功能;
- 抽象出 Base64 算法, 支持自定义的 编码表 和 字符串编码方式, 适应更多特殊场景;
- 支持字节数组 (既然在都有了 ArrayBuffer/Uint8Array 类型, 为什么不顺便支持一下, 其实比支持字符串更简单吧) , 其实我的项目也没用到;
GitHub: cnwhy/Base64.js
详细的使用方法可参看这篇
javascript是ucs2编码是什么意思
@strugglexiang ucs2 可以看成是 utf16 的子集
浏览器不支持 buffer ?
@waitingsong 浏览器是 ArrayBuffer 我说的是 Nodejs的 Buffer 对象 , 它虽然继承自Uint8Array ,但是并不适合在浏览器中实现
其它的解决方案: https://developer.mozilla.org/zh-CN/docs/Web/API/WindowBase64/Base64_encoding_and_decoding 不知道满足 3.javascript 字符串无损转换 不?
@waitingsong Solution #2 – JavaScript's UTF-16 => UTF-8 => base64
这不失为一个很好的解决方案, 不考虑4字节字符的事全部, 我的方案1是 ucs2 > base64
它是 ucs2 > UTF-8 > base64
, 解决英文字符会占两位题, 不过, 当遇到 4 字节安符时, 会看成两个字符进行UTF8编码,所以会占到6字节, 如果不拆转的话 一般是4个字节;
@DerekYeung 他这个方法是利用 Buffer
或btoa'; 实测编码
"\ud800"` 在node环境中肯定被替换, 在浏览器环境报错了, 没深究;
@cnwhy utf8.ts 转换方法似乎可以用 str2Uint8Array() 这个函数来简化。 浏览器和 Node.js 通用。
反正就认准一点,我一个字符串 出了个门回来就变了 这是不可以的.
我认为所谓完全保持原样转换的思路是不正确的:
\ud800
单独这个码点不是个合法的字符(需要和其它码点配套使用)。各个开发语言在处理这种不合法的 UTF-8 码点时一会都会用 EF BF BD
这个备胎来替换,表示为异常值
console.log(Buffer.from('\ud800')) // <Buffer ef bf bd>
console.log(Buffer.from('\ud801')) // <Buffer ef bf bd>
console.log(Buffer.from('\ud802')) // <Buffer ef bf bd>
如果在转换、传输中对于非法字符(码点)不处理为 EF BF BD
而是“原样”传输,那么就可能会产生多字节字符(拼接)攻击漏洞。这是我们所不希望发生的。
对于我来说:
反正就认准一点,首先一切非法的字符统统干掉,不管是外面进来的还是内部产出的。
@waitingsong 如果认定你认定 '\u800'
是非法字符 那就别管我瞎叨叨了; 用你喜欢的方式就好.
@cnwhy 那看来我们的应用场景不同。不同需求自然有不同实现。
- 我的应用这对于 单个
\uD800
之类代理码点,或者无对应 Unicode 字符的代理组合码点,统统认为是非法的、无效的。 - 你的场景需要原样转换、传输。
基于他人成果包装出一个轮子,适用于通用场景:
- Base64 编码、解码
- URL-safe base64 编码、解码
- 支持长青浏览器以及 Node.js
- cjs, esm, umd 多种格式编译
- 丰富的测试用例
- TypeScript 开发,静态类型提高工作效率,减少错误
项目: https://github.com/waitingsong/base64 文档: https://waitingsong.github.io/base64/index.html NPM: https://www.npmjs.com/package/@waiting/base64
欢迎各位食用 (=・ω・=),
最近优化了UTF8转码函数, 效率有所提高 , 但在 node 环境下 和 TestEncode
还是有不小的差距, node下的 TestEncode
是C写的吧. @waitingsong
@cnwhy 应该是C++吧。 我这儿测试 nodejs 通过 Buffer 转换 base64 的方式比 textEncoder 方式性能更高(粗略估计快一倍以上) 。
确实好用 请问 如果自定义base64的编码表, 这样是不是相当于加密了 容易破解么?
@csc860 只是比普通base64好一点, 因为算法是一样的, 只要别人只要拿到编码表 分分钟破解. 但很多系统还是喜欢这样用.
@csc860 广义上说 base64 也算加密算法,只是算法公开。你自定义码表只是些许提高了点破解难度。