Blob、File、ArrayBuffer、TypedArray、DataView究竟应该如何应用

·  阅读 1583
Blob、File、ArrayBuffer、TypedArray、DataView究竟应该如何应用

引言

Blob、File、ArrayBuffer、TypedArray、DataView、Object URL ..等等 Web 应用中有关于进制的应用你了解多少?

其实我们可以利用上述 Web API 来做很多事情,并且它们之前存在着惟妙惟肖的关联关系。

来吧,一篇文章让你带你畅游 Web 世界中最直观的二进制应用。

ArrayBuffer

概念

ArrayBuffer 对象用来表示通用的、固定长度的原始二进制数据缓冲区。它是一个字节数组,通常在其他语言中称为“byte array”。

你不能直接操作 ArrayBuffer 的内容,而是要通过类型数组对象或 DataView 对象来操作,它们会将缓冲区中的数据表示为特定的格式,并通过这些格式来读写缓冲区的内容。

它的含义类似 NodeJs 中的 Buffer 。简单来说,我们可以通过 ArrayBuffer 来开辟一段二进制数据空间,但是它只能通过 TypedArray 或者 DataView 来进行操作。

用法

在计算机中我们都了解每 8 位代表一个字节,在 Web Api 中提供给了我们一个 ArrayBuffer 内置模块,通过实例化 new ArrayBuffer(number) 可以创建对应固定 number 大小的字节长度缓冲区。

比如:

// 创建一个长度为 8 的 ArrayBuffer ,此时开辟一个固定 8 个字节的缓冲区也就是 64 位
const buffer = new ArrayBuffer(8);

console.log(buffer.byteLength);
// expected output: 8
复制代码

具体关系如下图所示:

image.png

ArrayBuffer 代表的含义就和他的名字类似,它是一个数组,数组中包含的元素就是字节。

上图中每一个无间隔小块代表一个位,8位组成一个字节。每一个存在间隔的长块表示一个字节,整个8个字节组成了我们创建的 buffer 对象。

TypedArray

上边我们说到通过 new ArrayBuffer(number) 可以创建对应长度字节的固定缓冲区。

同时也提供要操作创建的缓冲区实例对象,需要通过类型数组对象(TypedArray)或者 DataView 来进行操作。

那么我们就先来看一看什么是 TypedArray。

概念

类型化数组TypedArray) 对象描述了一个底层的二进制数据缓冲区(binary data buffer)的一个类数组视图(view)。

稍微翻译下上边的话,也就是说可以通过 TypedArray 来操作 ArrayBuffer 的实例。

其次,没有名为 TypedArray 的全局属性,也没有一个名为 TypedArray 的构造函数。相反,有许多不同的全局属性,它们的值是特定元素类型的类型化数组构造函数

这句话简单来讲,你可以将 TypedArray 理解为一种接口的形式。所谓 TypedArray 它并不包含具体的实现而是代表一系列具有相同特性(类数组视图)的集合概念。

也就是说 TypedArray 不可被直接实例化,本身也无法访问。但是它有很多种不同的实现方式。

也许有部分同学不太理解 TypedArray 是一种接口的形式,别着急,稍后我们来举个例子你马上就会明白了。

关于 TypedArray 的具体实现,它存在以下方式:

