二进制及文件

Blob

什么是 Blob

binary large object 二进制大对象

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

简单来说,Blob 对象就是一个不可修改的二进制文件。

创建 Blob

API

1
new Blob(array, options);

array:由 ArrayBufferArrayBufferViewBlobDOMString 等对象构成的,将会被放进 Blob

options:配置项

  • type:默认值为 “”,表示将会被放入到 blob 中的数组内容的 MIME 类型
  • endings:默认值为”transparent“,用于指定包含行结束符\n的字符串如何被写入,不常用。

实例

1
2
3
4
const blob = new Blob(["Hello World"], {type: "text/plain"});

console.log(blob.size); // 11
console.log(blob.type); // "text/plain"

这里可以成为动态文件创建,其正在创建一个类似文件的对象。这个 blob 对象上有两个属性:

  • size:Blob对象中所包含数据的大小(字节);
  • type:字符串,认为该Blob对象所包含的 MIME 类型。如果类型未知,则为空字符串。

字符串”Hello World”是 UTF-8 编码的,因此它的每个字符占用 1 个字节。

使用

可以使用 URL.createObjectURL() 方法将将其转化为一个 URL,并在 Iframe 中加载:

1
<iframe></iframe>
1
2
3
4
5
const iframe = document.getElementsByTagName("iframe")[0];

const blob = new Blob(["Hello World"], {type: "text/plain"});

iframe.src = URL.createObjectURL(blob);

Blob 切片

Blob 对象内置了 slice() 方法用来将 blob 对象分片。

我们无法直接在 Blob 中更改数据,但我们可以通过 slice 获得 Blob 的多个部分,从这些部分创建新的 Blob 对象,将它们组成新的 Blob。类似 js 字符串。

API

1
const blob = instanceOfBlob.slice([start [, end [, contentType]]]};

start:设置切片的起点,即切片开始位置。默认值为 0,这意味着切片应该从第一个字节开始;

end:设置切片的结束点,会对该位置之前的数据进行切片。默认值为blob.size

contentType:设置新 blob 的 MIME 类型。如果省略 type,则默认为 blob 的原始值。

实例

1
2
3
4
5
6
7
const iframe = document.getElementsByTagName("iframe")[0];

const blob = new Blob(["Hello World"], {type: "text/plain"});

const subBlob = blob.slice(0, 5);

iframe.src = URL.createObjectURL(subBlob);

页面显示 Hello

File

什么是 File

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

实际上,File 对象是特殊类型的 Blob,且可以用在任意的 Blob 类型的 context 中。Blob 的属性和方法都可以用于 File 对象。

File 对象中只存在于浏览器环境中,在 Node.js 环境中不存在。

在 JavaScript 中,主要有两种方法来获取 File 对象:

  • <input> 元素上选择文件后返回的 FileList 对象;
  • 文件拖放操作生成的 DataTransfer 对象;

需要注意的是:前端无法像原生 APP 一样直接操作本地文件,否则的话打开个网页就能把用户电脑上的文件偷光了,只能通过用户触发。

input

1
<input type="file" id="fileInput" multiple="multiple">

这里给 input 标签添加了三个属性:

  • type="file":指定 input 的输入类型为文件;
  • id="fileInput":指定 input 的唯一 id;
  • multiple="multiple":指定 input 可以同时上传多个文件;

下面来给 input 标签添加 onchange 事件,当选择文件并上传之后触发:

1
2
3
4
5
const fileInput = document.getElementById("fileInput");

fileInput.onchange = (e) => {
console.log(e.target.files);
}

当点击上传文件时,控制台就会输出一个 FileList 数组,这个数组的每个元素都是一个 File 对象,一个上传的文件就对应一个 File 对象:

每个 File 对象都包含文件的一些属性,这些属性都继承自 Blob 对象:

  • lastModified:引用文件最后修改日期,为自1970年1月1日0:00以来的毫秒数;
  • lastModifiedDate:引用文件的最后修改日期;
  • name:引用文件的文件名;
  • size:引用文件的文件大小;
  • type:文件的媒体类型(MIME);
  • webkitRelativePath:文件的路径或 URL。

通常,我们在上传文件时,可以通过对比 size 属性来限制文件大小,通过对比 type 来限制上传文件的格式等。

