前端二进制初探

215 阅读5分钟

Introduction

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

ArrayBuffer

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

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则是采用四舍五入的形式取整,举个例子:

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. 当前类型的最高值加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可以实现本地生成文件和远程下载文件

<!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属性,以实现图片等文件的本地预览

<!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()可以实现文件的分片上传,如下示例:

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 对象