0


最详细的前端二进制数据流

一、数据类型

1、ArrayBuffer

ArrayBuffer 的基本使用

ArrayBuffer 对象表示存储二进制数据的一段内存,他不能直接读写,只能通过视图(TypedArray 视图和 DataView 视图)来读写,是图的作用就是以指定的格式读写二进制数据,ArrayBuffer 也是一个构造函数,可以分配一段可以存放数据的连续内存。

const buffer = new ArrayBuffer(32);
复制代码

如上所示使用 ArrayBuffer 生成了一段32字节的内存区域,每个字节默认都是0,为了查看 buffer 中保存的数据我们这里使用

TypeArray

中的

Int8Array

进行查看:

const array = new IntArray(buffer);
console.log(array[0]); // 0
复制代码

ArrayBuffer.byteLength

ArrayBuffer 实例的 byteLength 属性表示的是当前分配的内存区域的字节长度。

console.log(buffer.byteLength) // 32
复制代码

但是如果我们需要分配的内存区很大时有可能会分配失败,因为可能当前不存在这么多连续的内存空间,所以我们需要自己去检查是否分配成功。

if(buffer.byteLength === len) {

} else {
    // 失败
}
复制代码

ArrayBuffer.slice

slice

方法允许将内存区的一部分拷贝生成一个新的

ArrayBuffer

对象,他接受两个参数,第一个参数表示开始的字节序号,第二个参数表示截止的字节序号(左闭右开),如果省略第二个参数,则默认是原 ArrayBuffer 对象的结尾。

const newBuffer = buffer.slice(1,10);
复制代码
slice

其实包含了两步,首先去要重新分配一段新的内存,其次就是将原来的

ArrayBuffer

对象拷贝过去。

2、TypedArray视图

我们知道

ArrayBuffer

对象是一个内区区域,如果我们想要读写里面的数据就需要借助视图,这里要介绍的就是

TypedArray

,目前

TypedArray

一共包含9种类型,每种视图类型都是一个构造函数。

  1. Int8Array:8位有符号整数,每一项占1个字节。
  2. Uint8Arry:8位无符号整数,每一项占1个字节。
  3. Uint8ClampedArray:8位无符号整数,每一项占1字节。
  4. Int6Array:16位有符号整数,每一项占2个字节。
  5. Uint16Array:16位无符号整数,每一项占2个字节。
  6. Int32Array: 32位有符号整数,每一项占4个字节。
  7. Uint32Array:32位无符号整数,每一项占4个字节。
  8. Float32Array:32位浮点数,每一项占4个字节。
  9. Float64Array:64位浮点数,每一项占8个字节。

这些视图其实和数组很相似,都有 length 属性,都能通过下标访问,数组上的方法也能在这里使用,但是他们之间还是有很大不同,如下:

  • TypedArray 的每一项都是相同类型的,而普通数组可以是多个类型组合。
  • TypedArray 的每一项默认是0,而普通数组则是空位。
  • TypedArray 本身只是一层视图不存储任何数据。

下面拿

Int8Array

Int16Array

举例。

const buffer = new ArrayBuffer(8);
const arr = new Int8Array(buffer);
复制代码

首先我们用

Int8Array

数组去视图话

buffer

,因为

Int8Array

每一项刚好是一个字节,所以他的每一项刚好对应

buffer

的每一个字节的数据。

image.png

我们尝试去修改缓冲区的数据。

arr[0] = 12;
arr[1] = 13;
复制代码

这里我们将缓冲区的第一个字节和第二个字节的数据改了,如下图所示: image.png

下面我们使用

Int16Array

来视图化

buffer

,这里要注意我们此时已经通过 Int8Array 改变了

buffer

中的数据,我们使用

Int16Array

视图化的同样也是改变后的

buffer

const arr1 = new Int16Array(buffer);
复制代码
Int16Array

的每一项占2个字节,也就是说 arr1 中的每一项对应着 buffer 中两个字节的数据。

image.png

我们可以打印一下

arr[0]

