你不常用的 FileReader 能干什么?

前言

欢迎关注同名公众号《熊的猫》,文章会同步更新,也可快速加入前端交流群!

本文灵感源于上周小伙伴遇到一个问题:

一个本该返回 Blob 类型的下载接口,却返回了 JSon 类型的内容!!!

你不常用的 FileReader 能干什么?

这会有什么问题呢?

按原逻辑就是调用该接口后,就会一股脑把该接口接返回过来的内容,直接经过 Blob 对象 转换后再通过隐藏的 <a> 标签实现下载。

但是有一个问题,那就是接口也是需要进行各种逻辑处理、判断等等,然后再决定是给前端响应一个正常的 Blob 格式的文件流,还是返回相应 JSon 格式的异常信息 等等。

如果返回了 JSon 格式的异常信息,那前端应该给用户展示信息内容,而不是将其作为下载的内容!

你不常用的 FileReader 能干什么?

FileReader 实现 Blob 从 String 到 JSON

复现问题

为了更直观看到对应的效果,我们这里来简单模拟一下前后端的交互过程吧!

前端

由于小伙伴发送请求时使用的是 Axios,并且设置了其对应的 responsetype:blob | arraybuffer,所以这里我们也使用 Axios 即可,具体如下:

    // 发起请求
    const request = () => {
      axios({
        method: 'get',
        url: 'http://127.0.0.1:3000',
        responseType: 'arraybuffer'
      })
        .then((res) => {

          // 转换为 bloc 对象
          const blob = new Blob([res.data])

          // 获取导出文件名,decodeURIComponent为中文解码方法
          const fileName = decodeURIComponent(res.headers["content-disposition"].split("filename=")[1])

          // 通过a标签进行下载
          let downloadElement = document.createElement('a');
          let href = window.URL.createObjectURL(blob);
          downloadElement.href = href;
          downloadElement.download = fileName;
          document.body.appendChild(downloadElement);
          downloadElement.click();
          document.body.removeChild(downloadElement);
          window.URL.revokeObjectURL(href);
        });
    }

后端

这里我们就简单通过 koa 来实现将一个表格文件响应给前端,具体如下:

    const xlsx = require("node-xlsx");

    const Koa = require("koa");
    const app = new Koa();

    const cors = require("koa2-cors");

    // 处理跨域
    app.use(
      cors({
        origin: "*", // 允许来自指定域名请求
        maxAge: 5, // 本次预检请求的有效期,单位为秒
        methods: ["GET", "POST"], // 所允许的 HTTP 请求方法
        credentials: true, // 是否允许发送 Cookie
      })
    );

    // 响应
    app.use(async (ctx) => {
      // 文件名字
      const filename = "人员信息";

      // 数据
      const data = [
        { name: "赵", age: 16 },
        { name: "钱", age: 20 },
        { name: "孙", age: 17 },
        { name: "李", age: 19 },
        { name: "吴", age: 18 },
      ];

      // 表格样式
      const oprions = {
        "!cols": [{ wch: 24 }, { wch: 20 }, { wch: 100 }, { wch: 20 }, { wch: 10 }],
      };

      // JSON -> Buffer
      const buffer = JSONToBuffer(data, oprions);

      // 设置 content-type
      ctx.set("Content-Type", "application/vnd.openxmlformats");

      // 设置文件名,中文必须用 encodeURIComponent 包裹,否则会报异常
      ctx.set(
        "Content-Disposition",
        "attachment; filename=" + encodeURIComponent(filename) + ".xlsx"
      );

      // 文件必须设置该请求头,否则前端拿不到 Content-Disposition 响应头信息
      ctx.set("Access-Control-Expose-Headers", "Content-Disposition");

      // 将 buffer 返回给前端
      ctx.body = buffer;
    });

    // 将数据转成 Buffer
    const JSONToBuffer = (data, options = {}) => {
      let xlsxObj = [
        {
          name: "sheet",
          data: [],
        },
      ];

      data.forEach((item, idx) => {
        // 处理 excel 表头
        if (idx === 0) {
          xlsxObj[0].data.push(Object.keys(item));
        }

        // 处理其他 excel 数据
        xlsxObj[0].data.push(Object.values(item));
      });

      // 返回 buffer 对象
      return xlsx.build(xlsxObj, options);
    };
    
    // 启动服务
    app.listen(3000);

