JS

JS-blob对象

Posted by Youzi on January 26, 2021

JS-blob 对象

记录 JS 对象中比较特别的,经常在文件上传中运用的 blob 对象;

前置任务

在了解 blob 之前先来看下密不可分的 JS 二进制类型的数据分类;

关于缓冲区

由于在 JS 里用 Array 这种数据类型存储大字节流很低效,且浪费内存空间,所以在node里创建了一种数据缓冲区来操作二进制数据;这是一块物理内存(相对于直接读写硬盘来说更快),用于在数据从一个位置移动到另一位置时使用的临时存储空间,JS 解释器借助缓冲区读写行。这个概念其实挺像RAM | cache之间的关系的,我们需要一块速度更快,更小的存储空间来对数据流进行快速的读写切换,毕竟一次性加载特别大的文件(或者大量数据流)是很费时的;

buffer

用过node的同学应该知道,这是node用于IO 操作的时候,比如读取文件,接收请求,通过 buffer 对象创建一个存放二进制数据的缓存区域,对数据进行整合,一个 buffer 对象类似于一个整型数组,对于v8引擎来说是堆内存之外的一块原始内存。

ArrayBuffer、ArrayBufferView

ArrayBuffer

ArrayBuffer表示固定长度的二进制数据的原始缓冲区,其构造函数是分配一段连续的内存区域供数据存放,所以对于高密度的访问(如音频数据)操作,比 Array 快,其存在的价值在于作为数据源提前写入到内存里,长度固定,感觉有点类似 C 语言里的malloc函数,分配一块固定的内存区域;

对于二进制数据,无非就是读写;读取可以通过FileReader将文件对象转换成ArrayBuffer对象,实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<input type="file" name="" id="f" onchange="fun()" />
<script>
  function fun() {
    let file = document.querySelector('#f').files[0];
    console.warn(file);
    let arrBuf;
    const reader = new FileReader();
    reader.onload = e => {
      console.warn(e);
      // arrBuf就是ArrayBuffer类型数据
      arrBuf = e.target.result;
      console.warn(arrBuf);
    };
    reader.readAsArrayBuffer(file);
  }
</script>

ArrayBuffer对象是只读的,初始化后大小固定,只能借助TypedArray | DataView写入;

ArrayBuffer对象有一个属性byteLength用于表示大小,方法slice用于截取

ArrayBufferView

由于ArrayBuffer本身的实例不提供读写方法,所以创建了这个对象作为ArrayBuffer的视图,指定了原始二进制数据的基本处理单元,通过ArrayBufferView来读取实际的内容,上面说的TypedArray | DataView是该对象的实例。

TypedArray

类型化数组是 JS 中新出现的概念,为了访问原始的二进制数据,本质和ArrayBuffer一样,只是具备了读写功能;而TypedArray本身不存在在全局属性中,也没有对应的构造函数,有的都是以下列表中的类型数组;包含了所有相关属性和方法 TypedArray 相关

1
2
3
4
5
6
7
8
9
Int8Array();
Uint8Array();
Uint8ClampedArray();
Int16Array();
Uint16Array();
Int32Array();
Uint32Array();
Float32Array();
Float64Array();

构造函数参数:

  • length:缓冲区大小;
  • typedArray:当传入以上任意的类型化数组对象时,会复制一份新的,但是在复制之前会进行相应的类型转换,比如原来是Int8Array,新的是Int16Array,数组中的每一项都进行了转换,并且length属性不变。
  • object,传入对象就像通过TypedArray.from()创建一个新的类型化数组一样;
  • buffer,byteOffset,length,三个参数可以创建一个类型化数组的视图,后两个参数表示下标偏移量和偏移距离;

由于普通数组对象查找用的是 Hash 方式,而类型数组是直接访问内存,访问速度来说比普通数组要快;而且处理二进制数据的能力更强。这个类型的对象基本拥有数组的所有属性和方法