文件拖放

拖放区域用于响应放置操作并从放置的项目中提取信息。这些是通过 ondropondragover 两个 API 实现的。

例子

定义放置区域

1
<div id="drop-zone"></div>

添加事件处理程序

1
2
3
4
5
6
7
8
9
10
11
const dropZone = document.getElementById("drop-zone");

dropZone.ondragover = (e) => {
e.preventDefault();
}

dropZone.ondrop = (e) => {
e.preventDefault();
const files = e.dataTransfer.files;
console.log(files)
}

当拖放文件到拖放区域时,控制台就会输出一个 FileList 数组,该数组的每一个元素都是一个 File 对象。这个 FileList 数组是从事件参数的 dataTransfer 属性的 files 获取的.

这里得到的 File 对象和通过 input 标签获得的 File 对象是完全一样的。

FileReader

什么是 FileReader

异步 API,用于读取文件并提取内容以供进一步使用

FileReader 可以将 Blob 读取为不同的格式。

创建实例

1
const reader = new FileReader();

这个对象常用属性如下:

  • error:表示在读取文件时发生的错误;
  • result:文件内容。该属性仅在读取操作完成后才有效,数据的格式取决于使用哪个方法来启动读取操作。
  • readyState:表示 FileReader 状态的数字
常量名 描述
EMPTY 0 还没有加载任何数据。
LOADING 1 数据正在被加载。
DONE 2 已完成全部的读取请求。

读取文件

  • readAsArrayBuffer():读取指定 Blob 中的内容,完成之后,result 属性中保存的将是被读取文件的 ArrayBuffer 数据对象;
  • FileReader.readAsBinaryString():读取指定 Blob 中的内容,完成之后,result 属性中将包含所读取文件的原始二进制数据;
  • FileReader.readAsDataURL():读取指定 Blob 中的内容,完成之后,result 属性中将包含一个data: URL 格式的 Base64 字符串以表示所读取文件的内容。
  • FileReader.readAsText():读取指定 Blob 中的内容,完成之后,result 属性中将包含一个字符串以表示所读取的文件内容。

上面这些方法都接受一个要读取的 blob 对象作为参数,读取完之后会将读取的结果放入对象的 result 属性中,只是结果的形式不同。

事件处理

FileReader 对象常用的事件如下:

  • abort:该事件在读取操作被中断时触发;
  • error:该事件在读取操作发生错误时触发;
  • load:该事件在读取操作完成时触发;
  • progress:该事件在读取 Blob 时触发。

当然,这些方法可以加上前置 on 后在HTML元素上使用,比如onloadonerroronabortonprogress。除此之外,由于FileReader对象继承自EventTarget,因此还可以使用 addEventListener() 监听上述事件。

例子

上传图片并显示在页面上
1
2
3
<input type="file" id="fileInput" />

<img id="preview" />
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const fileInput = document.getElementById("fileInput");
const preview = document.getElementById("preview");
const reader = new FileReader();

// 上传文件时触发
fileInput.onchange = (e) => {
// 读取为 dataurl 的形式
reader.readAsDataURL(e.target.files[0]);
};

// reader.readAsDataURL 处理完毕后触发
reader.onload = (e) => {
preview.src = e.target.result;
console.log(e.target.result);
};
上传大文件监控进度
1
2
3
4
5
6
7
8
9
const reader = new FileReader();

reader.onprogress = (e) => {
// progress 事件提供了两个属性:loaded(已读取量)和total(需读取总量)。
if (e.loaded && e.total) {
const percent = (event.loaded / event.total) * 100;
console.log(`上传进度: ${Math.round(percent)} %`);
}
});

ArrayBuffer

上面4行是 TypedArray,最后一行是 DataView

什么是 ArrayBuffer

在处理文件(创建、上传、下载),尤其是图像和音频时,经常会遇到二进制数据。

ArrayBuffer 对象用来表示对固定长度连续内存空间的引用

ArrayBuffer 本身就是一个黑盒,不能直接读写所存储的数据,需要借助以下视图对象来读写:

  • TypedArray:用来生成内存的视图,通过9个构造函数,可以生成9种数据格式的视图。
  • DataViews:用来生成内存的视图,可以自定义格式和字节序。