类型单个元素值的范围大小(bytes)描述Web IDL 类型C 语言中的等价类型
Int8Array-12812718 位二进制有符号整数byteint8_t
Uint8Array025518 位无符号整数(超出范围后从另一边界循环)octetuint8_t
Uint8ClampedArray025518 位无符号整数(超出范围后为边界值)octetuint8_t
Int16Array-3276832767216 位二进制有符号整数shortint16_t
Uint16Array065535216 位无符号整数unsigned shortuint16_t
Int32Array-21474836482147483647432 位二进制有符号整数longint32_t
Uint32Array04294967295432 位无符号整数unsigned longuint32_t
Float32Array-3.4E383.4E38 最小正数为:1.2E-38432 位 IEEE 浮点数(7 位有效数字,如 1.1234567unrestricted floatfloat
Float64Array-1.8E3081.8E308 最小正数为:5E-324864 位 IEEE 浮点数(16 有效数字,如 1.123...15)unrestricted doubledouble
BigInt64Array-2^632^63-1864 位二进制有符号整数bigintint64_t (signed long long)
BigUint64Array02^64 - 1864 位无符号整数bigintuint64_t (unsigned long long

Uint8Array

乍一看特别多对吧,其实它们的用法是类似的。这里我们以为 Int8Array 和 Unint8Array 来举一个简单的例子:

// 创建8个字节长度的缓存冲
const buffer = new ArrayBuffer(8);

// 将buffer转化为Uint8Array
// Uint8Array中每一个元素表示一个字节(8位)
const uint8Array = new Uint8Array(buffer);

// log: [0, 0, 0, 0,0, 0, 0, 0]
console.log(uint8Array);

// 64位 8字节(log:8)
console.log(uint8Array.length);
复制代码

我们稍微用一张图来解释下对应的 Unint8Array 的含义:

image.png

  • 上述代码我们首先通过 new ArrayBuffer(8) 创建了 8 个字节大小的缓冲区。

  • 之后通过new Uint8Array(buffer)创建了一个 Unint8Array。

所谓 Unint8Array 中每个元素代表 8 位(一个字节)大小,我们可以通过 Unint8Array 来操控刚才创建的 ArrayBuffer 。

之后我们打印了,unint8Array 的长度,因为 buffer 大小为8个字节64位自然通过 buffer 创建的 unit8Array 大小为 64/8 = 8 个长度大小的 unit8Array。

Uint8Array 意味无符号整形数组,故而在二进制中每个元素最大为8个1,最小为8个0。自然转化为10进制后每个元素范围为0~255。

同理 Int8Array 表示有符号的整形数组,每个位首代表正负符号。故而 Int8Array 每个元素大小范围为-128~127。

关于 Uint8Array 更加详尽的 API 你可以查阅这里

Uint16Array

在清楚了 Uint8Array 代表的含义后,趁热打铁我们来看看 Uint16Array 是如何使用的。

其实在上述我们说过,无论是 Uint16Array 、 Uint8Array 还是其他类似 API 本质上用法都是一样的。它们统一被归类为 TypedArray。

// 创建8个字节长度的缓存冲
const buffer = new ArrayBuffer(8);

// 将buffer转化为Uint16Array
// Uint8Array中每一个元素表示两个字节(16位)
const uint8Array = new Uint16Array(buffer);

// log: Uint16Array(4) [ 0, 0, 0, 0 ]
console.log(uint8Array);

// 64位 8字节 -> 4个元素(log:4)
console.log(uint8Array.length);
复制代码

image.png

同样,Uint16Array 代表16位无符号整数,Uint16Array 中每个元素存储16位(2个字节)。

自然,我们输出它的长度位 4。Int16Array 同样每个元素存储为有符号 16 位整数,每个元素位首位置表示正负数。

换算为10进制,Uint16Array中每个元素大小范围为 0 ~ 2^16 也就是 0 ~ 65536 。

DataView

在了解了 TypedArray 之后,我们来看看另一种操作 ArrayBuffer 的方式:DataView。

相较于 TypedArray,DataView 对于 ArrayBuffer 的操作更加灵活。

我们可以发现在 TypedArray 中操作二进制 ArrayBuffer 时每个元素占用的字节大小是固定的,要么每个元素占用8位、16位或者32位。

而 DataView 对于 ArrayBuffer 的操作就显得更加灵活了,我们可以通过 DataView 从 ArrayBuffer 中自由的读写多种数据类型,从而控制字节顺序。

概念

DataView 视图是一个可以从 二进制ArrayBuffer 对象中读写多种数值类型的底层接口,使用它时,不用考虑不同平台的字节序问题。

简单来讲,想较与 TypedArray 每个元素中固定的字节大小,我们可以通过 DataView 来自由的操作 ArrayBuffer 。

用法

同样关于 DataView 的操作 Api 也是非常庞大的,但是它们都是大同小异。

具体所有相关 API 你可以在这里查阅

这里我们同样以 8位无符号以及 16位无符号整数来举例如何通过 DataView 来操控 ArrayBuffer:

创建DataView

new DataView(buffer [, byteOffset [, byteLength]])
复制代码

创建 DataView 支持传入三个参数:

  • 第一个参数 buffer 为必填,它支持传入一个 ArrayBuffer 表示 DataView 中的源数据。

  • 第二个参数 byteOffset 选填,它表示创建 DataView 时开头从 buffer 的哪个字节开始,可以作为启始偏移量。未指定时,默认从第一个字节开始。

  • 第三个参数 btyeLength 选填,它表示创建该 DataView 时的长度,当不传递默认时表示匹配 buffer 的长度。

// 创建8个字节长度的缓存冲
const buffer = new ArrayBuffer(8);

// 根据传入的buffer 从第一个字节开始,并且字节长度为匹配buffer的长度
const dataView = new DataView(buffer);

/**
 * DataView {
    byteLength: 8,
    byteOffset: 0,
    buffer: ArrayBuffer {
      [Uint8Contents]: <00 00 00 00 00 00 00 00>,
      byteLength: 8
    }
  }
 */
console.log(dataView, 'dataView');

// log: 8
console.log(dataView.byteLength, 'dataView');
复制代码

比如上述的代码,我们通过 new DataView 创建了对应 buffer 的 DataView 。

接下来我们来看看如何利用 DataView 来操作 ArrayBuffer,这里我们以为 setUint8 来举例:

setUint8

setUint8() 表示从DataView起始位置以byte为计数的指定偏移量(byteOffset)处储存一个8-bit数(无符号字节).

比如:

// 创建8个字节长度的缓存冲
const buffer = new ArrayBuffer(8);

// 根据传入的buffer 从第一个字节开始,并且字节长度为匹配buffer的长度
const dataView = new DataView(buffer);

// 将DataView中偏移量为0个字节的字节,也就是第一个字节设置为十进制的1
dataView.setUint8(0, 1);
// 将DataView中偏移量为1个字节的字节,也就是第二个字节设置为十进制的2
dataView.setUint8(1, 2);
复制代码

具体的 DataView.prototype.setUint8 API 你可以在这里看到

setUint8 支持传入两个参数,分别表示:

  • 第一参数为 byteOffset,它表示设置的字节偏移量,偏移量单位为字节。

  • 第二个参数 value,它表示设置的值。为 10 进制表示法。

比如上述我们 Demo 中通过 setUint8 来操纵创建好的 ArrayBuffer ,当首次创建ArrayBuffer时内部所有位全部为空也就是:

image.png

当代码执行到 dataView.setUint8(0, 1) 时,表示我们将要给 dataView 中以 8位(一个字节位单位)设置偏移量为 0 (表示第一个字节),设置它的值为 1 (10进制)。

此时,dataView 中的 ArrayBuffer 如下图所示:

image.png

分别将第一个字节(8位)的值变为 1 和将第二个字节变为 10 进制的 2。

关于10进制和2进制的转化,这里我就不做过多介绍了。这里基础不太明白的同学可以自行百度。

此时,我们就通过 DataView 将 ArrayBuffer 中的 第一个字节变成了 1 以及将第二个字节变成了 2。

getUint8

了解了 setUint8 之后,我们一起来看看 getUint8 吧。

getUint8() 方法``从DataView相对于起始位置偏移 n 个字节处开始,获取一个无符号的8-bit整数(一个字节).

getUint8 的用法和 setUint8 的用法类似,只不过一个是作为获取另一个是作为设置来说的。

// 创建8个字节长度的缓存冲
const buffer = new ArrayBuffer(8);

// 根据传入的buffer 从第一个字节开始,并且字节长度为匹配buffer的长度
const dataView = new DataView(buffer);

// 将DataView中偏移量为0个字节的字节,也就是第一个字节设置为十进制的1
dataView.setUint8(0, 1);
// 将DataView中偏移量为1个字节的字节,也就是第二个字节设置为十进制的2
dataView.setUint8(1, 2);

// 从dataView中偏移第0个字节,也就是第一个字节,获取8位
// log: 1
dataView.getUint8(0);

// 从dataView中偏移第一个字节获取八位,也就是获取第二个字节的值
// log: 2
dataView.getUint8(1);

console.log(dataView.getUint8(0));
console.log(dataView.getUint8(1));

复制代码

相信通过上述的例子配合注释对于 getInt8 表示的含义大家应该都可以明白了。

setUint16 & getUint16

接下来我们在看看看 DataView 中另一组 Api : setUint16 和 getUnint16。

setUint16() DataView起始位置以byte为计数的指定偏移量(byteOffset)处储存一个16-bit数(无符号短整型).

setUint16 和 setUint8 用法是完全一致的,唯一的区别就是setUint16设置的是后续16位也就是两个字节的值,而setUint8设置的仅仅是后续8位也就是一个字节的值。

同理,getUnit16 和 getUint8 也是同样。

我们来看一个简短的例子:

// 创建8个字节长度的缓存冲
const buffer = new ArrayBuffer(8);

// 根据传入的buffer 从第一个字节开始,并且字节长度为匹配buffer的长度
const dataView = new DataView(buffer);

// 将DataView中偏移量为0个字节的字节,也就是第一个字节设置为十进制的1
dataView.setUint8(0, 1);
// 将DataView中偏移量为1个字节的字节,也就是第二个字节设置为十进制的2
dataView.setUint8(1, 2);

// 从dataView中偏移第0个字节,也就是第一个字节,获取8位
// log: 1
dataView.getUint8(0);

// 从dataView中偏移第一个字节获取八位,也就是获取第二个字节的值
// log: 2
dataView.getUint8(1);

// 偏移量为0个字节,获取后续16位大小(也就是获取前两个字节大小)
// log: 258
dataView.getUint16(0);

// 偏移量为2个字节,设置后16位大小为256(也就是设置第三个字节和第四个字节大小和为256)
dataView.setUint16(2, 256);

// 偏移量为2个字节,获取后16位大小
// log: 256
dataView.getUint16(2);
复制代码

同样,我们来用一张图来表示:

image.png

结合图示来理解上述的代码就会容易许多,相对 int8 来说 int16 与它不同仅仅是 8 位和 16位的区别。

当然关于 DataView 还有许多 16位 、32位等等之类 API,但是用法都是大同小异。大家掌握了上述的含义之后都是触类旁通的。

ArrayBuffer & TypedArray & DataView

上边我们理清了什么是 ArrayBuffer 以及 TypedArray 和 DataView 与它的关系。

本质上,ArrayBuffer 字节数组就是一段固定长度大小的二进制数据缓冲区。它并不能直接操作,我们需要通过 TypedArray 和 DataView 来进行对于 ArrayBuffer 的操作。

当然,它们三者之间也是可以互相转化的。同样,我们用一张图来进行阶段性的总结:

image.png

我们可以通过 new DataView 构造 DataView 实例,同样可以通过 new TypedArray 来将 buffer 实例转化为 TypedArray 进行操作。

同样,也可以通过它们各自的 buffer 属性来获取对应 ArrayBuffer 的内容。

// 创建8个字节长度的缓存冲
const buffer = new ArrayBuffer(8);

const dataView = new DataView(buffer);
// 获取对应buffer内容
console.log(dataView.buffer)

const typedArray = new Uint8Array(buffer);
// 获取对应buffer内容
console.log(typedArray.buffer);
复制代码

Blob

了解了 Web 的一些基础二进制操作后,我们来看看 Web 中基于它们的延伸。

相信大家在日常工作中或多多少都遇到过 blob 相关的应用,比如 blob 格式的 Url 以及对于文件上传中切片等等应用场景。接下来,我们一起来看看所谓 blob 对象。

基础概念

Blob 对象表示一个不可变、原始数据的类文件对象。它的数据可以按文本或二进制的格式进行读取,也可以转换成 ReadableStream 来用于数据操作。

注意 File 对象是继承与 blob 的,我们会在之后探讨 File 。

const aBlob = new Blob( array, options );
复制代码

Blob()  构造函数返回一个新的 Blob 对象。 blob的内容由参数数组中给出的值的串联组成。

通过 new Blob 可以创建一个新的 blob 对象实例,构造函数支持接受两个参数:

  • 第一个参数 array 是一个由ArrayBufferArrayBufferViewBlobDOMString 等对象构成的 Array ,或者其他类似对象的混合体,它将会被放进 Blob。DOMStrings会被编码为UTF-8。

  • 第二个参数 options 是一个对象,它拥有如下属性:

    • type,默认值为 "",它代表了将会被放入到blob中的数组内容的MIME类型。

    • endings,默认值为"transparent",用于指定包含行结束符\n的字符串如何被写入。 它是以下两个值中的一个: "native",代表行结束符会被更改为适合宿主操作系统文件系统的换行符,或者 "transparent",代表会保持blob中保存的结束符不变。

我们来试试创建一个 blob 对象:


const name = JSON.stringify({ name: '19QIngfeng' });

// 传入DOMString创建blob
const blob = new Blob([name], {
  type: 'application/json',
});

// log: 21 utf8中一个英文代表一个字节
console.log(blob.size);

const buffer = new ArrayBuffer(8);

// 传入ArrayBuffer创建blob
const bufferToBlob = new Blob([buffer]);

// log: 8 
console.log(bufferToBlob.size);
复制代码

读取blob内容

通过上边的基础内容,我们清楚了如何利用 DOMString、ArrayBuffer 等创建 blob 对象,但是如何读取 blob 中的内容呢?

这个时候,就引出了另一个关于文件操作中的常见 Web Api :fileReader

FileReader 对象允许Web应用程序异步读取存储在用户计算机上的文件(或原始数据缓冲区)的内容,使用 File 或 Blob 对象指定要读取的文件或数据。

我们可以结合 FileReader Api 来读取对应 blob 的内容,并且将它转化为各种我们需要得到的格式:

    const name = JSON.stringify({
      name: '19QIngfeng'
    });

    // 传入DOMString创建blob
    const blob = new Blob([name], {
      type: 'application/json',
    });

    /**
     *
     * @param {*} blob blob 对象
     * @param {*} type 输出的结果
     */
    function getBlobByType(blob, type) {
      const fileReader = new FileReader(blob);
      switch (type) {
        // 读取文件的 ArrayBuffer 数据对象.
        case 'arrayBuffer':
          fileReader.readAsArrayBuffer(blob);
          break;
          // 读取文件为的字符串
        case 'DOMstring':
          fileReader.readAsText(blob, 'utf8');
          break;
          // 读取文件为data: URL格式的Base64字符串
        case 'dataUrl':
          fileReader.readAsDataURL(blob);
          break;
          // 读取文件为文件的原始二进制数据(已废弃不推荐使用)
        case 'binaryString':
          fileReader.readAsBinaryString(blob);
          break;
        default:
          break;
      }

      return new Promise((resolve) => {
        // 当文件读取完成时候触发
        fileReader.onload = (e) => {
          // 获取最终读取结果
          const result = e.target.result;
          resolve(result);
        };
      });
    }

    // ArrayBuffer 对象
    getBlobByType(blob, 'arrayBuffer').then((res) => console.log(res));

    // {"name":"19QIngfeng"}
    getBlobByType(blob, 'DOMstring').then((res) => console.log(res));

    // data:application/json;base64,eyJuYW1lIjoiMTlRSW5nZmVuZyJ9
    getBlobByType(blob, 'dataUrl').then((res) => console.log(res));

    // {"name":"19QIngfeng"}
    getBlobByType(blob, 'binaryString').then((res) => console.log(res));
复制代码

上边的代码我们通过 fileReader 对应的 API 传入 blob 对象,从而读取 blob 内容为各种格式的内容。

Blob & ArrayBuffer

细心的小伙伴可能已经发现了,此时,我们又清楚了 ArrayBuffer 和 Blob 微妙的关系。

让我们继续再来完善上边的关系图:

image.png

关于 File 接口,它提供有关文件的信息,并允许网页中的 JavaScript 访问其内容。

通常情况下, File 对象是来自用户在一个 <input> 元素上选择文件后返回的 FileList 对象,也可以是来自由拖放操作生成的 DataTransfer 对象,或者来自 HTMLCanvasElement 上的 mozGetAsFile() API。

简单来说,File 就是基于 Blob 而来。它拥有Blob的所有功能的同时扩展了一系列关于文件的属性。

Object URL

概念

大多数情况下,我们可以看到一些网页内部可以看到一些诸如此类的 Blob Url:

image.png

我们可以看到视频标签 vide 的 src 属性正式一个 Blob 类型的 Url。

关于 Blob URL/Object URL 其实它们是一种伪协议,允许将 Blob 和 File 对象用作图像、二进制数据下载链接等的 URL 源。它的好处其实有很多,比如:

平常我们并不可以直接处理 Image 标签之类的原始二进制数据,所以对于图片等需要 Url 作为源的标签通常做法是将图片上传到服务器上得到一个 Url 从而通过 URL 加载二进制数据。

与其上传二进制数据,然后通过 URL 将其返回,不如使用 Blob/Object Url 无需额外的步骤,使用浏览器本地 Api 即可直接访问数据而不需要通过服务器来上传数据。

当然,一些小伙伴可能会有疑惑。Base64 字符串编码不也可以解决上述说的问题吗。重点是相较于 base64 编码来说, Blob 是纯二进制字节数组,不会像 Data-URI 那样有任何显着的开销,这使得它们处理起来更快更小。

同时这些 URL 只能在浏览器的单个实例和同一会话(即页面/文档的生命周期)中本地使用,这意味者离开当前浏览器实例这个 URL 就会失效。

我们可以通过 URL.createObjectURL(object) 来创建对应的 Object URL,这个方法会返回一个 DOMString 字符串,其中包含一个表示参数中给出的对象的URL。

同时,这个 URL 的生命周期和创建它的窗口中的 document 绑定。这个新的URL 对象表示指定的 File 对象或 Blob 对象。

在创建时候它会接受一个参数:

同样它会返回一个DOMString包含了一个对象URL,该URL可用于指定源 object的内容。

返回的 DOMString 格式为 blob:<origin>/<uuid>

当然,当在你的网页上不再使用通过 URL.createObjectURL(object) 创建的 URL 时,记得使用 URL.revokeObjectURL(url) 来主动释放它们。

使用

我们来结合一个实例来看看如何使用 URL.createObjectURL :

const name = JSON.stringify({
  name: '19QIngfeng',
});

// 传入DOMString创建blob
const blob = new Blob([name], {
  type: 'application/json',
});

// 创建 Object Url
const url = URL.createObjectURL(blob);

const aLink = document.createElement('a');

// href属性
aLink.href = url;
// 定义下载的文件名
aLink.download = 'name.json';

// 派发a链接的点击事件
aLink.dispatchEvent(new MouseEvent('click'));

// 下载完成后,释放 URL.createObjectURL() 创建的 URL 对象。
URL.revokeObjectURL(url);
复制代码

这段 JS 代码会在我们打开 html 页面后自动下载一个 name.json 的文件,而下载的 name.json 的 URL 来源正是我们通过 URL.createObjectURL(blob); 创建的。

URL & Blob

根据上述提到的 Object URL 的基本概念以及与 Blob 之间的联系,趁热打铁我们来在继续完善上述的关系图:

image.png

上图可以看到一个完整清晰的流程,我们可以通过 URL.createObjectURL 将 blob 转化为 url 使用。

写在结尾

首先,感谢每一位可以看到这里的小伙伴。

当然,文章中对于 Web 世界中的二进制应用仅仅起到了一个抛砖引玉的作用。

对于一些更加底层的二进制基础,比如基数、位权等等。文章并没有过多涉猎,当然有兴趣的小伙伴可以私下去补充这部分基础知识。

同时,对于一些更多 ArrayBuffer 后续的应用,比如 Blob 与 Canvas 以及 image 与 Canvas 、 DataUrl 之间的关系这里我也没有过多延伸。

后续,如果有小伙伴对于这部分感兴趣我可以额外补充一篇文章去延伸更多在 Web 中这些进制 API 的应用和关联。

最后,大家加油!

分类:
前端
收藏成功!
已添加到「」, 点击更改