前端处理二进制数据

前端操作二进制数据的场景还是比较多的,像 webGL 中浏览器和 GPU 的通信、用户上传下载文件、canvas 导出图片、FileReader 读取本地文件、图片/音视频处理等等

注意:下文相关名词最好参考下 MDN,这里只做简短介绍

相关协议

除了 HttpURL,还有一些二进制数据相关的伪协议

  • object URLBlob URL)。表示存储在浏览器内存中的 File 或 Blob 对象
// 创建 objectURL
URL.createObjectURL(Blob | File);
// 释放 objectURL
URL.revokeObjectURL(Blob | File);
  • Data URL。携带 base64 数据

上述两种协议都可以作为资源 url 使用,比如在 img 标签中使用等等

相关对象

先看下几种二进制对象的关系:

二进制对象关系

原始二进制数据

我们知道,字节(byte)是计算机技术中关于二进制数据的一种基本单位,1 字节有 8 个二进制位,即 8 比特(bit)。前端可以通过 fetch 或者 ajax 请求从服务端获取二进制数据,具体表现形式其实就是字节流

fetch(httpURL | dataURL).then((res) => res.blob());
// res.arrayBuffer()
// res.formData() 表单数据
// res.json() JSON
// res.text() 文本

ArrayBuffer

**ArrayBuffer**对象用于表示通用的原始二进制数据缓冲区,其实就是字节数组。通过这个对象,JavaScript 可以读写二进制数据,但是不能直接读写,可以借助 DataViewTypedArray 来读写 ArrayBuffer

// 创建一个 8 字节的 ArrayBuffer 数据
const arrayBuffer = new ArrayBuffer(8);

转换原始的二进制数据为 ArrayBuffer 对象,比如 fetch 内置了对应的方法进行转换:

fetch(url).then((res) => res.arrayBuffer());

怎么理解 JavaScript 中的 ArrayBuffer?

Blob

Blob 对象(binary large object)表示一个不可变、原始数据的类文件对象,它可以表示包含任意类型数据的不可变原始数据,例如图像、音频、视频、压缩文件等。Blob 提供了更便捷的文件操作方法,通过 Blob 对象可以实现文件上传、预览、读取、下载等操作,然后也可以借助它提供的 slice 方法实现数据切分上传

new Blob(array, option);
// array 可以是服务端返回的二进制文件流、blob、ArrayBuffer、DOMString等
// option.type 指定 MIME 类型