,发现输出的是3340,这其实是将13和12的二进制进行组合,这里要注意的是组合的顺序是倒序的(小端字节序),让我们去修改 arr1 时其实就是修改了两个字节的内容。

3、DataView

ArrayByffer

的另一种视图就是

DataView

,他相比于

TypeArray

更加的灵活,假如我们使用

Int16Array

去视图化

ArrayBuffer

,那他的每一项只能对应两个字节的数据,即我们一次只能读写两个字节的数据,但是对于

DataView

来说就更加灵活了,我们一次可以读写一个或者多个字节的数据,同时还能控制字节序为大端还是小端。

DataView 的读取

DataView

提供了8个方法读取内存。

  • getInt8:读取一个字节的数据,返回一个8位的整数
  • getUint8:读取一个字节的数据,返回一个8位的无符号整数
  • getInt16:读取两个字节的数据,返回一个16位的整数
  • getUint16:读取两个字节的数据,返回一个无符号的16位整数
  • getInt32:读取4个字节的数据,返回一个32位整数
  • getUint32:读取4个字节的数据,返回一个无符号的32位整数
  • getFloat32:读取4个字节的数据,返回一个32位浮点数
  • getFloat64:读取8个字节的数据,返回一个64位浮点数

这些方法接受两个参数,第一个参数表示从哪一个字节开始读取,第二个参数是一个布尔值,表示是采用大端字节序还是小端字节序,默认情况下是大端。

const buffer = new ArrayBuffer(16);
const arr = new DataView(buffer);
// 采用大端序的方式从第一个字节开始读取
const data1 = arr.getInt8(0);
// 采用大端序的方式从第二个字节开始读取
const data2 = arr.getInt8(1, false);
// 采用小端序的方式从第三个字节开始读取
const data3 = arr.getInt8(2, true);
复制代码

DataView的写入

DataView

也有8种写入方法。

  • setInt8:写入一个字节的8位整数
  • setUint8:写入一个字节的无符号8位整数
  • setInt16:写入2个字节的16位整数
  • setUint16:写入2个字节的16位无符号整数
  • setInt32:写入4个字节的32位整数
  • setUint32:写入4个字节的无符号整数
  • setFloat32:写入4个字节的32位浮点数
  • setFloat64:写入8个字节的64位浮点数

这些方法接受三个参数,第一个参数是写入的起始位置,第二个参数是要写入的数据,第三个参数表示使用大端序还是小端序,默认是大端序。

const buffer = new ArrayBuffer(16);
const arr = new DataView(buffer);
// 在第一个字节中以大端序写入12
arr.setInt8(0, 12)
// 在第二个字节中以小端序写入12
arr.setInt8(1, 12, true)
// 在第三个字节中以大端序写入12
arr.setInt(2, 12, false)
复制代码

4、Blob

Blob

表示二进制类型的大对象,这个对象一旦创建就不允许更改,通常表示影像、声音或者多多媒体文件,在

Mysql

数据库中就有一种

Blob

类型,专门用来存放二进制数据,在 JavaScript 中

Blob

对象表示一个不可变的原始数据的文件对象,JavaScript 中的

File

接口也是基于

Blob

的,继承了

Blob

的功能并将其扩展使其能够支持用户系统上的文件。

Blob

由字符串 type 和 blobParts 组成,其中 type 通常为 MIME 类型。MIME 表示多 用途互联网邮件扩展类型,比如超文本标记语言文本的

text/html

,图像中的

image/xxx

,普通本的

text/plain

等,blobParts 是由 ArrayBuffer、ArrayBufferView、Blob、DOMString 等组成。

下面通过一个具体的实例来介绍:

image.png

从图中可以看到,Blob 实例上包含两大部分,一个是基本组成即 size 和 type,一个是原型上定义的一些属性和方法。

属性介绍

  • size:只读属性,表示 Blob 实例中包含数据的大小,单位是字节,上面我们使用的字符串刚好就是9个字节。
  • type:只读属性,上面也介绍过了,表示 Blob 实例所存放数据的 MIME 类型,如果我们不穿则为空字符串。

