使用 ReadableStream 控制 fetch API 响应以实现下载进度条

使用 ReadableStream 控制 fetch API 响应以实现下载进度条

起因

一般前端业务在用户点击下载远程服务器文件,只需要浏览器新开一个 url 下载即可。今天在业务实现中遇到一个比较棘手的问题,由于数据保密的需求,后台OSS服务器需要实现私有读写,这就意味着前端获取远程文件时需要通过秘钥访问SDK,然后回调一个临时下载链接,这又造成了下载遇到中文文件名会乱码的问题。

初步解决方案

  • 将下载的链接通过 fetch API 获取并转为 blob
  • 使用第三方库 file-saver 做文件转存,这样可以保留原有的中文名

进阶需求

现在有个新的问题,如果用户下载的文件很大,浏览器没有任何进度提示(实测>10MB 后,就需要等待漫长的时间完成 fetch API 的 blob() 方法转换),从用户体验的角度出发我们必须实时获得转换的进度,给用户一个进度条提示,这样才会更加 ui friendly。

ReadableStream

  • MDN 的解释

ReadableStream() 构造函数创建并从给定的处理程序返回一个可读的流对象。

  • 咋一看让人一头雾水,我们引入业务代码进一步讨论:

  • 原有的代码逻辑:

const downloadFileToBlob = async (url: string) => {
  const res = await fetch(url);
  if (res.status !== 200) return;
  const blob = await res.blob();
  return blob;
};

const download = async (key: string, fileName?: string) => {
  // 从存储桶中获得临时秘钥 并生成临时下载链接 
  const url = await getDecodeUrl(key, fileName);
  // 生成 blob
  const blob = await downloadFileToBlob(url);
  if (!blob) {
    return 404; // 让上层函数返回 404 页面
  }
  saveAs(blob, fileName); // file-saver 另存为
};

可以看到在 downloadFileToBlob 方法之中使用 fetch API 获得这个文件的二进制流,然后返回,但这个方法是一个黑盒,我们无从得知转换的过程是怎么样的,自然没法读取转换的进度了。

但是, fetch API 返回的 res.body 是一个 ReadableStream<Uint8Array>
示例,而且有一个 getReader() 方法获取这个文件可读取流,那么我们再回到 ReadableStream() 构造函数上。

  • 实例 start() 方法,继续附 MDN 解释

start (controller) 可选
这是一个当对象被构造时立刻调用的方法。此方法的内容由开发人员定义,并应着眼于访问流,并执行其他任何必需的设置流功能。如果这个过程是异步完成的,它可以返回一个 promise,表明成功或失败。传递给这个方法的 controller 是一个 ReadableStreamDefaultController 或 ReadableByteStreamController (en-US),具体取决于 type 属性的值。开发人员可以使用此方法在设立期间控制流。

说人话就是,你可以把上面的 res.body 可读取流放进去,并手动控制这个进程了,是不是很意外地发现了解决办法?

我们改造一下 downloadFileToBlob 代码:

type DownloadProgressCb = (current: number, total: number, done?: boolean) => void;

const downloadFileToBlob = async (url: string, cb?: DownloadProgressCb) => {
  const res = await fetch(url);
  if (res.status !== 200 || !res.body) {
    return false;
  }
	
  // 从 Content-Length 中得到文件的大小
  const size = Number(res.headers.get('Content-Length'));
  // 已读取的大小
  let receivedSize = 0;
  // 获得 ReadableSteam
  const reader = res.body.getReader();

  // 手动控制读取流以便回传下载进度
  const stream = new ReadableStream({
    start(controller) {
     // 声明一个 pumpRead 方法读取片段
     // 因为reader.read() 方法是 Promise 异步的,所以这里是在进行链式调用
      const pumpRead = (): any => reader.read().then(({ done, value }) => {
        // done  - 当 stream 传完所有数据时则变成 true
        // value - 数据片段 - done 为 true 时为 undefined
        if (done) {
          // 结束读取 关闭读取流
          controller.close();
          cb?.(receivedSize, size, true);
          return;
        }
        // 累加进度
        receivedSize += value?.length || 0;
        // 回调进度
        cb?.(receivedSize, size);
        // 每次推入读取队列 并链式执行
        controller.enqueue(value);
        return pumpRead();
      });
	  // 开始读取
      return pumpRead();
    },
  });
  // 最后我们通过 Response 函数接收这个文件流 并转为 blob
  const blob = await new Response(stream).blob();
  return blob;
};

效果

配合回调实时回传的进度,我们就可以实现一个下载进度效果了:
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值