Binary Data In Frontend

Introduction

在日常的前端开发中,涉及到的工作内容大多和 UI 有关,比如说页面样式调整之类的基础工作。但是随着前端的发展,很多业务逻辑都放到了前端,例如文件生成和下载,图片处理等功能,这时候就涉及到了前端的二进制相关的内容,这篇文章从二进制相关内容和 API 出发,探究前端二进制相关的用途。

ArrayBuffer

ArrayBuffer 是一段基础的,固定长度的二进制数据,类似于其他语言的byte array。不能直接修改相关的内容,但是可以通过TypedArrayDataView进行读写。

1
2
3
4
5
const buffer = new ArrayBuffer(8);

const slicedBuffer = buffer.slice(0, 3);

console.log(buffer.byteLength, slicedBuffer.byteLength); // 8 3

如上所示,可以通过 new ArrayBuffer 创建新的 buffer, slice 截取 buffer 内容,slice 的操作类似于Array.prototype.slice

TypedArray

TypedArray 提供了多种类型用来处理和操作二进制数据,如下所示[1]
TypedArray

Uint8Array vs Uint8ClampedArray

Uint8Array 在处理小数位的时候采用的是向下取整,Uint8ClampedArray 则是采用四舍五入的形式取整,举个例子:

1
2
Uint8Array([0.9]); // 0
Uint8ClampedArray([0.9]); // 1

Uint8ClampedArray 当赋值在区间[0,255]之外,则只有取值为 0 或者 255 的两种情况,所以更多的运用于防止溢出的情况,例如增加图片的亮度[2]

overflow

TypedArray 的溢出处理方式简单来说就是抛弃溢出的位,然后按照视图类型进行解释。如下所示:

1
2
3
4
5
6
const uint8 = new Uint8Array(1);
uint8[0] = 256;
uint8[0]; // 0

uint8[0] = -1;
uint8[0]; // 255

256 转换为 2 进制为100000000,但是 unsigned int 只能最多保存 8 位,最开始的1被舍弃,结果为00000000,转换为十进制的数则为 0,所以最终结果为 0

负数转化为二进制则是将对应的正数做否运算,然后加1, 这里-1 对应的正整数为 1,转换为 unsigned int 为11111110,加1之后则为11111111,转换为十进制的数则为 255,所以最终结果为 255

TypedArray 的溢出可以总结为如下:

  1. 当前类型的最高值加 1 会被转换为当前类型的最低值
  2. 当前类型的最低值减一则会被转换为当前类型的最高值

DataView

DataView 提供底层接口用来读写不同类型的组成的 ArrayBuffer,并且可以不关心不同环境下的字节序endianness。具体的使用可以参考MDN

Little Endian VS Big Endian

小端字节序(Little Endian)和大端字节序(Big Endian)主要是在存储大数的时候出现的不同存储规则,Little Endian 在存储数据的时候是按照从小到大的顺序存储的,常常用在本地数据存储交互的情况下。Big Endian 则按照从大到小的顺序存储,通常被称作network byte order,更符合人类阅读理解习惯,更多的用于网络传输时的字节存储交互。

Blob(Binary Large Object)

Blob 表示二进制类型的大对象,在前端中多用于文件,音频,视频等内容。特点是 Blob 对象只读,不能进行相应的修改,可以通过Blob.prototype.slice()来获取相应的分割之后的结果。Blob 在前端中的应用主要在以下方面。

File Download

结合URLFetch以及Blob可以实现本地生成文件和远程下载文件

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
<!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>Download File Demo</title>
</head>
<body>
<a id="local">click to download local file</a>
<br />
<a id="remote">click to download remote file</a>
<script>
function downloadLocal() {
const link = document.getElementById('local');
link.addEventListener('click', () => {
const blob = new Blob(['hello world!'], {
type: 'text/plain',
});
const tempLink = document.createElement('a');
tempLink.href = URL.createObjectURL(blob);
tempLink.download = 'foo.txt';
tempLink.click();
tempLink.remove();
URL.revokeObjectURL(tempLink.href);
});
}

