base64编码

255 阅读9分钟

为什么需要编码

前端我们基本上不用考虑编码,后端需要读取数据操作文件,则需要用到编码,前端早期是不能操作文件的,也就是说不能操作二进制数据的(现在也可以了,不过写入文件的话,浏览器的兼容性不太行),所以 node 就实现了一个 Buffer 用于描述内存的。

使用十六进制的 bfffer

内存中存储的数据都是二进制的,二进制的特点就是 0 和 1 组成的特别长的数字,node 为了让它变得短一点,于是产生了 16 进制的 buffer。

node 中只支持 utf8 编码(单个字符除外,使用 ascii),在此编码的规范中,规定一个汉字三个字节,一个字节由八个位(二进制)组成,八个二进制位表示的最大值为 11111111,也就是 255,如果换算成 16 进制,就是一个字节由 2 个 16 进制位组成,最大为 ff。

// 10进制的 255 转 16 进制 
255..toString(16); // ff

进制之间的转换

其他进制转十进制(前端的整数就是十进制),按权展开求和

计算 二进制 101 转十进制的结果。
//  1 * 2**2 + 0 * 2**1 + 1 * 2**0  = 5
所以 parseInt(101, 2); 结果是:5

整数转其他进制,需要不停的取余数,倒序相加 参考:整数转二进制

我们来归纳一下,其实不管是十进制转其他进制,还是其他进制转十进制,都可以使用下面两种更简单的方式来实现。

parseInt(101, 2); // 二进制的 101,转十进制
10..toString(2); // 十进制的 10,转二进制,俩点表示 10 的小数点后面是没有值的

小数十进制转二进制

乘2取整,顺序排列

// 0.1 转 二进制
// 0.1 * 2 = 0.2  ->  0
// 0.2 * 2 = 0.4  ->  0
// 0.4 * 2 = 0.8  ->  0
// 0.8 * 2 = 1.6  ->  1
// 0.6 * 2 = 1.2  ->  1
// 0.2 * 2 = 0.4  ->  0
// ...

// 所以得到的结果为: 0.000110...

0.1 + 0.2 为什么不准确

浮点数在计算机底层存储的时候,十进制的 0.1 转化为存储的二进制值,可能被舍掉一部分「因为最多只有 64 位」,所以本身和原来的十进制就不一样了,这是所有编程语言都存在的问题,因为浮点数在后端语言中,也是按二进制进行存储的。

而 0.1 转二进制就被截取了一部分, 所以计算机底层进行的运算 0.1 + 0.2 本身就是不准确的,最后转成浏览器能识别的十进制,这样也可能是一个不准确的很长的值,例如可能是这样的 0.300000000000000040000… 但是浏览器也会存在长度的限制(小数点后17位),会截掉一部分,最后面全是 0 的省略掉,这也是导致不准确的一个原因。

思考 0.2 + 0.2

为什么 0.2 + 0.2 是准确的呢?

常用的编码(语言层面)

一个字节包含8个位(二进制位数),也就是说,一个字节能表示的最大值为二进制的 11111111,这个是十进制的 255,也就是能表示 255 个数。

  1. ascii 编码,最早的编码,美国人发明的,一个字节就能全部表示完英文 + 字符了,而且人家只用了 127 个。
  2. gbk 编码:但是对于汉字来说,是明显不够用的,所以我们中国人就发明了自己的编码,最早叫 gb18030,特点就是一个汉字使用两个字节,最早是能表示 127 * 127个数(没占满整个字节),后面基于 gb18030 进行了扩展,才出现了 gbk(国标编码)。
  3. unicode 编码:其他国家也有自己的编码规则,这时候,就需要有一个统一的编码规则,所以 unicode 组织诞生了,使用多个字节进行编码,但是遗憾的是这个组织并没有把 unicode 发展起来。
  4. utf8 编码: 它借用了 unicode 的编码风格,实现了自己的一套机制,包括 、utf8、utf16、utf32等。

node 中主要使用了 utf8 编码,但是单个字符还是使用的 ascii。

base64 编码规范(需求层面)

base64 是一种编码规范,常用于开发中替换掉路径,前后端中文传输等。

我们知道,utf8 编码是一个汉字代表 3 个字节,一个字节代表 8 个二进制位,那就是说一个汉字代表 24 个二进制,base64 指的是,每个字节所有的二进制转 10 进制,不能超过 64,也可以理解成 base64 指的是每个字节都能用 64 个元素的映射表表示,不允许超过。

把 3 * 8 的结构,转成 4 * 6 的格式,3 字节转 4 字节导致的后果是,base64 编码后的结果,都会比之前大三分之一。

因为二进制的 6 个 1 最大值为 63,刚好小于 64,所以最多拆成 3 * 8 / 6 = 4 个字节, 同理转 base32 的编码,每个字节最大容量为 5 个 1,也就是 31,所以每个汉字包含 3 * 8 / 5 = 5 个字节。

所以说,把图片全部转为 base64 用来节省网络资源,这个说法本身没毛病,但是都转 base64,可能图片占用的本地内存资源从 1m 到 1.3m,占用的资源空间更大了,我们一般只处理比较小的图片或者 icon 转 base64 就行。

base64 url 格式

