Introduction
在日常的前端开发中,涉及到的工作内容大多和UI有关,比如说页面样式调整之类的基础工作。但是随着前端的发展,很多业务逻辑都放到了前端,例如文件生成和下载,图片处理等功能,这时候就涉及到了前端的二进制相关的内容,这篇文章从二进制相关内容和API出发,探究前端二进制相关的用途。
ArrayBuffer
ArrayBuffer是一段基础的,固定长度的二进制数据,类似于其他语言的byte array。不能直接修改相关的内容,但是可以通过TypedArray和DataView进行读写。
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]:
Uint8Array vs Uint8ClampedArray
Uint8Array在处理小数位的时候采用的是向下取整,Uint8ClampedArray则是采用四舍五入的形式取整,举个例子:
Uint8Array([0.9]) // 0
Uint8ClampedArray([0.9]) // 1
Uint8ClampedArray当赋值在区间[0,255]之外,则只有取值为0或者255的两种情况,所以更多的运用于防止溢出的情况,例如增加图片的亮度[2]
overflow
TypedArray的溢出处理方式简单来说就是抛弃溢出的位,然后按照视图类型进行解释。如下所示:
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会被转换为当前类型的最低值
- 当前类型的最低值减一则会被转换为当前类型的最高值
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
结合URL和Fetch以及Blob可以实现本地生成文件和远程下载文件
<!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最终生成下载文件的整个过程,需要注意以下几点:
URL.createObjectURL()方法可以允许使用Blob对象作为URL源,以实现下载二进制文件- 远程通过fetch方法获取到的Response对象,需要调用
Response.prototype.blob()方法来将响应转化为blob - 下载完成之后,调用
URL.revokeObjectURL()方法来释放内存资源和性能优化
Image Preview
URL.createObjectURL()创建的URL也可以用于img元素的src属性,以实现图片等文件的本地预览
<!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
分片上传在前端处理大文件上传的时候有以下的优点:
- 断点续传,大文件分割上传失败之后只需要上传对应失败的分片内容
- 某些服务器会有上传内容和上传时间的限制,分片上传可以避免这些限制
使用Blob.prototype.slice()可以实现文件的分片上传,如下示例:
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
- 关于分片之后不同分片的到达后端的顺序,我们只需要传的时候把对应的分片序号一并带过去就可以,这样在所有的分片都传输完成之后,后端就可以将分片拼接成一个整体的文件
- 可以事先判断文件的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 对象