TypedArray视图和 DataView视图的区别主要是字节序,前者的数组成员都是同一个数据类型,后者的数组成员可以是不同的数据类型。

视图对象本身并不存储任何东西。它是一副“眼镜”,透过它来解释存储在 ArrayBuffer 中的字节。

使用

new ArrayBuffer()

1
new ArrayBuffer(bytelength)

ArrayBuffer()构造函数可以分配指定字节数量的缓冲区,其参数和返回值如下:

  • 参数:它接受一个参数,即 bytelength,表示要创建数组缓冲区的大小(以字节为单位。);
  • 返回值:返回一个新的指定大小的ArrayBuffer对象,内容初始化为0。

ArrayBuffer.prototype.byteLength

只读属性,表示 ArrayBuffer 的 byte 的大小

ArrayBuffer 构造完成时生成,不可改变

1
2
const buffer = new ArrayBuffer(16); 
console.log(buffer.byteLength); // 16

ArrayBuffer.prototype.slice()

截取 ArrayBuffer 实例,它返回一个新的 ArrayBuffer ,它的内容是这个 ArrayBuffer 的字节副本。

1
2
const buffer = new ArrayBuffer(16); 
console.log(buffer.slice(0, 8)); // 16

这个方法实际上有两步操作,首先会分配一段指定长度的内存,然后拷贝原来ArrayBuffer对象的置顶部分。

ArrayBuffer.isView()

这个方法就是用来判断参数是否是 TypedArray 实例或者 DataView 实例

1
2
3
4
5
const buffer = new ArrayBuffer(16);
ArrayBuffer.isView(buffer) // false

const view = new Uint32Array(buffer);
ArrayBuffer.isView(view) // true

TypedArray

什么是 TypedArray

TypedArray 对象一共提供 9 种类型的视图,每一种视图都是一种构造函数

因此,一个 16 字节 ArrayBuffer 中的二进制数据可以解释为 16 个“小数字”,或 8 个更大的数字(每个数字 2 个字节),或 4 个更大的数字(每个数字 4 个字节),或 2 个高精度的浮点数(每个数字 8 个字节)。

这些构造函数生成的对象统称为 TypedArray 对象。它们和正常的数组很类似,都有length 属性,都能用索引获取数组元素,所有数组的方法都可以在 TypedArray 上面使用

那类型化数组和数组有什么区别呢?

  • 类型化数组的元素都是连续的,不会为空;
  • 类型化数组的所有成员的类型和格式相同;
  • 类型化数组元素默认值为 0;
  • 类型化数组本质上只是一个视图层,不会存储数据,数据都存储在更底层的 ArrayBuffer 对象中。

使用

new TypedArray()

1
2
3
4
new Int8Array(length);
new Int8Array(typedArray);
new Int8Array(object);
new Int8Array(buffer [, byteOffset [, length]]);

BYTES_PER_ELEMENT

每种视图的构造函数都有一个 BYTES_PER_ELEMENT 属性(实例上也有),表示这种数据类型占据的字节数:

1
2
3
4
5
6
7
8
Int8Array.BYTES_PER_ELEMENT // 1
Uint8Array.BYTES_PER_ELEMENT // 1
Int16Array.BYTES_PER_ELEMENT // 2
Uint16Array.BYTES_PER_ELEMENT // 2
Int32Array.BYTES_PER_ELEMENT // 4
Uint32Array.BYTES_PER_ELEMENT // 4
Float32Array.BYTES_PER_ELEMENT // 4
Float64Array.BYTES_PER_ELEMENT // 8

TypedArray.prototype.buffer

TypedArray 实例的 buffer 属性会返回内存中对应的 ArrayBuffer对象,只读属性。

1
2
const a = new Uint32Array(8);
const b = new Int32Array(a.buffer);

TypedArray.prototype.slice()

TypeArray 实例的 slice方法可以返回一个指定位置的新的 TypedArray实例。

1
2
const view = new Int16Array(8);
console.log(view.slice(0 ,5));

byteLength 和 length

  • byteLength:返回 TypedArray 占据的内存长度,单位为字节;
  • length:返回 TypedArray 元素个数;
1
2
3
const view = new Int16Array(8);
view.length; // 8
view.byteLength; // 16

DataView

什么是 DataView

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

