当前,前端对二进制数据有许多的API可以使用,这丰富了前端对文件数据的处理能力,有了这些能力,就能够对图片等文件的数据进行各种处理。
本文将着重介绍一些前端二进制数据处理相关的API知识,如Blob、File、FileReader、ArrayBuffer、TypeArray、DataView等等。
在介绍各种API之前,我们需要先了解下和字节有关的知识。
我们知道,计算机是二进制的世界,而字节(byte)是计算机技术中关于二进制数据的一种基本单位,1字节有8个二进制位,即8比特(bit)。
比特又叫位,一位二进制数据要么是0、要么是1,只有两种状态,所以1比特有2种状态。
1字节有8比特,即8个二进制位,那就能表示 2**8 = 256
种状态,取值从 00000000 到 11111111。
字节作为基本单位,在很多地方都被使用,如字符编码知识,见前文前端需要搞懂的字符编码知识。
二进制数据在存储的时候,以字节为单位,这里还涉及到一个关于字节序的知识。
字节序描述的是计算机如何存储字节。
因为我们知道,内存存储都有索引地址,每个字节对应一个索引地址。一个字节存储8位二进制,即0到255之间,但需要存储大于255的数值的时候,就需要多个字节,多个字节就涉及到排序问题。
所以字节序就是:当需要多个字节表示一个值的时候,这多个字节使用什么样的排序方式在内存中进行存储。
而排序方式主要是两种:大端存储(big-endian)和小端存储(little-endian)。
大端存储又称大字节序、高字节序,方式是低位字节排在内存中的高地址端,高字节位排放在内存中的低地址端。图片文件 png、jpg都是这种方式。
小端存储又称为小字节序、低字节序,方式是低位字节排在内存中的低地址端,高位字节排在内存中的高地址端。图片文件gif是小端序。
当我们使用不同的字节序存储数字 0x12345678
(这里是16进制表示,对应的十进制:305419896。进制相关知识可见前文Javascript中的进制和进制转换:
大端存储在内存中的存储地址:
小端存储在内存中的存储地址:
这里数字字节的高-低位是从左到右,最高位是 12
,最低位是 78
;而内存中存储时从左到右是低地址——高地址。
所以在大端序中高位字节的 12
在内存最左边的低地址位,而低字节位 78
则在内存最右边的高地址位;而小端序则正好相反。
从视觉习惯上,大端存储似乎更顺眼,但无论哪种方式,计算的结果都是一样的,只是在计算的时候需要处理这个排序方式,下文会涉及到。
Blob,即 Binary large Object,本质上是一个二进制对象,该对象表示的是一个不可变、原始数据的类文件对象。
它的不可变,代表它是只读的,不可被改变。
Blob对象的构造函数语法:new Blob(array, options)
。
参数array:是一个数据数组,可以是多种对象的数据,包含 ArrayBuffer、Blob、String 等等。
参数options:可选对象,指定两个属性:
type
表示Blob对象数据的MIME类型;
endings
指定包含行结束符\n的字符串如何写入。
我们可以使用构造函数直接创建一个新的 Blob 对象:
const blob = new Blob(['123456789'], {type : 'text/plain'});
新创建的对象实例,结构如下:
从以上示例,我们就可以看到Blob对象的方法和属性:
contentType
可以为新Blob对象指定自己的MIME类型可以针对上面的 blob
实例进行操作:
blob.slice(0, 3).text().then(res => {
console.log(res)
})
// 结果:123
以上代码,使用slice()方法获取原blob的前三位的数据,生成新的Blob实例后,通过text()方法打印出文本内容。
下面可以看看Blob在接口请求中的应用,Fetch API中的 Response
对象,拥有一个blob方法,能够得到Blob对象。
const imgRequst = new Request('11.jpg')
fetch(imgRequst).then((response) => {
return response.blob()
}).then((mBlob) => {
console.log(mBlob)
})
通过以上代码,请求一个jpg图片文件,响应对象通过 blob()
方法转为Blob对象:
File对象继承了Blob对象,是一种特殊类型的Blob,它扩展了对系统文件的支持能力。
File提供文件信息,并能够在javascript中进行访问,一般在使用 标签选择文件时返回,因为
标签允许选择多个文件,这里返回的是文件列表
files
。
除了 标签以外,还有两种方式返回File对象:
DataTransfer
对象。FileSystemFileHandle
对象的 getFile()
方法。File的构造函数:new File(bits, name[, options])
。
有三个参数:
下面代码,通过 标签读取文件:
<input id="input-file" type="file" accept="image/*" />
document.getElementById('input-file').onchange = (e) => {
const file = e.target.files[0]
console.log(file)
// ...
}
这是一个简单的图片上传,获取到的file实例,控制台打印出来:
通过上图(chrome浏览器下),可以看到File继承了Blob的素有属性和方法:
File继承自Blob,都是只读对象,除了使用slice分片以外,并没有其他操作能力,所以如果对它们进行处理需要借助其他的API。
主要用于操作Blob的API有:FileReader、URL.createObjectURL()、createImageBitmap()和XMLHttpRequest.send()。下面将介绍这几种方式。
Blob和File都是
WebAPI
,是由浏览器环境提供的,而上面提到这四种对象也同样是WebAPI。
FileReader是用于异步读取文件类型(或原始数据缓冲区)的内容,指定Blob或File对象为需要读取的文件数据。
FileReader 不能在文件系统中用路径名的方式读取文件。
构造函数:new FileReader()
。
如果对文件处理功能开发较多,对FileReader对象应该较熟,我们先看一个示例:
document.getElementById('input-file').onchange = (e) => {
const file = e.target.files[0]
const reader = new FileReader()
reader.onload = async (event) => {
const img = new Image()
img.src = event.target.result
}
reader.readAsDataURL(file)
}
以上代码,就是很常用的,使用FileReader读取一个图片文件的Base64数据,然后使用图片对象加载。Base64知识,可参考前文[深入理解Base64编码字符串。
这段代码也涉及到FileReader对像的属性、事件、方法。
常量名 | 值 | 状态描述 |
---|---|---|
EMPTY | 0 | 没有加载 |
LOADING | 1 | 正在加载 |
DONE | 2 | 已完成全部读取 |
URL是浏览器环境提供的,用于处理url链接的一个接口对象。可以通过它,解析、构造、规范和编码各种url链接。
而URL提供的一个静态方法 createObjectURL()
,可以用来处理Blob和File文件对象。
先看一个例子:
document.getElementById('input-file').onchange = (e) => {
const file = e.target.files[0]
const url = URL.createObjectURL(file)
const img = new Image()
img.onload = () => {
document.body.append(img)
}
img.src = url
}
页面展示:
这段代码就实现了上传图片,通过 URL.createObjectURL
读取后生成一个本地映射的url,再使用Image对象加载图片。
通过查看页面元素,可以看到新添加的图片元素,它的src是一个类似链接的字符串:blob:http://localhost:8088/29c8f4a5-9b47-436f-8983-03643c917f1c
,通过这个字符串,图片就能加载显示出来。
再来看 createObjectURL()
,它返回一个包含给定的Blob或File对象的url,就可以当做文件资源被加载。而这个url的生命周期和它的窗口同步,窗口关闭这个url就自动释放了。
这个url就是被称为伪协议的Objct URL。
Object URL 又被称为Blob URL,一般使用Blob或File对象生成,通过 URL.createObjectURL()
方法创建一个唯一的URL。
Object URL的格式为:blob:origin/唯一标识(uuid)
。
上面生成的URL字符串就符合这个格式:blob:http://localhost:8088/29c8f4a5-9b47-436f-8983-03643c917f1c
。
http://localhost:8088/
,如果直接打开本地html文件,则origin为null。29c8f4a5-9b47-436f-8983-03643c917f1c
。浏览器内部会为生成Object URL保持一个 URL
到 Blob
的映射,Blob是留存在内存中,浏览器只有在卸载当前窗口文档时才会释放。
如果要手动释放,则需要URL的另外一个静态方法:URL.revokeObjectURL()
,它用于销毁之前创建的URL实例,在合适的时机调用即可销毁Object URL。
URL.revokeObjectURL(url)
XMLHttpRequest.send(body):用于在XHR的HTTP请求中,发送数据体。
这里的body参数,可以是多种数据类型,包括Blob对象。
const xhr = new XMLHttpRequest()
xhr.send(new Blob())
createImageBitmap(): 主要处理图片资源,接受不同的图片资源对象为参数,并生成一个ImageBitmap对象。
这些参数就就可以是Blob和File对象。
ImageBitmap表示可以绘制在canvas上的位图图像。
createImageBitmap(file).then(imageBitmap => {
const canvas = document.createElement('canvas')
canvas.width = imageBitmap.width
canvas.height = imageBitmap.height
const ctx = canvas.getContext('2d')
ctx.drawImage(imageBitmap, 0, 0)
document.body.append(canvas)
})
如上代码,即可读取图片文件,使用canvas绘制。
ArrayBuffer 对象表示通用的、固定长度的原始二进制缓冲区,它是一个字节数组,但不能直接操作它的内容,而需要通过其他方式(如TypeArray或DataView等)进行处理。
构造函数:new ArrayBuffer(length)
,返回一个指定大小的ArrayBuffer对象。
参数length:要创建的 ArrayBuffer 的字节大小。大于Number.MAX_SAFE_INTEGER(>= 2 ** 53)或为负数,则抛出一个RangeError异常。
下面我们先使用前面介绍的 FileReader
读取一个文件的ArrayBuffer内容:
document.getElementById('input-file').onchange = (e) => {
const file = e.target.files[0]
const reader = new FileReader()
reader.onload = async (event) => {
console.log(event.target.result)
}
reader.readAsArrayBuffer(file)
}
控制台日志打印输出:
从上图,可以看到ArrayBuffer的实例属性和方法:
ArrayBuffer还有静态属性和方法:
由于我们无法直接操作ArrayBuffer,所以需要使用其他对象来处理,下面将介绍其中两种。
TypeArray,即类型化数组,它描述了二进制数据缓冲区的一个类数组。TypeArray本身不是一个可用的对象,只是一个辅助的数据类型,作为所有类型数组的构造原型,真正可用的类型数组包含了多种,如Int8Array、Uint8Array等。
常用的类型数组如下表所示:
对象 | 元素所占字节数 | 取值范围 | 描述 |
---|---|---|---|
Int8Array | 1 | -128 - 127 | 8 位有符号整型数组 |
Uint8Array | 1 | 0 - 255 | 8 位无符号整型数组 |
Uint8ClampedArray | 1 | 0 - 255 | 8 位无符号整型固定数组 |
Int16Array | 2 | -32768 - 32767 | 16 位有符号整型数组 |
Uint16Array | 2 | 0 - 65535 | 16 位无符号整型数组 |
Int32Array | 4 | -2147483648 - 2147483647 | 32 位有符号整型数组 |
Uint32Array | 4 | 0 - 4294967295 | 32 位无符号整型数组 |
Float32Array | 4 | 1.2×10**-38 to 3.4×10**38 |
32 位浮点数型数组 |
Float64Array | 8 | 5.0×10**-324 to 1.8×10**308 |
64 位浮点数型数组 |
BigInt64Array | 8 | -2**63 to 2**63-1 |
64 位有符号数型数组 |
BigUint64Array | 8 | 0 to 2**64-1 |
64 位无符号整型数组 |
类型化数组与普通数据也较相似,同样拥有一系列的方法和属性,但不支持 push
、pop
、shift
、unshift
、splice
等可以改变原数组的增删改方法。
类型化数组由于定义了数据类型,则各元素必须是同类型的数据,不能像普通数据那样元素可以是不同类型;当元素数据类型固定统一时,处理效率更优。
各类型数组在构造函数、属性、方法等语法上相同,下面就以 Uint8Array
为例。
Uint8Array构造函数:
new Uint8Array()
new Uint8Array(length)
new Uint8Array(typedArray)
new Uint8Array(object)
new Uint8Array(buffer [, byteOffset [, length]])
length 参数的最大取值
8 位类数组是 2145386496
16 位类数组是 1072693248
32 位类数组是 536346624
32 位类数组是 268173312
Uint8Array
,Uint32Array类型返回 Uint32Array
等等介绍完静态属性和方法,下面通过一个示例,来查看下Uint8Array的实例属性和方法,代码如下。
const reader = new FileReader()
reader.onload = async (event) => {
const aBuffer = event.target.result
const uint8Array = new Uint8Array(aBuffer)
console.log(uint8Array)
}
reader.readAsArrayBuffer(file)
以上代码,直接读取文件的ArrayBuffer数据,然后通过 Uint8Array
构造函数,得到Uint8Array实例,控制台查看:
通过加载一张png图片,得到它的Uint8Array数组数据,可以看到类型数组大部分的属性和方法都和普通数组类似,除了前文提到的增删改数组的方法以外。因此,对类型数组使用下标、循环等等方式进行读取,和普通函数没什么两样。
而类型数组也自己的特殊属性(都只读)和方法,如下:
要了解常见类型数组间的关系,我们先看下面这张图:
图上所示,是一张png图片的ArrayBuffer数据,可以看到,ArrayBuffer的字节长度属性默认取8位整型数组的长度,即与Int8Array和Uint8Array的长度一致。
而Int8Array的长度29848,正好是Int16Array的长度14924的两倍,是Int32Array的长度7462的四倍,可知,这里就是对字节的合并计算:
类型数组通过数组的方式对ArrayBuffer的内容进行读取操作,可以方便我们处理文件的二进制数据。
但使用类型数组的时候,碰到多字节的数据时,需要考虑字节序的问题。
下面,我们以读取小端存储的GIF图片为例。
GIF图片的Uint8Array数组数据中,宽高数据的存储就是使用了两个字节,第7-8位存储图片的宽度,9-10位存储图片的高度。
我们加载的GIF图片宽高皆为600,需要处理字节序,代码如下:
const uint8Array = new Uint8Array(aBuffer)
let bufferIndex = 6
// 获取GIF宽度的两个字节的值
const width1 = uint8Array[bufferIndex]
// width1 结果:88
const width2 = uint8Array[bufferIndex + 1]
// width2 结果:2
// 得到各自的16进制数据
const width1hex = width1.toString(16)
const width2hex = width2.toString(16)
// 转换成实际的宽度大小,注意这里把两个字节的顺序做了调整,符合小端序
const width = parseInt(width2hex + width1hex, 16)
// width 结果:600
使用小端序处理后,宽度结果等于600,符合图片实际宽度。
自己手动处理字节序会稍显麻烦,如果不想手动去处理字节序的问题,可以使用另外一各对象:DataView
。
DataView 是一个从 ArrayBuffer
中读取多种类型数值并且不用考虑字节序的接口对象。它的使用简单方便,拥有一系列的 get-
和 set-
实例方法操作数据。
DataView的构造函数:new DataView(buffer [, byteOffset [, byteLength]])
。
参数:
DataView不用考虑字节序,同样是读取GIF的宽度时,代码可简化:
const fileDataView = new DataView(arrBuffer)
let bufferIndex = 6
const width = fileDataView.getUint16(bufferIndex, true)
// 结果:600
bufferIndex += 2
const height = fileDataView.getUint16(bufferIndex, true)
// 结果:600
以上代码,很方便就得到GIF图片的宽高数据(600),因为使用了 DataView
和它的 getUint16
方法,不需要手动处理字节序。
getUint16
方法有两个参数:第一个参数代表字节索引;第二参数表示字节序,默认大端序,为true则是小端序,GIF是小端,所以上面代码为true。
除了getUint16
以外,DataView
还有十多个类似的实例方法。
get系列方法通过字节偏移索引获取对应的数值,其中多字节的数据,需要两个参数:
名称 | 参数 | 描述 |
---|---|---|
getInt8 | (byteOffset) | 有符号 8-bit 整数(1个字节) |
getUint8 | (byteOffset) | 无符号 8-bit 整数(1个字节) |
getInt16 | (byteOffset [, littleEndian]) | 16-bit数(短整型,2个字节) |
getUint16 | (byteOffset [, littleEndian]) | 16-bit数(无符号短整型,2个字节) |
getInt32 | (byteOffset [, littleEndian]) | 32-bit数(长整型,4个字节) |
getUint32 | (byteOffset [, littleEndian]) | 32-bit数(无符号长整型,4个字节) |
getFloat32 | (byteOffset [, littleEndian]) | 32-bit浮点数(单精度浮点数,4个字节) |
getFloat64 | (byteOffset [, littleEndian]) | 64-bit数(双精度浮点型,8个字节) |
getBigInt64 | (byteOffset [, littleEndian]) | 带符号的64位整数(long long类型)值 |
getBigUint64 | (byteOffset [, littleEndian]) | 无符号的64位整数(unsigned long long类型)值 |
set系列方法是和get方法对应的,处理相应字节偏移索引位置的数值,参数如下:
名称 | 参数 | 描述 |
---|---|---|
setInt8 | (byteOffset, value) | 8-bit数(一个字节) |
setUint8 | (byteOffset, value) | 8-bit数(无符号字节) |
setInt16 | (byteOffset, value [, littleEndian]) | 16-bit数(短整型) |
setUint16 | (byteOffset, value [, littleEndian]) | 16-bit数(无符号短整型) |
setInt32 | (byteOffset, value [, littleEndian]) | 32-bit数(长整型) |
setUint32 | (byteOffset, value [, littleEndian]) | 32-bit数(无符号长整型) |
setFloat32 | (byteOffset, value [, littleEndian]) | 32-bit数(浮点型) |
setFloat64 | (byteOffset, value [, littleEndian]) | 64-bit数(双精度浮点型) |
setBigInt64 | (byteOffset, value [, littleEndian]) | 带符号的64位整数(long long类型)值 |
setBigUint64 | (byteOffset, value [, littleEndian]) | 无符号的64位整数(unsigned long long类型)值 |
对于Blob和ArrayBuffer两个对象,我们可以稍做总结:
Blob和ArrayBuffer之间的转换:
如下代码:
const aBuffer = new ArrayBuffer(4)
// 使用Blob构造函数
const blob = new Blob([aBuffer])
// Blob的arrayBuffer()方法(promise)
blob.arrayBuffer()
// FileReader
const reader = new FileReader()
reader.readAsArrayBuffer(blob)