从日常需求聊起
前端中后台系统开发过程中,经常会遇到 Excel 表格、图片等文件处理的需求。公司基建如果做得好,那么处理 Excel 下载的需求,你可能仅仅只需要调用一个 download 的方法,返回一个 Blob 文件流即可。又或者在处理某些图片问题时,作为新人的你往往手足无措,img 标签中使用 src 引入图片时出现各种问题。如果你有丰富的知识储备,或者有老人带教,那么就知道,图片除了纯 URL 指定资源地址之外,还可以使用 Base64 等加密字符处理,优点就是图片资源直接使用,而 URL 可能出现加载失败或者异步导致的数据填充失败问题。而 Base64 是直接使用图片转换后的字符串,不存在上述情况。特别是当你存在打印需求时,需要使用字符串模板,并且要在字符串模板中显示图片资源,这时直接使用 Base64 显示图片会安全、稳定得多。
下面,就针对这几种数据类型进行简要讨论。
Blob:二进制长文件
MDN:
Blob对象表示一个不可变、原始数据(二进制数据)的类文件对象。它的数据可以按文本或二进制的格式进行读取,也可以转换成ReadableStream来用于数据操作。Blob 表示的不一定是 JavaScript 原生格式的数据。File接口基于Blob,继承了 blob 的功能并将其扩展以支持用户系统上的文件。
关键词:不可变、类文件、文本或二进制。
多余而又复杂的概念就不扯了,简单来说你工作中涉及到文件处理的需求中很大概率会用到它。Blob 对象大概就长这样:
size 表示数据的大小,单位是字节,type 是 MIME 类型的字符串。原型上有 slice,stream 等方法:
- slice(start, end, contentType):返回一个新的
Blob对象,包含了源Blob对象中指定范围内的数据; - stream():返回一个能读取 blob 内容的
ReadableStream; - text():返回一个 Promise 对象且包含 blob 所有内容的 UTF-8 格式的
USVString; - arrayBuffer():返回一个 Promise 对象且包含
Blob所有内容的二进制格式的ArrayBuffer。
Ajax 请求响应 Blob 类型
// 原生 Ajax
const downloadBlob = (url, callback) => {
const xhr = new XMLHttpRequest()
xhr.open('GET', url)
xhr.responseType = 'blob'
xhr.onload = () => {
callback(xhr.response)
}
xhr.send(null)
}
// Axios
axios({
method:'...',
url:'...',
// `responseType` 表示服务器响应的数据类型,可以是 'arraybuffer', 'blob', 'document', 'json', 'text', 'stream'
responseType: 'blob'
}).then(res => {
const {headers, data} = res;
filename = decodeURI(headers["content-disposition"].split("filename=")[1]);
blob = new Blob([data], {type: headers["content-type"]});
});
File 对象与 Blob 的关系
File
File 对象代表一个文件,用来读写文件信息。它继承了 Blob 对象,或者说是一种特殊的 Blob 对象,所有可以使用 Blob 对象的场合都可以使用它。
浏览器原生提供一个File()构造函数,用来生成 File 实例对象。
new File(array, name, [options])
- array:一个数组,成员可以是二进制对象或字符串,表示文件的内容;
- name:字符串,表示文件名或文件路径;
- options:配置对象,设置实例的属性。该参数可选。
FileList 对象
FileList 对象是一个类似数组的对象,代表一组选中的文件,每个成员都是一个 File 实例。我们常用的组件库中的 Upload 组件就是基于 <input type="file"> 封装的。
<input type="file">
const files = document.getElementById('input').files;
files instanceof FileList // true
FileReader
FileReader 对象用于读取 File 对象或 Blob 对象所包含的文件内容。
浏览器原生提供一个 FileReader 构造函数,用来生成 FileReader 实例。
const reader = new FileReader();
FileReader 实例属性:
- FileReader.error:读取文件时产生的错误对象;
- FileReader.readyState:整数,表示读取文件时的当前状态。一共有三种可能的状态,
0表示尚未加载任何数据,1表示数据正在加载,2表示加载完成; - FileReader.result:读取完成后的文件内容,有可能是字符串,也可能是一个
ArrayBuffer实例; - FileReader.onabort:
abort事件(用户终止读取操作)的监听函数; - FileReader.onerror:
error事件(读取错误)的监听函数; - FileReader.onload:
load事件(读取操作完成)的监听函数,通常在这个函数里面使用result属性,拿到文件内容; - FileReader.onloadstart:
loadstart事件(读取操作开始)的监听函数; - FileReader.onloadend:
loadend事件(读取操作结束)的监听函数; - FileReader.onprogress:
progress事件(读取操作进行中)的监听函数。
提到 File,FileReader 主要在于 FileReader 能够读取 Blob 对象内容,还可以将其转换为 ArrayBuffer 格式,利于后期各类数据类型间的互转。
js 代码创建 Blob 对象
const strArray = ['123456789'];
const blob = new Blob(strArray);
在 js 中,允许我们通过构造函数 Blob 来创建 Blob 对象。从代码中可以看到我们传递了一个字符串数组作为它的第一个参数。
MDN: const blob = new Blob(array, options); 参数
array是一个由ArrayBuffer,ArrayBufferView,Blob,DOMString等对象构成的Array,或者其他类似对象的混合体,它将会被放进Blob。DOMStrings 会被编码为 UTF-8。参数options是一个可选的BlobPropertyBag字典,它可能会指定如下两个属性:type,默认值为"",它代表了将会被放入到 blob 中的数组内容的 MIME 类型。endings,默认值为"transparent",用于指定包含行结束符\n的字符串如何被写入。 它是以下两个值中的一个:"native",代表行结束符会被更改为适合宿主操作系统文件系统的换行符,或者"transparent",代表会保持 blob 中保存的结束符不变
总之,我们可以知道 Blob 是可以利用代码生成的,同时,它的第一个参数是一个 array 。并且 ArrayBuffer 是 Blob 参数的一种。那么由此,我们引出 ArrayBuffer。
ArrayBuffer:通用定长的原始二进制数据缓冲区
MDN:
ArrayBuffer对象用来表示通用的、固定长度的原始二进制数据缓冲区。它是一个字节数组,通常在其他语言中称为“byte array”。你不能直接操作ArrayBuffer的内容,而是要通过类型数组对象或DataView对象来操作,它们会将缓冲区中的数据表示为特定的格式,并通过这些格式来读写缓冲区的内容。
同样的,使用构造函数看创建 ArrayBuffer 对象:
new ArrayBuffer(length)
其中,参数 length 表示要创建的 buffer 对象的长度,由于 ArrayBuffer 的特点是定长,这个参数不可省略。length 在正负 2**53 范围内。
可以看到,ArrayBuffer 原型上有一些类似 Uint8Array 的对象,实际上,ArrayBuffer 是不能直接被操作的,需要通过类型数组对象 TypedArray 或者 DataView 对象来操作,它们能将缓冲区中的数据表示为特定的格式,并通过这些格式来读写缓冲区内容。
TypedArray 格式类型:
Int8Array();
Uint8Array();
Uint8ClampedArray();
Int16Array();
Uint16Array();
Int32Array();
Uint32Array();
Float32Array();
Float64Array();
DataView 则是指自定义的解释器,暂且不管它。拿 Uint8Array 举个例子,Uint8Array 表示把 ArrayBuffer 的每个字节(byte,8-bit)当成一个无符号整型数,值的范围是 0-255。有什么用呢?我们可以通过 String.fromCharCode 将这些数值对应的 Unicode 编码拼接成数据字符串。进而使用这个字符串再进行 Base64 编码,处理我们的图片数据。这也正是我们开头说的,确实存在实际需求会用到这类数据类型。
const arr = new Uint8Array([12, 27, 38, 99, 122]);
console.log(arr.length); // 5
console.log(arr[1]); // 27
Base64:二进制编码字符串
MDN:
Base64是一组相似的二进制到文本(binary-to-text)的编码规则,使得二进制数据在解释成 radix-64 的表现形式后能够用 ASCII 字符串的格式表示出来。Base64这个词出自一种 MIME 数据传输编码
同样的,Base64 也有对应的方法进行创建。但是,Base64 实际上是一种编码方式,算不上数据类型,存在编码,就存在解码。分别是:window.atob(string),window.btoa(string)。
window.atob() 函数能够解码通过 Base64 编码的字符串数据。相反地,window.btoa() 函数能够从二进制数据“字符串”创建一个 Base64 编码的 ASCII 字符串。
前面讲到 ArrayBuffer 时,由 Uint8Array 创建的二进制数据字符串正好可以作为 btoa() 的参数从而编码成 Base64 字符串。这样,可以直接在 img 标签中这么使用:
<img src="data:image/png;base64,******" />
其中的 ****** 表示图片编码后的 Base64 字符串。
进阶:三种文件类型互转
说了这么多,其实都是铺垫。实际工作中的运用,都是在这几种数据类型间相互转换。
Blob 转 ArrayBuffer
const reader = new FileReader();
reader.onload = function (result) {
// result.target.result 就是 ArrayBuffer 对象
}
reader.readerAsArrayBuffer(blob)
ArrayBuffer 转 Base64
const base64String = window.btoa(String.fromCharCode(...new Uint8Array(buffer)))
解析:
window.btoa专门用于将普通字符串转换为Base64编码的字符串;Uint8Array前面有讲解,是一个数组;String.fromCharCode将单个Uint8Array数值对应的Unicode转为普通字符串;
Blob 转 Base64
前面 Blob 转 ArrayBuffer 及 ArrayBuffer 转 Base64,那么顺其自然的,Blob 转 Base64 也是由这两部分组成。
// 传入一个回调函数拿到转换后的 Base64 字符串
const blobToBase64 = (blob, callback) => {
const reader = new FileReader();
reader.onload = function (result) {
// result.target.result 就是 ArrayBuffer 对象
const base64String = window.btoa(String.fromCharCode(...new Uint8Array(result.target.result)))
callback(base64String)
}
reader.readerAsArrayBuffer(blob)
}
当然了,还可以反向转换,这部分就留给大家自行实现。
总结
关于 Blob,File,Base64 的应用真的很多,本文仅仅是浅尝辄止,讲了一些皮毛,能应对工作中的一些基础问题。知之为知之,知道一个东西,用到的时候能想起,并能解决实际问题就是好的。列了很多概念,可能看起来不是很顺畅,但是了解过后一定会有很大收获。
回到开头,如果你有下载 Excel 的需求,但是公司封装的 download 方法已经很好用了,那你了解 Blob 后便可以阅读它的源码,了解它的实现,当然了,这也需要后端配合。如果你们没有类似实现,那么你现在也有了一个大概的思路,Excel 后端本质就是返回文件流,至于前端怎么处理,至少 Blob 是需要用到的,搜一下就有很多实现可以参考。
另外,关于图片处理等常见需求,在必要时转换为 Base64 会有很好的效果,但是并不是所有情况都适用 Base64,因为经过 Base64 编码后的文件资源一般会增加 1/3,除非一些必须以字符显示图片数据的场景,不然 Base64 不见得是最佳方案。