正常效果展示

你不常用的 FileReader 能干什么?

异常效果展示

可以看到当返回的内容为 JSON 格式 的内容时,原本逻辑在获取 filename 处就发生异常了,即使这一块没有发生异常,被正常下载下来也是不对的,因为这种情况应该要进行提示。

你不常用的 FileReader 能干什么?

并且此时直接去访问 res.data 得到的也不是一个 JSON 格式 的内容,而是一个 ArrayBuffer

你不常用的 FileReader 能干什么?

返回的明明是 JSON ,但是拿到的却是 ArrayBuffer?

responseType 惹的祸

还记得我们在通过 Axios 去发起请求时设置的 responseType:'arraybuffer' 吗?

没错,就是因为这个配置的问题,它会把得到的结果给转成设置的类型,所以看起是一个 JSON 数据,但实际上拿到的是 Arraybuffer

这个 responseType 实际上就是 XMLHttpRequest.responseType,可点击该链接自行查看。

不设置 responseType 行不行?

那么既然是这个配置的问题,那么我们不设置不就好了!

确实可行,如下是未设置 responseType 获取到的结果:

你不常用的 FileReader 能干什么?

但也不行,如果不设置 responseType 或者设置的类型不对,那么在 正常情况 下(即 文件被下载)时 会导致文件格式被损坏,无法正常打开,如下:

你不常用的 FileReader 能干什么?

FileReader 来救场

实际上还有个比较直接的解决方案,那就是把接收到的 Arraybuffer 转成 JSON 格式不就行了吗?

你不常用的 FileReader 能干什么?

没错,我们只需要通过 FileReader 来完成这一步即可,请看如下示例:

// json -> blob
const obj = { hello: "world" };

const blob = new Blob([JSON.stringify(obj, null, 2)], {
  type: "application/json",
});

console.log(blob) // Blob {size: 22, type: 'application/json'}

// blob -> json
const reader = new FileReader()

reader.onload = () => {
    console.log(JSON.parse(reader.result)) // { hello: "world" }
}

reader.readAsText(blob,  'utf-8')

是不是很简单啊!

值得注意的是,并不是任何时候都需要转成 JSON 数据,就像并不是任何时候都要下载一样,我们需要判断什么时候该走下载逻辑,什么时候该走转换成 JSON 数据。

怎么判断当前是该下载?还是该转成 JSON?

这个还是比较简单的,换个说法就是判断当前返回的是不是文件流,下面列举较常见的两种方式。

根据 filename 判断

正常情况来讲,再返回文件流的同时会在 Content-Disposition 响应头中添加和 filename 相关的信息,换句话说,如果当前没有返回 filename 相关的内容,那么就可以将其当做异常情况,此时就应该走转 JSON 的逻辑。

不过需要注意,有时候后端返回的某些文件流并不会设置 filename 的值,此时虽然符合异常情况,但是实际上返回的是一个正常的文件流,因此不太推荐这种方式

你不常用的 FileReader 能干什么?

根据 Content-Type 判断

这种方式更合理,毕竟后端无论是返回 文件流 或是 JSON 格式的内容,其响应头中对应的 Content-Type,必然不同,这里的判断更简单,我们直接判断其是不是 JSON 类型即可。

更改后的代码,如下:

axios({
    method: 'get',
    url: 'http://127.0.0.1:3000',
    responseType: 'arraybuffer'
  })
    .then(({headers, data}) => {
      console.log("FileReader 处理前:", data)

      const IsJson = headers['content-type'].indexOf('application/json') > -1;

      if(IsJson){
        const reader = new FileReader()
        
        // readAsText 只接收 blob 类型,因此这里需要先将 arraybuffer 变成 blob
        // 若后端直接返回的就是 blob 类型,则直接使用即可
        reader.readAsText(new Blob([data], {type: 'application/json'}), 'utf-8')
        
        reader.onload = () => {
          // 将字符内容转为 JSON 格式
          console.log("FileReader 处理后:", JSON.parse(reader.result))
        }
        return
      }

      // 下载逻辑
      download(data)
    });

值得注意的是,readAsText 只接收 blob 类型,因此这里需要先将 arraybuffer 变成 blob,若后端直接返回的就是 blob 类型,则直接使用即可。