注意:浏览器通常使用 MIME 类型(而不是文件扩展名)来确定如何处理 URL,因此 Web 服务器在响应头中添加正确的 MIME 类型非常重要(详见 MDN-MIME

ArrayBuffer 转 Blob:

const buffer = new ArrayBuffer(32);
const blob = new Blob([buffer]);

Blob 转 ArrayBuffer,借助 FileReader:

const blob2 = new Blob(["blob string"], {
  type: "text/plain",
});
const reader = new FileReader();
reader.readAsArrayBuffer(blob2);
reader.onload = function (e) {
  console.log(reader.result); // ArrayBuffer {}
  const buf = new DataView(reader.result);
  console.log(buf); // DataView {}
  reader.readAsText(new Blob([buf]), "utf-8");
  reader.onload = function () {
    console.log(reader.result); // blob string
  };
};

BlobArrayBuffer 的关系:

  • 需要对二进制数据进行写入/编辑操作时使用 ArrayBuffer,否则一般使用 Blob
  • Blob 对象不可变,而 ArrayBuffer 可以通过 TypedArraysDataView 操作
  • ArrayBuffer 是内存上一段连续的二进制数据,Blob 是一个封装二进制数据的整体,可以封装 ArrayBuffer 数据,可以位于磁盘、高速缓存内存和其他不同位置

File

继承了 Blob,并将其扩展使其支持用户系统上的文件,File 对象还包含 lastModifiedname 等属性

const file = new File([Blob | ArrayBuffer], fileName);

哪些场景会生成 File?

  • input[file] 选择的文件
  • 拖拽生成的 dataTransfer 对象

base64

base64 是一种基于 64 个可打印字符来表示二进制数据的表示方法,常用于在处理文本数据的场合,表示、传输和存储一些二进制数据

可以通过 atobbtoa 这两个 api 处理解码和编码 base64 字符串

base64 转成 File:

function dataURLtoFile(dataurl, filename) {
  const arr = dataurl.split(",");
  // 将 base64 编码转为字符串
  const byteStr = atob(arr[1]);
  const mimeStr = dataURI.split(",")[0].split(":")[1].split(";")[0];
  let n = byteStr.length;
  // 创建初始化为0的长度为n的无符号整型数组
  const u8arr = new Uint8Array(n);
  while (n--) {
    u8arr[n] = byteStr.charCodeAt(n);
  }
  return new File([u8arr], filename, {
    type: mimeStr,
  });
}

或者可以先转成 blob,再转成 File:

function dataURItoBlob(dataURI) {
  const arr = dataurl.split(",");
  // 将 base64 编码转为字符串
  const byteStr = atob(arr[1]);
  const mimeStr = dataURI.split(",")[0].split(":")[1].split(";")[0];
  const ab = new ArrayBuffer(byteStr.length);
  const ia = new Uint8Array(ab);
  for (var i = 0; i < byteStr.length; i++) {
    ia[i] = byteStr.charCodeAt(i);
  }
  return new Blob([ab], { type: mimeStr });
}

const file = new File(dataURItoBlob(dataURL));

FileReader

FileReader  是浏览器提供的一个 API,用于读取文件内容。通过 FileReader,我们可以通过异步方式读取文件,并将文件内容转换为可用的数据形式,比如文本数据或二进制数据

const reader = new FileReader();
reader.readAsDataURL(Blob | File);
// .readAsText()
// .readAsArrayBuffer()

应用场景

下载文件

如果请求函数是基于 axios:

  1. axios 对象需要配置 responseType: 'blob'
  2. 前端接收到数据后,使用 Blob 对象来接收二进制流,并且创建 a 标签进行下载;或者将二进制数据转成 base64 再下载
function saveBlob(content, fileName) {
  let downLink = document.createElement("a");
  downLink.download = fileName;
  let blob = new Blob([content]);
  // 生成 BlobURL
  downLink.href = URL.createObjectURL(blob);
  document.body.appendChild(downLink);
  downLink.click();
  document.body.removeChild(downLink);
}

上传文件

借助 FormData,我们可以上传二进制数据

// 可以先将文件转成 Blob
const file = new Blob();
const formData = new FormData();
formData.add("user", "JacksonZhou");
formData.add("file", file);
axios.post("url", formData);

不论是 Blob/File 还是 ArrayBuffer 对象,浏览器最终还是会将其转成字节流再进行传输

分片上传

blob.slice() 可以用于实现大文件的分片上传,这里简单带过,后续有时间再深入研究下 ~

const chunkSize = 100000;
const url = "https://file.zhouweibin.top/upload";
async function selectFile(e) {
  const file = e.target.files[0];
  await uploadFile(file);
}
async function uploadFile(file) {
  for (let start = 0; start < file.size; start += chunkSize) {
    const chunk = file.slice(start, start + chunkSize + 1);
    const formData = new FormData();
    formData.append("data", chunk);
    await fetch(url, { method: "post", body: formData }).then((res) => {
      console.log(res);
    });
  }
}

文件预览

  • 图片预览:File -> FileReader -> Blob -> objectUrl -> img

文件处理

  • 拼接两个音频文件:data -> ArrayBuffer -> TypedArray -> 拼接成一个 TypedArray -> ArrayBuffer -> Blob -> Object URL
  • 图片压缩:canvas.toBlob(callback, mimeType, qualityArgument)
  • 图片灰度化,需要操作到图片像素数据
function draw() {
  const ctx = canvas.getContext("2d");
  ctx.drawImage(image, 0, 0, 230, 230);
  // 基础像素数据
  const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
  const data = imageData.data;
  const grayscale = function () {
    for (let i = 0; i < data.length; i += 4) {
      const avg = (data[i] + data[i + 1] + data[i + 2]) / 3;
      data[i] = avg;
      data[i + 1] = avg;
      data[i + 2] = avg;
    }
    ctx.putImageData(imageData, 0, 0);
  };
}

其他文件操作方法

  • File System API。一组用于在 Web 应用程序中访问和操作本地文件系统的 JavaScript API。它允许 Web 应用程序创建、读取、写入、复制、移动和删除文件和目录,以及查询文件系统的状态和属性
  • 更快速且安全的本地文件读写 - OPFS

参考

聊聊 JS 的二进制家族

一文教你学会 Blob