DataView视图提供更多操作选项,而且支持设定字节序。

在设计目的上,ArrayBuffer对象的各种TypedArray视图,是用来向网卡、声卡之类的本机设备传送数据,所以使用本机的字节序就可以了;而DataView视图的设计目的,是用来处理网络设备传来的数据,所以大端字节序或小端字节序是可以自行设定的。

使用

new DataView()

1
new DataView(buffer [, byteOffset [, byteLength]])

buffer:一个已经存在的 ArrayBuffer 对象,DataView 对象的数据源。

byteOffset:可选,此 DataView 对象的第一个字节在 buffer 中的字节偏移。如果未指定,则默认从第一个字节开始。

byteLength:可选,此 DataView 对象的字节长度。如果未指定,这个视图的长度将匹配 buffer 的长度。

buffer、byteLength、byteOffset

DataView实例有以下常用属性:

  • buffer:返回对应的ArrayBuffer对象;
  • byteLength:返回占据的内存字节长度;
  • byteOffset:返回当前视图从对应的ArrayBuffer对象的哪个字节开始。

读取内存

getInt8:读取1个字节,返回一个8位整数。

getUint8:读取1个字节,返回一个无符号的8位整数。

getInt16:读取2个字节,返回一个16位整数。

getUint16:读取2个字节,返回一个无符号的16位整数。

getInt32:读取4个字节,返回一个32位整数。

getUint32:读取4个字节,返回一个无符号的32位整数。

getFloat32:读取4个字节,返回一个32位浮点数。

getFloat64:读取8个字节,返回一个64位浮点数。

参数都是一个字节序号,表示开始读取的字节位置

1
2
3
4
5
6
7
8
9
10
11
const buffer = new ArrayBuffer(24);
const view = new DataView(buffer);

// 从第1个字节读取一个8位无符号整数
const view1 = view.getUint8(0);

// 从第2个字节读取一个16位无符号整数
const view2 = view.getUint16(1);

// 从第4个字节读取一个16位无符号整数
const view3 = view.getUint16(3);

写入内存

DataView 实例提供了以下方法来写入内存,它们都接受两个参数,第一个参数表示开始写入数据的字节序号,第二个参数为写入的数据:

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

Object URL / Blob URL

什么是 Object URL

用来表示 File Object 的 URL

image.png

Blob URL/Object URL 是一种伪协议,允许将 Blob 和 File 对象用作图像、二进制数据下载链接等的 URL 源。

使用

URL.createObjectURL()

将 File 对象转为 URL

input 上传图片,在 img 中显示

1
2
3
<input type="file" id="fileInput" />

<img id="preview" />
1
2
3
4
5
6
7
8
const fileInput = document.getElementById("fileInput");
const preview = document.getElementById("preview");

fileInput.onchange = (e) => {
// URL.createObjectURL() 将File 对象转化为一个 URL, 然后赋值给 img 的 src
preview.src = URL.createObjectURL(e.target.files[0]);
console.log(preview.src);
};

image.png

URL.revokeObjectURL

虽然浏览器会在文档卸载时自动释放 Data URL,但为了提高性能,我们应该使用createObjectURL()来手动清除 URL 释放内存。

1
2
3
const objUrl = URL.createObjectURL(new File([""], "filename"));
console.log(objUrl);
URL.revokeObjectURL(objUrl);

Data URL / base64

Data URL 即 Data As URL。所以, 如果资源过大,地址便会很长。

base64 编码将二进制数据表示为一个由 0 到 64 的 ASCII 码组成的字符串,非常安全且“可读“。

为什么要将图片转为base64编码而不是直接传图片的url地址

  1. 减少http请求数

将图片转为base64编码放在html或css中,可以随着html的下载同时下载,不需要再请求服务器

  1. 先于内容加载

将编码成base64的图片置于css中,而css代码又一般是在html头部加载的,其加载顺序先于下面内容。

  1. 可以像单独图片一样使用,而且对于同一张图片应用地方多,可以放入公共common块中

使用前提

被base64编码的图片要足够小的尺寸:因为将图片生成的base64字符串编码一般而言都会比原文件大一些,即使能被gzip压缩,但是css样式代码中要写一大版的base64编码字符难看,消耗的是css文件体积

使用Base64也不代表性能优化

