我报名参加金石计划1期挑战——瓜分10万奖池,这是我的第1篇文章,点击查看活动详情
Intro
作为前端开发者,我们在很多场景中会使用到 Base64,但很惭愧,笔者对其原理并不熟悉,导致在遇到实际问题时耗费了很多时间去 📌 定位问题。这篇文章,将对 Base64 进行刨根问底,一探究竟,下次就不再陌生了。
这是又一篇和编码知识相关的内容,关联之前写过的几篇相关文章:
- 硬核基础二进制篇(一)0.1 + 0.2 != 0.3 和 IEEE-754 标准
- 硬核基础二进制篇(二)位运算
- 硬核基础编码篇(一)烫烫烫烫烫烫
- 硬核基础编码篇(二)ascii、unicode 和 utf-8
- 淦,为什么 "𠮷𠮷𠮷".length !== 3
前端场景 🥷
在笔者过往的工作经历中,大概有以下几种场景会用到 Base64:
- 将小图处理成
Base64,以 Data URLs 的形式传入img标签的src或background-image,这样可以减少 HTTP 请求次数; - 通过 Canvas 实现图片压缩/裁切/绘制,最终调用
canvas.toDataURL()方法,将图片传给客户端或后端进行保存; - 对二进制文件做 Hash 算法 (例如 SHA-1,SHA256)生成 checksum,最后使用 Base64 encode 使得摘要可打印,方便判断是否一致;
- ...
如果仔细看,其实这几种使用场景做的事情都是:将二进制 bytes 通过新的编码系统编码成可读文本,方便以文本形式表示、传输、存储内容。
那做 Base64 的必要性是没什么,直接输出二进制不行吗?答案当然是不行了,例如👆的第一个例子,HTML 是以文本形式传输的,而图片是二进制文件(一堆 0 和 1),强行将图片按 utf-8 编码输出,保不齐会产生 IMG 结束标签(例如 "/>),导致 HTML 文本解析异常。
在文本协议中传输二进制终归是需要编码的,Base64 就是其中一种比较常见通用的编码方案。下面继续看它的编码逻辑。
编码逻辑 ❓
首先,Base64 有自己一套字符集,包括 A-Z, a-z, 0-9, +, / 和 =,总共 65 个字符。除了作为填充字符的 = 之外,每个字符都有自己的一个 Index。(注意和 ASCII 的 charCode 不是一回事)
这些字符全都是常见的可打印字符,Base64 将使用这 65 个字符来表示任意二进制内容。它的规则是以 6 个 bit 为一个单元,每 4 个单元为一组。
可是一个字节最小都有 8 个 bit,只用 6 个 bit 是如何表示的呢?直接看 wiki百科的实例,对字符串 Man 做 base64encode。
每个字符占用 8-bit,三个字符总共 24-bit,刚好划分为 6-bit * 4 ,按每 6-bit 为一个单元分别编码得到 Index,再根据字典表映射到 base64 中的字母。就得到 Man 做 base64 之后的结果 TWFu。
那如果不是刚好 24-bit 呢?比如只有两个字符 Ma 是怎么编码的?
很简单,如果 6-bit 不是完整的,用 0 补位(例如第三个单元只有 0001,末尾补充两个 0);如果 6-bit 是空的用填充字符 = 占位(例如第四个单元)。所以 Ma 经过 base64 编码后是 TWE=
A和=表示的含义不一样,A是 6-bit 全为 0,=是这个单元是填充补齐的。
这就是 Base64 的编码逻辑,根据这个规则,我们可以得出一些结论:
- 编码后内容占用体积会增加,至少是原来的 4/3 倍
- 编码后内容都满足 6-bit 为一个单元,每 4 个单元为一组的这个规律(Base64 的 64 就是这么来的)
- 编码后文本至多仅包含 2 个
=号,且仅会在结尾出现。
如何使用 ✂️
Node
Node 中的 buffer 模块是专门用来弥补 JS 对二进制操作的空缺的,其原生支持了 base64 的的编码解码。
Buffer.from('Man').toString('base64')
// TWFu
Buffer.from('TWFu', 'base64').toString()
// Man
浏览器
浏览器环境中也提供了 base64 编码解码的方法,名字叫 atob (ASCII to Binary),btoa(Binary to ASCII)。
btoa('Man')
// TWFu
atob('TWFu')
// Man
遗憾的是这俩方法仅支持 (U+0080) - FF (U+00FF) 这个范围内的字符编/解码,如果不在这个范围内,就会报错。
Uncaught DOMException: Failed to execute 'btoa' on 'Window': The string to be encoded contains characters outside of the Latin1 range.
要满足日常工作中对众多 Unicode 字符编解码的需求,可以使用 webtoolkit 提供的方法。
Linux
linux 中也内置了 base64 命令,可以使用管道符号 | 将需要编码解码的内容传递给它。
$ echo -n "Man" | base64
TWFu
$ echo -n "TWFu" | base64 -d
Man
Ending
可以看到 Base64 无论是应用还是其自身的编码逻辑都非常简单易懂,看完这篇文章希望你也对 Base64 不再陌生(记住 6-bit,4 个为一组就可以🌶🌶🌶)