你不常用的 FileReader 能干什么?

FileReader 还能干什么?

以上是使用 FileReader 解决一个实际问题的例子,那么除此之外它还有什么应用场景呢?

不过我们还是先来了解一下 FileReader 的一些相关内容吧!!!

FileReader 是什么?

FileReader 对象允许 Web 应用程序 异步读取 存储在用户计算机上的文件(或原始数据缓冲区)的内容,使用 File 或 Blob 对象指定要读取的文件或数据。

不过还要注意如下两条规则:

  • FileReader 仅用于以安全的方式从用户(远程)系统读取文件内容,它不能用于从文件系统中按路径名简单地读取文件
  • 要在 JavaScript 中按路径名读取文件,应使用标准 Ajax 解决方案进行 服务器端文件读取

总结起来就是,FileReader 只能读取 FileBlob 类型的文件内容,并且不能直接按路径的方式读取文件,如果需要以路径方式读取,最好要通过 服务端 返回流的形式。

四种读取方式

FileReader 可以如下四种方式读取目标文件:

  • FileReader.readAsArrayBuffer()

    • 开始读取指定的 Blob中的内容,读取完成后,result 属性中保存的将是被读取文件的 ArrayBuffer 数据对象
  • FileReader.readAsBinaryString() (非标准

    • 开始读取指定的Blob中的内容,读取完成后,result 属性中将包含所读取文件的 原始二进制数据
  • FileReader.readAsDataURL()

    • 开始读取指定的Blob中的内容,读取完成后,result 属性中将包含一个 data: URL 格式的 Base64 字符串以表示所读取文件的内容
  • FileReader.readAsText()

    • 开始读取指定的Blob中的内容,读取完成后,result 属性中将包含一个 字符串 以表示所读取的文件内容

如上对应的方法命名十分符合顾名思义的特点,因此可以很容易看出来在不同场景下应该选择什么方法,并且如上方法一般都会配合 FileReader.onload 事件FileReader.result 属性 一起使用。

FileReader 的其他应用场景

预览本地文件

通常情况下,前端选择了相应的本地文件(图片、音/视频 等)后,需要通过接口发送到服务端,接着服务端在返回一个相应的预览地址,前端在实现支持预览的操作。

如果说现在有一个需要省略掉中间过程的需求,那么你就可以通过 FileReader.readAsDataURL() 方法来实现,但是要考虑文件大小带来转换时间快慢的问题。

这一部分比较简单,就不贴代码占篇幅了,效果如下:

你不常用的 FileReader 能干什么?

传输二进制格式数据

通常在上传文件时,前端直接将接收到的 File 对象以 FormData 发送给后端,但如果后端需要的是二进制的数据内容怎么办?

此时我们就可以使用 FileReader.readAsArrayBuffer() 来配合,为啥不用 FileReader.readAsBinaryString(),因为它是非标准的,而且 ArrayBuffer 也是原始的 二进制数据

具体代码如下:

// 文件变化
const fileChange = (e: any) => {
  const file = e.target.files[0]
  const reader = new FileReader()
  reader.readAsArrayBuffer(file)
  result.value = file

  reader.onload = () => {
    upload(reader.result, 'http://xxx')
  }
}

// 上传
const upload = (binary, url) => {
  var xhr = new XMLHttpRequest();
  xhr.open("POST", url);
  xhr.overrideMimeType("application/octet-stream");

  //直接发送二进制数据
  xhr.send(binary);

  // 监听变化
  xhr.onreadystatechange = function (e) {
    if (xhr.readyState === 4) {
      if (xhr.status === 200) {
        // 响应成功       
      }
    }
  }
}

最后

欢迎关注同名公众号《熊的猫》,文章会同步更新,也可快速加入前端交流群!

上面我们通过 FileReader 解决了一个实际问题,同时也简单介绍了其相应的使用场景,但这个场景具体是否是用于你的需求还要具体分析,不能盲目使用。

以上就是本文的全部内容了,希望本文对你有所帮助!!!

你不常用的 FileReader 能干什么?

原文链接:https://juejin.cn/post/7248207744087900197 作者:熊的猫

(0)
上一篇 2023年6月25日 上午10:41
下一篇 2023年6月25日 上午10:51

相关推荐

发表回复

登录后才能评论