TypedArrar <-> ArrayBuffer互相转换:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 创建ArrayBuffer对象,只读对象
const arrbuf = new ArrayBuffer(10);
// 利用TypedArray构造函数转换
const int8 = new Int8Array(arrbuf);
// 通过下标
int8[0] = 20;
// 访问元素,支持数组的部分方法
int8.forEach(el => {
  console.warn(el);
});
const int16 = new Int16Array(20);
// 通过set方法批量设值,第二个参数是偏移量
int16.set(int8);
int16.set(int8, 10);

简单应用,JS 获取音频,进行拼接;参考的是这个思路fetch请求音频资源 -> ArrayBuffer -> TypedArray -> 拼接成一个 TypedArray -> ArrayBuffer -> Blob -> Object URL

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
<audio id="audio" controls>audio</audio>
<script>
  async function audio(params) {
    const au = document.querySelector('#audio');
    let tmpStream, stream;
    let p1, p2;
    p1 = fetch('./audio/vue.mp3').then(async res => {
      // 转换成ArrayBuffer对象
      return await res.arrayBuffer();
    });
    p2 = fetch('./audio/function.mp3').then(async res => {
      return await res.arrayBuffer();
    });
    let streamLen = 0;
    await Promise.all([p1, p2])
      .then(async result => {
        // console.warn(result);
        tmpStream = result.map(el => {
          // console.warn(el, el.arrayBuffer());
          // streamLen += el.length;
          // let ab = await el.arrayBuffer();
          // 转换成TypedArray对象
          let u8 = new Uint8Array(el);
          // 得到音频段落的总长度
          streamLen += u8.length;
          return u8;
        });
      })
      .catch(err => {});
    console.warn(p1, p2);
    /* streamLen = tmpStream.reduce((prev, curr) => {
      return prev + curr.length;
    }, streamLen); */
    // 新的TypedArray对象,内存空间是音频总长度
    stream = new Uint8Array(streamLen);
    let tmpCount = 0;
    tmpStream.forEach((el, idx) => {
      // 调用set方法,拼接几段音频,注意第二个参数的使用
      stream.set(el, tmpCount);
      tmpCount += el.length;
      /* el.forEach(e => {
          }); */
    });
    console.warn(stream);
    // 转换成blob对象,参数可以设置该blob对象的MIME类型,我这里是mp3
    let blob = new Blob([stream], {
      type: 'audio/mpeg'
    });
    // 转换成url对象,可以直接赋值给src
    let url = URL.createObjectURL(blob);
    au.src = url;
  }
  window.onload = () => {
    audio();
  };
</script>

DataView

DataView对象可以在 ArrayBuffer 任意位置读取和存储不同类型的二进制数据。

其构造函数语法为:buffer,byteOffset,byteLength三个参数可以创建一个类型化数组的视图,后两个参数表示下标偏移量和偏移距离;

对象的属性也是上述的三个参数,且是只读属性,在创建时就固定了。对象的读写方法分别是getInt8 | setInt8,以及其他类型,入参和数组下标类似,set方法额外传入元素值。来看例子:

1
2
3
4
5
6
7
let ab = new ArrayBuffer(10);
let dv1 = new DataView(ab);
let dv2 = new DataView(ab);
dv1.setInt16(0, 257);
dv2.getInt16(0); // 257
dv2.getInt8(0); // 1
dv2.getInt8(1); // 1

blob

blob用来表示二进制类型的对象,是不可变的、原始数据的类文件对象,MDN 关于 blob 对象的介绍

构造函数:

1
new Blob(blob, options);
  • blob:数组类型,可以存放任意多个 ArrayBuffer, ArrayBufferView, Blob 或者 DOMString(会编码为 UTF-8),将它们连接起来构成 Blob 对象的数据。
  • options:选项参数,设置blob对象的属性,可选值为:
    • type:存放到第一个参数的MIME类型,默认""
    • endings:指定包含行结束符\n如何被写入;两个可选项,transparent默认值表示不改变原来的结束符,native表示根据操作系统来决定;

看几个实例,上面提到了blob构造函数的第一个参数,是一个数组类型,可以是几个类型的;DOMString是一个编码为UTF-16的字符串,因为 JS 已经使用了这样的字符串,所以会被直接映射到一个String类型。