方法介绍

  • slice:slice 方法用来对 Blob 对象进行切割,他会返回一个新的 Blob 对象,通常在上传大数据时需要用到。
  • stream:返回一个能够读取 Blob 中内容的 ReadableStream
  • text:返回一个 Promise 对象,里面包含了 Blob 中所有内容,这些内容是以 UTF-8 格式的 USVString 展示出来的。
  • arrayBuffer:返回一个 Promise 对象,Promise 的返回值是包含了 Blob 所有数据的 ArrayBuffer 对象。

5、File

File

表示一个文件,可以通过 Javacript 来访问文件的内容,通常情况下

File

对象来自用户在

<input>

元素上选择文件后返回的

FileList

数组中,里面每一项都是一个

File

对象,也可以是来自拖放操作中的

DataTransfer

对象。

File

对象本身是特殊的

Blob

类型,它继承了

Blob

原型上的所有方法,我们可以通过一个示例来查看:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
<body>
    <input type="file" onchange="loadFile(event)">
</body>
</html>

<script>
   const loadFile = function(event) {
       console.log(event.target.files[0]);
   };
</script>
复制代码

下面是

File

的具体信息。

image.png 从图中可以直观的看到其实

File

对象是在

Blob

的基础上添加了一些比如lastModified、lastModifiedDate、webkitRelativePath、name 属性用于描述当前文件的信息。

  • lastModified:文件最后修改时间戳
  • lastModifiedDate:文件最后修改时间的 Data 对象
  • webkitRelativePath:返回 File 的路径或者 URL

现在我们可以正常拿到文件信息,但是我们怎么使用

File

对象中的文件信息呢?比如我上传一张本地的图片想展示在页面上,或者将本地上传的文件通过接口传给服务器,此时就需要用到

FileReader

对象。

6、FileReader

FileReader

对象可以用于读取计算机上的文件的内容,可以使用

File

或者

Blob

指定需要读取文件或数据。简而言之就是用于读取

File

对象或者

Blob

对象中所包含的文件内容。

实例的属性

  • FileReader.error : 表示在读取文件时发生的错误。
  • FileReader.readyState : 整数,表示读取文件时的当前状态,该状态有三种值- 0: 表示还没有加载任何数据- 1: 表示数据正在加载- 2: 表示加载完成
  • FileReader.result 读取完成后的文件内容。

实例的事件

  • FileReader.onabort:该事件在读取操作被中断时触发。
  • FileReader.onerror:该事件在读取操作发生错误时触发。
  • FileReader.onload:该事件在读取操作完成时触发。
  • FileReader.onloadstart:该事件在读取操作开始时触发。
  • FileReader.onloadend:该事件在读取操作结束时(要么成功,要么失败)触发。
  • FileReader.onprogress:该事件在读取 Blob 数据时触发。

实例的方法

  • FileReader.abort():终止读取操作,此时 readyState 属性将变成2。
  • FileReader.readAsArrayBuffer():将数据存储在 ArrayBuffer 实例中并返回。
  • FileReader.readAsBinaryString():将数据转换为二进制字符串返回。
  • FileReader.readAsDataURL():将数据转换为 Base64 编码的字符串返回。
  • FileReader.readAsText():将数据转换为纯文本字符串。

示例

使用

readAsArrayBuffer

存储图片文件

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
<body>
    <input type="file" onchange="loadImage(event)">
    <img src="" height="200">
</body>
</html>

<script>
   function loadImage(event) {
      let preview = document.querySelector('img');
      let reader  = new FileReader();

      reader.addEventListener('load', function () {
        const blob = new Blob([reader.result])
        // 将blob转换成对应的url对象
        const url = window.URL.createObjectURL(blob);
        preview.src = url;
      }, false);

      reader.readAsArrayBuffer(event.target.files[0]);
    }
</script>
复制代码

使用

readAsDataURL

将图片转换为 base64

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
<body>
    <input type="file" onchange="loadImage(event)">
    <img src="" height="200">
</body>
</html>