function downloadRemote() {
const link = document.getElementById('remote');
link.addEventListener('click', () => {
fetch(
'https://pic2.zhimg.com/v2-3be05963f5f3753a8cb75b6692154d4a_1440w.jpg?source=172ae18b'
).then(response => {
response.blob().then(blob => {
const tempLink = document.createElement('a');
tempLink.href = URL.createObjectURL(blob);
tempLink.download = 'remote-picture.jpg';
tempLink.click();
tempLink.remove();
URL.revokeObjectURL(tempLink.href);
});
});
});
}

downloadLocal();
downloadRemote();
</script>
</body>
</html>

如上所示,演示了本地生成的 blob 和远程 fetch 的 blob 最终生成下载文件的整个过程,需要注意以下几点:

  1. URL.createObjectURL()方法可以允许使用 Blob 对象作为 URL 源,以实现下载二进制文件
  2. 远程通过 fetch 方法获取到的 Response 对象,需要调用Response.prototype.blob()方法来将响应转化为 blob
  3. 下载完成之后,调用URL.revokeObjectURL()方法来释放内存资源和性能优化

Image Preview

URL.createObjectURL()创建的 URL 也可以用于img元素的src属性,以实现图片等文件的本地预览

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
<!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>Image Preview</title>
</head>
<body>
<input id="file" type="file" accept="image/png, image/jpeg" />
<script>
function imgPreview() {
document.getElementById('file').addEventListener('change', function () {
const localImg = this.files[0];
const img = document.createElement('img');
img.src = URL.createObjectURL(localImg);
img.onload = () => {
URL.revokeObjectURL(img.src);
};
document.body.appendChild(img);
});
}
imgPreview();
</script>
</body>
</html>

需要注意的是 input 的 type 为 file 时,对应的 files 属性是一个FileList对象

Upload Sliced Files

分片上传在前端处理大文件上传的时候有以下的优点:

  1. 断点续传,大文件分割上传失败之后只需要上传对应失败的分片内容
  2. 某些服务器会有上传内容和上传时间的限制,分片上传可以避免这些限制

使用Blob.prototype.slice()可以实现文件的分片上传,如下示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const file = new File(['a'.repeat(1000000)], 'test.txt');

const chunkSize = 40000;
const url = 'https://your.post.url/';

async function chunkedUpload() {
for (let start = 0; start < file.size; start += chunkSize) {
const chunk = file.slice(start, start + chunkSize + 1);
const fd = new FormData();
fd.append('data', chunk);

await fetch(url, { method: 'post', body: fd }).then(res => res.text());
}
}

NOTICE

  1. 关于分片之后不同分片的到达后端的顺序,我们只需要传的时候把对应的分片序号一并带过去就可以,这样在所有的分片都传输完成之后,后端就可以将分片拼接成一个整体的文件
  2. 可以事先判断文件的 md5 值,对于每个文件,md5 值是唯一的。这样当重复的文件上传时,服务器只需要根据 md5 值先判断文件是否存在,如果存在则不需要再次传输,实现类似于”秒传”功能

Blob VS ArrayBuffer

  • 除非你需要使用 ArrayBuffer 提供的写入/编辑的能力,否则 Blob 格式可能是最好的。
  • Blob 对象是不可变的,而 ArrayBuffer 是可以通过 TypedArrays 或 DataView 来操作。
  • ArrayBuffer 是存在内存中的,可以直接操作,而 Blob 可以位于磁盘、高速缓存内存和其他不可用的位置。
  • 虽然 Blob 可以直接作为参数传递给其他函数,比如 window.URL.createObjectURL()。但是,你可能仍需要 FileReader 之类的 File API 才能与 Blob 一起使用。Blob 与 ArrayBuffer 对象之间是可以相互转化的:
    • 使用 FileReader 的 readAsArrayBuffer()方法,可以把 Blob 对象转换为 ArrayBuffer 对象;
    • 使用 Blob 构造函数,如 new Blob([new Uint8Array(data]);,可以把 ArrayBuffer 对象转换为 Blob 对象

Comments