1
2
3
4
5
6
7
8
9
10
// DOMstring对象
let blob = new Blob(['<p>test</p>'], {
  type: 'text/html'
});
// ArrayBuffer
let arrbuf = new ArrayBuffer(8);
let blob1 = new Blob([arrbuf]);
// ArrayBufferView
let typed = new Int8Array(arrbuf);
let blob2 = new Blob([typed]);

Blob对象有两个属性,size | type含义显而易见,都是只读属性;

方法有slice | stream | text | arrayBuffer,第一个是类似数组的 slice 方法,可以用于对较大的二进制数据进行切片,后三个都是读取方法;

上传文件的例子,在input里选中的文件,都是保存为二进制形式的,可以利用formData,将二进制文件作为参数传到服务器上;关于formdatahttps://developer.mozilla.org/zh-cn/docs/Web/API/FormData

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<input type="file" name="" id="f" onchange="fun()" accept="image/*" />
<script>
  function fun() {
    let file = document.querySelector('#f').files[0];
    console.warn(file);
    const formData = new FormData();
    formData.append('files', file);
    axios
      .post('/upload', formData, {
        headers: {
          'Content-Type': 'multipart/form-data'
        }
      })
      .then(result => {})
      .catch(err => {});
  }
</script>

File 类

File对象是一种特殊的blob对象,继承了所有 blob 属性和方法,也可以直接作为二进制文件上传;

file对象可以从上面的例子,input标签中获取,也可以通过拖拽生成的DataTransfer对象;

通过type='file'的例子在上面已经有了,现在来写一个通过拖拽生成的file对象的例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<style lang="">
  .drag {
    height: 100px;
    width: 100px;
    border: 5px solid #000;
  }
</style>
<div class="drag" ondrop="drop(event)" ondragover="allowDrop(event)"></div>
<script>
  function allowDrop(ev) {
    ev.preventDefault();
  }
  function drop(ev) {
    ev.preventDefault();
    const files = ev.dataTransfer.files;
    console.log(files);
    console.log(files instanceof FileList);
  }
</script>

file对象的属性包括lastModified | name | size | type,都是很好理解的属性;方法没有特有的实例方法,都继承了blob对象的方法;

fileReader

由于file对象拿到的也是blob二进制的内容,所以 JS 还提供了fileReader对象来操作它们,可以把二进制内容转换成其他格式的数据;

  • readAsText:将blob对象转化成文本字符串;
  • readAsArrayBuffer:转化成 ArrayBuffer 对象;
  • readAsDataURL:转化成base64格式的DataURL

看个图片上传并预览的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<input type="file" name="" id="f" onchange="fun()" accept="image/*" />
<script>
  function fun() {
    let file = document.querySelector('#f').files[0];
    console.warn(file);
    let arrBuf;
    const reader = new FileReader();
    reader.onload = e => {
      console.warn(e);
      arrBuf = e.target.result;
      console.warn(arrBuf);
      let blob = new Blob([arrBuf], {
        type: 'image/*'
      });
      let src = URL.createObjectURL(blob);
      let img = document.createElement('img');
      img.setAttribute('src', src);
      document.body.appendChild(img);
      src = null;
      img.onload = function () {
        window.URL.revokeObjectURL(this.src);
      };
    };
    // 也可以直接调用这个方法,得到base64格式,可以直接赋值给图片的src属性,不需要像ArrayBuffer那样转格式
    // reader.readAsDataURL(file);
    reader.readAsArrayBuffer(file);
  }
</script>

上面的例子用了readAsArrayBuffer方法,得到的是ArrayBuffer对象,所以要转换格式为URL object,也可以直接readAsDataURL,就省去了一步转换;

blobURL

blobURL是一种伪协议,只能由浏览器在内部生成,我们知道script/img/video/iframe等标签的 src 属性和 background 的 url 可以通过 url 和 base64 来显示,我们同样可以把 blob 或者 file 转换为 url 生成 blobURL 来展示图像,blobURL 允许 Blob 和 File 对象用作图像,下载二进制数据链接等的 URL 源。base64 编码于解码

