浏览器文件处理小结分享

189 阅读5分钟

最近在开发中用到了浏览器端的二进制文件相关的功能。所以想写篇文章记录整理以及分享。

JS涉及二进制操作的API有很多,包括:Blob,File,FileReader,TypedArray,DataView,ArrayBuffer等。其中,ArrayBuffer是最重要的。无论想在浏览器上操作什么文件,怎样操作,都要围绕ArrayBuffer进行

ArrayBuffer介绍及使用ArrayBuffer获得文件

一个ArrayBuffer对象是一段纯二进制的数据。Buffer可以理解为浏览器分配给这个二进制文件的一段内存,无论哪个二进制文件,只要想在浏览器上操作就必然会有一个对应的ArrayBuffer对象存在。下面我们先自己创建一个:

const buffer = new ArrayBuffer(8);

console.log(buffer.byteLength); // 输出8

在这里我们直接创建了一个8个字节长度的ArrayBuffer对象。

ArrayBuffer还有一个最重要的特性就是没有任何方法直接操作。由于无法直接操作,所以从Array那里沿袭过来的方法只有一个,就是ArrayBuffer.prototype.slice。这个方法的实现和Array.prototype.slice几乎一模一样,唯一的区别在于其返回的是一个新的ArrayBuffer对象而不是一个Array对象。通过这个方法就可以轻松实现ArrayBuffer对象的复制:

{
  const buffer = new ArrayBuffer(8);

  const anotherBuffer = buffer.slice(0);

  console.log(anotherBuffer.byteLength); // 输出8
  console.log(buffer === anotherBuffer); // 输出false
}

对ArrayBuffer对象的复制就等于对文件本身的复制。

但是我们总归要操作二进制文件,既然ArrayBuffer不让我们直接操作,那我们该怎么办?这里就轮到TypedArray和DataView登场了。这里顺便给出生成文件及地址的方法

{
  const buffer = new ArrayBuffer(12);
  const u8 = new Uint8Array(buffer);
  const str = "Hello World!";
  for(let i = 0; i < str.length; i++){
    u8[i] = str.codePointAt(i);
  }

  const file = new File([u8.buffer], "aFile.txt", { type: "text/plain" });
  const domUrl = URL.createObjectURL(file);
  
  window.location.href = domUrl;
}

以上代码可以视情况简化,比方说Uint8Array创建时不传入ArrayBuffer对象时会自动创建一个,而生成文本时File可以直接传入字符串等。但是即便如此,也必须牢记ArrayBuffer是核心这一事实。

接下来讲解一下各种场景下如何获得文件对应的ArrayBuffer。

通过上传控件获取文件时

这是获取文件最常见的途径,当用户上传控件并选择一个文件时,我们就能在该控件的DOM对象中找到一个files属性,该属性是一个FileList类型的类数组,里面就有用户刚刚选择的一个或多个文件,都是File类型的对象。这个File类型其实刚刚已经出现过了,那么怎么从File类型的对象里拿到对应的ArrayBuffer呢?非常简单:

{
  // 对象获取过程略......
  file.arrayBuffer().then(buffer => /* process the ArrayBuffer */);
}

File是Blob的子类,而Blob就有直接获取arrayBuffer的方法,File的对象也可以直接调用,返回一个Promise。此时就可以使用TypedArray或DataView对取得的文件进行处理了,比方说我觉得文件太大,想切割一下再上传:

{
  // 对象获取过程略......
  file.arrayBuffer().then(buffer => {
    const u8 = new Uint8Array(buffer);
    // 循环略......
    const u8Chunk = u8.subarray(start, end);
    const fileChunk = const file = new File([u8], `chunk${start}_${end}`);
    const form = document.createElement("form");
    const formData = new FormData(form);
    formData.append("file", fileChunk);
    // 略......
  });
}

实际文件切割上传还涉及更多东西,这里不深入。

通过API接口获取文件

