实现从后端获取二进制数据并下载,以及乱码/打不开的问题

3,815 阅读4分钟

在工作中遇到一个关于文件下载的问题。
场景:从后端请求到一段二进制数据,点击按钮的时候下载。 一开始下载的文件呐,不是打不开,就是打开了乱码,通过查阅资料并学习最终解决了问题。以下是解决方法和学习笔记。

获取数据

axios发送请求。我用到的是post请求,

{
  url: 'your url',
  method: 'post',
  /* 一定要加这个 否则就会乱码 */
  responseType: 'arraybuffer',
}
  • XMLHttpRequest.responseType是一个枚举字符串值,用于指定响应中的数据类型。(还有别的值,可根据实际需要选择)
  • 当值arraybuffer时表示一个包含二进制数据的JavaScript ArrayBuffer。
  • ArrayBuffer 对象用来表示通用的、固定长度的原始二进制数据缓冲区。 ps.就是因为这个responseType,卡了我两三个小时,我在别人写的博客里其实也看到过这个参数,但是没往这个上想,平时发请求也没用过这个参数,这次新的知识又增加了。

将二进制数据变成blob

res是获取到的原始二进制数据

const blob = new Blob([res], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
  • Blob 对象表示一个不可变、原始数据的类文件对象。它的数据可以按文本或二进制的格式进行读取,也可以转换成 ReadableStream 来用于数据操作。
  • 构造函数Blob(blobParts[, options])
    • blobParts 是一个由ArrayBuffer, ArrayBufferView, Blob, DOMString 等对象构成的 Array ,或者其他类似对象的混合体,它将会被放进 Blob。DOMStrings会被编码为UTF-8。
    • options是一个可选的BlobPropertyBag字典,它可能会指定如下两个属性:
      • type,默认值为 "",它代表了将会被放入到blob中的数组内容的MIME类型
      • endings,默认值为"transparent",用于指定包含行结束符\n的字符串如何被写入。 它是以下两个值中的一个:
        • native,代表行结束符会被更改为适合宿主操作系统文件系统的换行符
        • transparent,代表会保持blob中保存的结束符不变
  • 直接打印blob对象,会得到 consoleblob.png 这只能看到大小和MIME类型,想看文本内容的话酱紫:
const READER = new FileReader();
READER.addEventListener('loadend', (e) => {
  console.log(e.target.result);// 此处输出结果
  });
READER.readAsText(blob);

blobtext.png ok啦

  • MIME类型,媒体类型(通常称为 Multipurpose Internet Mail Extensions 或 MIME 类型 )是一种标准,用来表示文档、文件或字节流的性质和格式。
    • 这次只用到了excel,需要用别的的时候去网上查一下就行,下面这个是.csv文件的type
    { type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=utf-8" }
    

下载文件

filer-saver下载,blob是上一步处理好的Blob对象。我这次要下载的是一个excel文件

import FileSaver from 'file-saver';
FileSaver.saveAs(blob, 'filename.xlsx');

这一步就是纯粹的API调用。

合起来酱紫

  const getListAll = () => {
    setExportLoading(true);
    getExcel({
      params: {
        ...queryParams,
      },
    }, { responseType: 'arraybuffer' }).then((res) => {
      if (res) {
        const blob = new Blob([res], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
        FileSaver.saveAs(blob, 'filename.xlsx');
      }
    }).finally(() => {
      setExportLoading(false);
    });
  };

补充-为什么增加了responseType后,返回结果就能正常处理了。

打印看看结果

先打印出,加和不加responseType: 'arraybuffer'时原始数据和blob缓冲区的数据
控制台打印加了responseType: 'arraybuffer'的结果 res.png 控制台打印不加responseType: 'arraybuffer'的结果 乱码.png 打印加了responseType: 'arraybuffer'的blob缓冲区 add.png 打印不加responseType: 'arraybuffer'的blob缓冲区 noadd.png

用其他的不需要responseType就可以正常返回对象或者数组的接口试一下,给他加上responseType: 'arraybuffer', 分别在network和控制台打印一下结果,发现请求的资源就是很正常的我们期待返回的结果, 但是控制台里,就是个这 arraybuffer.png

这时候推出:加或者不加responseType: 'arraybuffer'获取到的资源都是固定的(在network里看到的东西没有变化),加上这个会限制res(响应结果)的类型(加上responseType: 'arraybuffer'后打印出的res就是个ArrayBuffer对象)。

再查一下打印出来的东西都代表啥意思 ArrayBufferByteLength表示ArrayBuffer的大小,单位是字节。

  • Int8Array 类型数组表示二进制补码8位有符号整数的数组。
  • Uint8Array 数组类型表示一个8位无符号整型数组

这一堆数组都是二进制数组,应该就是这些数组里存放了文件相关的信息。

结论

一般情况下,我们发请求都不加这个responseType: 'arraybuffer',不加的时候默认值是text,会指定response是DOMString对象中的文本,所以平时不加responseType也没事。导出文件这个需求这里,后端返回的是二进制文件,这时候要是不加responseType,二进制流会被当成DOMString解析成一段乱码的字符串,这时候用乱码的字符串构建去的blob对象,显然不是预期的结果,所以会造成导出文件乱码或者文件打不开(被破坏)。我们预期的是用返回的二进制数据构建blob,这时候必须指定响应数据的类型,浏览器才能正确的处理数据。 这时候又有新的问题,浏览器是怎么解析响应正文的呢?