问题
调用公司某 APP 客户端拍照 API,成功后返回给前端的结果是一段 Base64 编码后的字符串,但是后端接口要求我们使用 FormData
上传 Blob
这样的二进制文件。下面来一步步分析、解决这个问题,同时复习相关知识点。
Base64
Base64 是网络上最常见的用于传输 8 位单字节的编码方式之一,基于 64 个可打印字符来表示二进制数据。
既然是用于表示 8 位字节,对编码前的字符编码范围便限制在 8 位以内,即 [0, 255]
(十进制表示)或 [0x00, 0xFF]
(十六进制表示)。这个编码范围对应 ISO-8859-1 字符集,也就是熟知的 Latin1。Base64 的编码范围意味着不能对汉字直接进行 Base64 编码。
标准 Base64 编码只有 64 个字符(26*2 + 10 + 2
),即大小写英文字母、数字和 +
、/
,加上用于填充空位的 =
。Base64 编码后的数据,包括 =
在内,全部在 7 位 ASCII 字符集范围内。所以 Base64 的作用可以简单理解为使用 ASCII 表示 Latin1。
编码后的数据尺寸要大于编码前(约 4/3),因此在上传文件时,为了减少传输的数据量,应该尽可能使用 Blob
代替 Base64 字符串。
JavaScript 的常见宿主环境,如浏览器和 Node 一般都包含以下 Base64 相关的函数:
btoa()
: Base64 编码 (Binary to ASCII)atob()
: Base64 解码 (ASCII to Binary)
示例:
const encoded = btoa('Hello, world!'); // "SGVsbG8sIHdvcmxkIQ=="
const decoded = atob('SGVsbG8sIHdvcmxkIQ=='); // "Hello, world!"
如果试图传入超出 Latin1 字符集的字符,如 btoa('永')
,则会抛出异常 The string to be encoded contains characters outside of the Latin1 range
。
从 Base64 到 Blob
对于 Base64 图片字符串,调用 atob
后可以得到原始的二进制数据:
const base64 = 'iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==';
const binary = atob(base64);
PNG
���
IHDR���0���0���¥,ä´���gAMA��±üa���$PLTE���;;;666333444444333333444333333333!kÛÆ���tRNS�
9<cqxÖìóúFªX���NIDAT8Ëc`²Q�DÔìâl»w`ÈÞ½ÝphØiCÃäa«!ÆÕ84àÀinËq:w¸iDZŨqfÜg��'Ê>Þe[����IEND®B`
处理后的结果是一段包含乱码的字符串,这是因为原始数据是二进制图片,不应该作为文本展示。
我们可以通过 String.prototype.charCodeAt()
将字符串转换为由字符编码组成的无符号整型数组,成为真正的二进制数据。String.prototype.charCodeAt()
本身返回的整数范围是 [0, 65535]
或 [0x0000, 0xFFFF]
,理论上应该使用 Uint16Array
来存储数据(因为 JavaScript 中 String
类型使用的是 16 位码元,UTF-16 编码)。实际上呢,前面提到过 Base64 解码后的数据编码范围对应单字节的 Latin1,因此只需要使用同样是单字节的 8 位无符号整型数组 Uint8Array
进行存储。
function latin1ToUint8Array(str) {
const bytes = new Uint8Array(str.length);
for (let i = 0; i < bytes.length; i++) {
bytes[i] = str.charCodeAt(i);
}
return bytes;
}
const bytes = latin1ToUint8Array(binary);
// [137, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13, 73, 72, 68, 82, 0, 0, 0, 5,
// 0, 0, 0, 5, 8, 6, 0, 0, 0, 141, 111, 38, 229, 0, 0, 0, 28, 73, 68, 65, 84,
// 8, 215, 99, 248, 255, 255, 63, 195, 127, 6, 32, 5, 195, 32, 18, 132, 208,
// 49, 241, 130, 88, 205, 4, 0, 14, 245, 53, 203, 209, 142, 14, 31, 0, 0, 0,
// 0, 73, 69, 78, 68, 174, 66, 96, 130]
获取到 Uint8Array
后,可以很方便地创建 Blob
对象:
function base64ImageToBlob(base64) {
const binary = atob(base64);
const bytes = new Uint8Array(binary.length);
for (let i = 0; i < bytes.length; i++) {
bytes[i] = binary.charCodeAt(i);
}
return new Blob([bytes], { type: 'image/png' });
}
这里 Blob
的媒体类型硬编码成 image/png
,只针对上文 Base64 图片,实际需要根据二进制内容进行识别。例如 PNG 图片的开头数据,即“魔法数字(magic number)”应该是 [137, 80, 78, 71, 13, 10, 26, 10]
或 [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]
,上文 Base64 图片符合这一特征。其中 [80, 78, 71]
或 [0x50, 0x4e, 0x47]
对应的字符恰好为字符串 PNG
。
String.fromCharCode(80, 78, 71); // "PNG"
相关能力已经整合到 https://github.com/keqingrong/web-apis 库中,使用时可直接调用 base64ToBlob()
转换,该函数内部的 checkMime()
函数支持大部分已知图片类型。