这种也比较常见。我们工作时经常会出现报表导出等功能。这种情况简单的做法就是直接window.location.href跳API地址,但是这种做法有一定的局限性。一个是只能发起get请求,假如请求需要的参数量较大就有点捉急;一个没有回调,无法追踪进度,如果文件较大,用户可能会疯狂点击,体验不好。但是如果使用ajax下载文件就可以完美解决上述痛点。

{
  const xhr = new XMLHttpRequest();
  xhr.open(method, url, true);
  xhr.responseType = "arraybuffer";
  
  xhr.onload = function() {
    // 略......
    const ab = this.response;
    
    if(ab instanceof ArrayBuffer) {
      // 文件类型可以从Content-type响应头获取
      // 文件名可以从Content-disposition响应头获取
      const file = new File([ab], filename, {type});
      const domUrl = URL.createObjectURL(file);
      
      const saveLink = document.createElement("a");
      saveLink.href = domUrl;
      saveLink.download = filename;
      
      // 模拟一次点击事件
      const event = document.createEvent('MouseEvents');
      event.initMouseEvent("click", true, false, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null);
      saveLink.dispatch(event);
    }else {
      // 降级处理
    }
  }
}

显然这样一来就可以使用post发送数据,并且可以在回调中修改一下按钮状态之类的,方便了很多。

有的时候即使设置了responseType为arraybuffer也无法获得ArrayBuffer类型的数据,可以降级到直接打开,在此之前,可以优先考虑使用fetch获取数据。

{
  fetch(url, {
    // ...请求参数
  }).then(res => {
    if(res.ok) {
      // 略......
      return res.arrayBuffer()
    }
  }).then(buffer => {
    // 操作buffer
  })
}

同样可以使用post方法发送数据以及使用回调。可以判断一下兼容性酌情使用。

使用canvas处理图片

在浏览器上可以利用canvas直接处理图片,不需要了解图片特定的格式。举个例子:

{
  const canvas = document.createElement('canvas');
  const ctx = canvas.getContext('2d');
  
  const img = new Image();
  let imgData = null;
  
  img.onload = () => {
    const width = img.width;
    const height = img.height;
    canvas.width = width;
    canvas.height = height;
    ctx.drawImage(img, 0, 0, width, height);
    imgData = ctx.getImageData(0, 0, width, height);
    console.log(imgData) // 打印出一个ImageData对象
    
    ctx.putImageData(imgData, 0, 0);
    canvas.toDataURL();
  }
  
  // 示意,实际上可能会有跨域问题
  img.src = 'https://iph.href.lu/879x200';
}

ImageData对象有个data属性,这是一个Uint8ClampedArray类型的对象,其中每四个数值代表一个像素,分别代表R,G,B,A的值,我们可以根据需要进行修改,如将图片透明度减半,就将每个像素的第四个值除以2再取个整就行了。但是这里必须注意,Uint8ClampedArray对象虽然可以直接通过imgData.data.buffer取得ArrayBuffer对象,却不可以用它去写图片文件,因为这个对象存储的只是图片的描述性信息,并不是真正的图片格式数据。这里要获取图片文件应该这么来:

{
  // 略......
  img.onload = () => {
    // 略......
    ctx.putImageData(imgData, 0, 0);
    // 直接获得base64编码的图片
    const dataUrl = canvas.toDataURL();
    // 或者直接获得文件
    canvas.toBlob(blob => {
      blob.arrayBuffer().then(buffer => /* process the ArrayBuffer */);
    })
  }
  
  // 示意,实际上可能会有跨域问题
  img.src = 'https://iph.href.lu/879x200';
}

以上两种方法都可以指定图片格式,在支持有损压缩的图片格式时还可以指定压缩率,如:

{
  const dataUrl = canvas.toDataURL('image/jpeg', 0.5);
  // 或者
  canvas.toBlob(blob => {
    blob.arrayBuffer().then(buffer => /* process the ArrayBuffer */);
  }, 'image/jpeg', 0.5)
}

总结

关于文件这块没有覆盖到的还有很多,但是以我的观点,把ArrayBuffer作为核心来进行思考总可以找到突破口。