<script>
   function loadImage(event) {
      let preview = document.querySelector('img');
      let reader  = new FileReader();

      reader.addEventListener('load', function () {
        preview.src = url;
      }, false);

      reader.readAsDataURL(event.target.files[0]);
    }
</script>
复制代码

7、Buffer

在处理 TCP 流或者文件流时,必须要使用二进制数据,但是 JavaScript 本身只有字符串数据类型,因此在 Node.js 中定义了一个

Buffer

类,该类用来创建一个专门存放二进制数据的缓存区。我们看下

Buffer

的类型定义:

interface Buffer extends Uint8Array {
    write(string: string, encoding?: BufferEncoding): number;
    write(string: string, offset: number, encoding?: BufferEncoding): number;
    write(string: string, offset: number, length: number, encoding?: BufferEncoding): number;
    toString(encoding?: BufferEncoding, start?: number, end?: number): string;
    toJSON(): {
        type: 'Buffer';
        data: number[];
    };
}
复制代码

我们发现

Buffer

其实是继承自

Uint8Array

的,这也就是说如果我们可以通过

Buffer.buffer

拿到对应的

ArrayBuffer

二、数据类型之间的转换

1、ArrayBuffer 转 Blob

const blob = new Blob([arrayBuffer], {type: xxxx})
复制代码

2、Blob 转 ArrayBuffer

Blob 转 ArrayBuffer 要借助 FileReader。

const blob = new Blob('xxxx');
cosnt fileReader = new FileReader();
fileReader.onload = function(){
    const result = fileReader.result
}
readAsArrayBuffer(blob);
复制代码

3、Blob 转 File

因为 File 的构造函数可以接受 Blob 的参数,所以可以直接通过 File 的构造函数进行转换。

const blob = new Blob('xxx', {type: 'test/plain'})
const file = new File([blob], 'blob', {type: blob.type});
复制代码

4、File 转 Blob

const file = new File(["xxx"], "file", { type: "text/plain" }); 
const blob = new Blob([file], { type: file.type });
复制代码

5、Blob/File 转 base64

Blob和File对象都需要借助 FileReader 来实现 base64的转换。