那么,我们经常看到的图片转 base64 编码,称为 data url scheme,格式为

// @1 { mimeType } 是指图片的类型,JPG 文件就是 image/jpeg,PNG 文件就是 image/png。
// @2 { code } 是指图片二进制转换成 base64 的字符串。
data:{ mimeType };base64,{ code }

Data URI scheme是在RFC2397中定义的,目的是将一些小的数据,直接嵌入到网页中,从而不用再从外部文件载入。

目前,IE8、Firfox、Chrome、Opera浏览器都支持这种小文件嵌入。 目前,Data URI scheme支持的类型有:

data:,文本数据

data:text/plain,文本数据

data:text/html,HTML代码

data:text/html;base64,base64编码的HTML代码

data:text/css,CSS代码

data:text/css;base64,base64编码的CSS代码

data:text/javascript,Javascript代码

data:text/javascript;base64,base64编码的Javascript代码

data:image/gif;base64,base64编码的gif图片数据

data:image/png;base64,base64编码的png图片数据

data:image/jpeg;base64,base64编码的jpeg图片数据

data:image/x-icon;base64,base64编码的icon图片数据

中文转 base64 示例

// 完整步骤
//    @1 通过 buffer.from 获取三个 16 进制的字节(utf8 中一个汉字代表 3 个字节)
//    @2 通过三个 16 进制字节获取底层存储的 24 位二进制编码的结果
//    @3 二进制数据按字节数重新分组,每个字节表示的最大值不能大于 64(六个 1),高位补 0
//    @4 新二进制转成浏览器能识别的 10 进制(base64 映射表的 key 是十进制数字)
//    @5 从 base64 映射表(A-Z,a-z,0-9,+/)取出结果拼接

// buffer 拿到的结果是 16 进制的
let r = Buffer.from('杨');

// 16进制的三个字节
console.log(r); // e6 9d a8

// 16 进制(0x 开头)转 2 进制字节
console.log((0xe6).toString(2));
console.log((0x9d).toString(2));
console.log((0xa8).toString(2));
// 11100110 10011101 10101000

// 拆成四个字节,不够八位的高位补 0,得到二进制的 base64 编码
// 111001 101001 110110 101000  => 补 0
// 00111001 00101001 00110110 00101000 

// 转换的值最大不超过 64,00111111 => 63
// 二进制转 10 进制 
console.log(parseInt('00111001', 2));
console.log(parseInt('00101001', 2));
console.log(parseInt('00110110', 2));
console.log(parseInt('00101000', 2));
// 57 41 54 40

// 创建 base64 对照表
let base64Map = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; // 26个大写字母
base64Map += base64Map.toLowerCase(); // 52个大小写
base64Map += '0123456789+/'; // 64个大小写+数字+运算符(规定的,只有 + 和 /)

console.log(base64Map[57] + base64Map[41] + base64Map[54] + base64Map[40]); // 5p2o

那么我们认为 '5p2o' 就是 '杨' 的 base64 编码结果,这就是 r.toString('base64') 的执行过程。

对照 base64 在线编码解码

中文转 base32(低位补0,等号填充)

base32 是一个汉字五个字符,所以整体是之前的 5/3 倍。

// 转 base32 完整步骤
//    @1 通过 buffer.from 获取三个 16 进制的字节(utf8 中一个汉字代表 3 个字节)
//    @2 通过三个 16 进制字节获取底层存储的 24 位二进制编码的结果
//    @3 二进制数据按字节数重新分组,每个字节表示的最大值不能大于 32(也就是 5 个 1),那么只
//       能把 24 个二进制位分为 5 组,这样就能保整每组最大不超过 5 个 1(32),低位补 0,最
//       后不足 5 * 8 位的填 '=' (一个等号代表五个字符)
//    @4 新二进制转成浏览器能识别的 10 进制(base32 映射表的 key 是十进制数字)
//    @5 从 base32 映射表(A-Z, 2-7)取出结果拼接


// buffer 拿到的结果是 16 进制的
let r = Buffer.from('杨');

// 16进制的三个字节
console.log(r); // e6 9d a8

// 16 进制(0x 开头)转 2 进制字节
console.log((0xe6).toString(2));
console.log((0x9d).toString(2));
console.log((0xa8).toString(2));
// 11100110 10011101 10101000

// 拆成 5 个字节,不够五位的低位补 0,最后不足 5 * 8 位补 = 号(一个等号代表五个二进制位)
// 得到二进制的 base32 编码
// 11100 11010 01110 11010 1000  => 低位补 0
// 11100 11010 01110 11010 10000 => 差 15 位,最后编码结果加上三个等号

// 二进制转 10 进制 
console.log(parseInt('11100', 2));
console.log(parseInt('11010', 2));
console.log(parseInt('01110', 2));
console.log(parseInt('11010', 2));
console.log(parseInt('10000', 2));
// 28 26 14 26 16

// 创建 base32 对照表
let base32Map = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; // 26个大写字母
base32Map += '234567'; // 32 个字母大小写+数字(2-7)

console.log(base32Map[28] + base32Map[26] + base32Map[14] + base32Map[26] + base32Map[16] + '==='); // 42O2Q===

对照 base64 在线编码解码