使用Base64减少一个图片的http请求的同时Css文件就会付出体积增大的代价

文件体积增大意味着CRP的阻塞,同时页面解析CSS生成的CSSOM时间增加

Object URL 和 Data URL 的区别

  • blob显示的形式blob:域名/e61c67e3-df3a-453a-8f41-df740c1f5faf ,dataURL的显示形式data:image/jpeg;base64,/9j/4AAQ...
  • Blob URL的长度一般比较短,但Data URL因为直接存储图片base64编码后的数据,往往很长,如上图所示,浏览器在显示Data URL时使用了省略号(…)。当显式大图片时,使用Blob URL能获取更好的可能性。
  • Blob URL可以方便的使用XMLHttpRequest获取源数据(xhr.responseType = 'blob')。对于Data URL,并不是所有浏览器都支持通过XMLHttpRequest获取源数据的
  • Blob URL 只能在当前应用内部使用,把Blob URL复制到浏览器的地址栏中,是无法获取数据的。Data URL相比之下,就有很好的移植性,你可以在任意浏览器中使用。
  • Blob URL除了可以用作图片资源的网络地址,Blob URL也可以用作其他资源的网络地址,例如html文件、json文件等,为了保证浏览器能正确的解析Blob URL返回的文件类型,需要在创建Blob对象时指定相应的type

Blob 转换为 object url

  • 如果介意内存,我们需要撤销(revoke)它们
  • 直接访问 Blob,无需“编码/解码”

Blob 转换为 data url

  • 无需撤销(revoke)任何操作。
  • 对大的 Blob 进行编码时,性能和内存会有损耗。

总的来说,小图片建议用 data url,可以减少 http 请求。大图片建议用 blob url,因为 data url 过大。

格式转换

ArrayBuffer - Blob

1
const blob = new Blob([new Uint8Array(buffer, byteOffset, length)]);

ArrayBuffer - base64

1
const base64 = btoa(String.fromCharCode.apply(null, new Uint8Array(arrayBuffer)));

base64 - blob

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const base64toBlob = (base64Data, contentType, sliceSize) => {
const byteCharacters = atob(base64Data);
const byteArrays = [];

for (let offset = 0; offset < byteCharacters.length; offset += sliceSize) {
const slice = byteCharacters.slice(offset, offset + sliceSize);

const byteNumbers = new Array(slice.length);
for (let i = 0; i < slice.length; i++) {
byteNumbers[i] = slice.charCodeAt(i);
}

const byteArray = new Uint8Array(byteNumbers);
byteArrays.push(byteArray);
}

const blob = new Blob(byteArrays, {type: contentType});
return blob;
}

blob - ArrayBuffer

1
2
3
4
5
6
7
8
function blobToArrayBuffer(blob) { 
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result);
reader.onerror = () => reject;
reader.readAsArrayBuffer(blob);
});
}

blob - base64

1
2
3
4
5
6
7
function blobToBase64(blob) {
return new Promise((resolve) => {
const reader = new FileReader();
reader.onloadend = () => resolve(reader.result);
reader.readAsDataURL(blob);
});
}

blob - Object URL

1
const objectUrl = URL.createObjectURL(blob);

总结

ArrayBuffer / TypedArray / DataView 是二进制数据,其中 ArrayBuffer 是一段固定长度的内存空间,不能直接修改,需要通过 TypedArray 或 DataView 来改写

Blob 表示 具有类型的二进制数据,可以通过 类型+ArrayBuffer 生成

Blob 可以通过 URL.createObjectURL(blob) 直接转化成 Object URL

FileReader 是异步API,可以将 Blob 读取为不同的形式,包括 ArrayBuffer / Base64 / Text / BinaryString

File 继承于 Blob,可以理解为 Blob 的子类

img

从图中我们不难看出如何修改一个已有文件,利用 FileReader API 将 blob 转为 ArrayBuffer,利用 DataView 或 TypedArray 进行修改,再 new Blob 生成新的文件。

参考

https://juejin.cn/post/7148254347401363463

https://zhuanlan.zhihu.com/p/147664305

https://zh.javascript.info/binary


二进制及文件
http://example.com/2022/11/02/二进制及文件/
Author
John Doe
Posted on
November 2, 2022
Licensed under