const blob = new Blob('xxxx');
conat file = new File('xxx);
const fileReader = new FileReader();
fileReader.onload = function(){
    const url = fileReader.result;
}
fileReader.readAsDataURL(blob);
fileReader.readAsDataURL(file);
复制代码

6、base64 转 Blob/File

base64 要转 Blob 或者 File 需要借助视图工具,使用视图工具来保存 base64 解码之后的二进制数据。

// url = "data:image/png;base64,xxxx"
const arr = url.split(',');
const type = arr[0].match(/^:(.?)*;$/)
const data = atob(arr[1]);
const len = data.length;
const u8arr = new Uint8Array(len);
for(let i=0;i<len;i++) {
    u8arr[i] = data.charCodeAt(i);
}
const blob = new Blob([u8arr], {type})
const file = new File([u8arr], {type})
复制代码

7、Buffer 转 ArrayBuffer

const arrayBuffer = Buffer.buffer;
复制代码

8、ArrayBuffer 转 Buffer

const arrayBuffer = new ArrayBUffer()
const u8arr = new Uint8Array(arrayBuffer);
const buffer1 = Buffer.from(arrayBuffer);
const buffer2 = BUffer.from(u8arr.buffer);
复制代码

三、应用场景

1、图片

图片一般有两种使用方式,一个是 Data URL(base64格式),一个是Object URL。

  1. Data URL

image.png 图中框选的就是图片的 Data URL 形式,它由四个部分组成。

data:[<mediatype>][;base64],<data>
复制代码

mediatype 表示文件的类型,是个MIME类型的字符串,比如 "image/jpeg"表示 JPEG 类型的图片,在开发过程中,通常为了减少 HTTP 请求的数量,如果图片比较小我们通常都会使用 Data URL 的形式将图片内嵌到 HTML 中,但是当图片体积较大时,这种方式就不太适合了,因为经过 base64 编码后生成的字符串将会非常的大,这会增加 HTML 页面的大小,从而减小页面加载的速度。

  1. Object URL

image.png 图中框选的就是图片 Object URL 形式,这种一般出现在从服务器动态获取图片数据并展示的情况中。

blob:null/18c3b021-0e0c-4bf9-81fd-d8d0cc5ebbc9
复制代码

Object URL 是一种伪协议,也被称为 Blob URL,在浏览器中我们可以使用 window.URL.createObjectURL 的方式来创建 Object URL,这个方法接收一个 Blob 对象,并返回一个唯一的 URL。

其实当我们调用

window.URL.createObjectURL

时浏览器会为每个 URL 生成一个 URL 到 Blob 对象的一个映射,因此通过这种方式生成的 URL 相比于 base64 是小了很多。但是有优点就有缺点,

createObjectURL

这种方式就是利用了浏览器的缓存效果,缓存就需要内存,因为我们创建的 URL 的生命周期是和创建它的窗口中的 document 绑定的,浏览器只有在 document 卸载的时候才会自动释放它们。针对这个问题我们可以调用

window.URL.revokeObjectURL

方法,从内部映射中删除引用并释放内存。

2、文件分片上传

上面我们讲过 File 中继承了 Blob 对象上的方法,所以针对大文件上传,我们可以使用 slice 方法对文件进行切割,然后分片上传。

const file = new File([上传文件], 'file.txt')
const size = 10000;
const uploadFile = async () {
    for(let i=0;i<file.size;i+=size) {
        const chunk = file.slice(i, i+size+1);
        // 使用form-data的形式上传二进制文件
        const formData = new FormData();
        formData.append("data", chunk);
        await axios.post({url,body: formData});
    }
}
复制代码

3、视频

对于音频视频来说也可以使用 Object URL 来进行播放,比如b站上的视频就是采用的这种方式:

image.png 将生成的 URL 赋给 video 播放器。但是对于一些比较长的视频来说我们不可能一次性将所得数据加载进来,而是应该采用分段的形式一段一段进行播放。

现在我们知道 video 标签的 src 是指向我们生成的 Object URL,每一段 URL 都指向对应的一段视频数据,假如我们要实现分段式播放首先想到的就是每次请求一段数据之后去生成对应的 URL 然后等上一段视频播放完了就可以将 src 修改为下一段视频的 URL,但是这种方式很难实现视频的无缝衔接播放,为了实现这一功能官方给我们提供了

MediaSource

这一 API,他作为一个媒体数据容器可以和 HTMLMediaElement 进行绑定,简单来说就是播放器播放的视频数据可以从这个容器中获取,而我们又可以不断的往这个容器中添加新的数据。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <input type="file" onchange="loadImage(event)">
    <video controls></video>
</body>
</html>
<script>
    function loadImage(event) {
      let preview = document.querySelector('img');
      let reader = new FileReader();

      reader.addEventListener('load', function () {
        play(reader.result);
      }, false);

      reader.readAsArrayBuffer(event.target.files[0]);
    }
    function play(buffer){
        var video = document.querySelector('video');
        var mediaSource = new MediaSource();
        video.src = window.URL.createObjectURL(mediaSource);
        mediaSource.addEventListener('sourceopen', function() {
            var mediaSource = this;
            const mimeCodec = 'video/mp4; codecs="avc1.64003c, mp4a.40.2"'; 
            var sourceBuffer = mediaSource.addSourceBuffer(mimeCodec);
            sourceBuffer.addEventListener('updateend', function (_) {
                video.play();
                // mediaSource.endOfStream();
            });
            sourceBuffer.appendBuffer(buffer);
        });
    }
</script>
复制代码

这里指加载了一段视频,如果有多段数据可以通过

appendBuffer

不断的往容器中添加数据。

往期好文

深入理解React-router6实现原理


本文转载自: https://blog.csdn.net/weixin_46721670/article/details/127058251
版权归原作者 方方|灿歌 所有, 如有侵权,请联系我们删除。

“最详细的前端二进制数据流”的评论:

还没有评论