下载文件的例子,用的是二进制方式;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<button onclick="handleClick()">click</button>
<script>
  function handleClick() {
    let blob = new Blob(['test plain text'], {
      type: 'text/plain'
    });
    const elink = document.createElement('a');
    elink.download = 'filename';
    elink.style.display = 'none';
    elink.href = URL.createObjectURL(blob);
    document.body.appendChild(elink);
    elink.click();
    URL.revokeObjectURL(elink.href);
    document.body.removeChild(elink);
  }
</script>

核心属性和方法在https://developer.mozilla.org/zh-cn/docs/Web/API/URL

可以看到这种方式下,标签的 src 或者 href 的值会变成以bolb:开头的一串链接字符串。这个特殊的字符串就是 objectURL;下面介绍另一种特殊的链接;

dataURL

前面的例子中也有出现过,dataURL也是一个特殊的字符串,用于将小文件嵌入到HTML文档中,经常在小图片中用到,可以节省一次 HTTP 请求,其语法形如:

1
2
3
4
5
6
data:[<mediatype>][;base64],data

data:前缀;
mediatype:可选,MIME类型的字符串;
base64:可选,标志位;
data:数据本身

获取 dataURL 的方式共有三种,

  1. fileReader 的方法,readAsDataURL,将二进制数据转化成 base64 格式的 dataURL;
  2. JS 的编码/解码方法,atob | btoa,两者都针对 dataURL 中的 data 部分进行处理;
  3. canvas 提供的toDataURL方法,该方法可用于获取 canvas 绘制的内容;

DataURL 可以作为 URL 的替代,和 blobURL 一样可以直接给img/video标签的src属性赋值;

区别

看起来 blobURL 和 DataURL 类似,都可以直接通过浏览器地址栏访问,它们之间的区别是:

  1. blobURL 是唯一的字符串,即使每次传入相同的 blob 参数,生成的也是不同的 blobURL;但是 DataURL 只会随着 blob 参数变化;
  2. blobURL 不代表数据本身,数据存储在浏览器中,blobURL 只是访问它的 key,数据会一直有效直到关闭浏览器或手动清除;而 DataURL 是编码后的数据本身;所以如果把 blobURL 传递给服务器也是无法访问到数据的;浏览器关闭后可以再打开也可以访问到 DataURL,但是不能访问 blobURL;
  3. blobURL 长度较短,DataURL 由于会存储编码后的数据,一般比较长(而且由于其编码的特殊性,一般会比二进制数据长 33%),所以如果应用场景中图片较大,选择 blobURL 会比较好;
  4. blobURL 可以使用 XHR 获取源数据(需要设置响应类型为blob类型);对于 DataURL,并不是所有浏览器都支持 XHR 获取源数据;
  5. blobURL 也可以作为其他资源的网络地址,如 HTML,JSON 等,需要注意的是在创建 blob 对象时指定相应的 MIME type;
  6. DataURL 不会被浏览器缓存,导致了每次访问相同页面,资源都会被下载一次,如果图片被大量重复使用,那用这种方式不太合理。

canvas

canvas可以在页面中确定一个区域作为画布,调用 JS 方法就可以动态的在画布中绘制图形;

canvas元素对象的方法有:

  • toDataURL(type, options):以指定格式返回DataURL;其中type表示图片格式,默认image/pngoptions表示图片质量,在指定图片格式为 image/jpeg 或 image/webp 的情况下,可以从 0-1 选择图片质量,默认 0.92。
  • toBlob(callback, type, options):创建 blob 对象,用于展示 canvas 图片,默认图片类型是 image/png,分辨率是 96dpi;回调函数的参数是blob对象;
  • getImageData(x, y, width, height):返回ImageData对象,该对象复制了画布指定矩形的像素;参数分别是起始坐标,复制的区域大小;
  • putImageData(imgData,x,y,dirtyX,dirtyY,dirtyWidth,dirtyHeight):将指定的imgData图像数据放到画布上;

canvas 实际应用场景

当需要获取 canvas 内容时,可以用到前两个方法(用于签名,图片裁剪,压缩等),后两个方法可以用于图片灰度或者复制时使用;

各类型间转换

原文总结的类型转换图,挺好的,搬过来:

类型转换图

参考

https://mp.weixin.qq.com/s/OYJJnBfzyR3J4CYWGxgm6w