XHR、Fetch实现真实的下载、上传进度

博客围绕在xhr和fetch中获取数据加载比例以实现真实进度条展开。介绍了对xhr和fetch进行封装获取加载量的方法,包括改造xhr封装获取各时间点数据量,利用fetch请求头Content - Length及引入流概念获取加载量。还提及上传情况及W3C计划的相关API。

想要在xhr和fetch中获取到数据的加载比例,从而实现一个真实的进度条,你会怎么实现?

想要得到进度就要知道过程中任意时间点目前加载数据的多少和总量
在得到当前量和总量后,要得到进度就很简单了,其关键就在于对于 xhr和fetch的封装了
先从xhr开始,也是做好理解的
我们先看来一个常见的xhr的封装

export function request(options = {}) {
  const { url, method = "GET", data = null } = options;
  return new Promise((resolve) => {
    const xhr = new XMLHttpRequest();
    xhr.addEventListener("readystatechange", () => {
      if (xhr.readyState === xhr.DONE) {
        resolve(xhr.responseText);
      }
    });
    xhr.open(method, url);
    xhr.send(data);
  });
}

当然这样是没有办法拿到目前传输的数据量,所以需要尽享改造

export function request(options = {}) {
  // 首先我们在配置里加入一个 onProgress
  // 这个 onProgress 要传递一个函数
  // 没每当服务器完成了一小段数据的加载之后,我们就会调用这个函数
  // 并且返回目前的加载量以及总量
  const { url, method = "GET", onProgress, data = null } = options;
  return new Promise((resolve) => {
    const xhr = new XMLHttpRequest();
    xhr.addEventListener("readystatechange", () => {
      if (xhr.readyState === xhr.DONE) {
        resolve(xhr.responseText);
      }
    });
    // xhr 给我们提供了一个 progress 事件,这里的 progress 事件只监听响应。
    // 每当服务器传输完一小段数据之后就会触发 progress 事件
    xhr.addEventListener("progress", (e) => {
      // 在事件 e 里包含了总量与加载量,我们打印到控制台
      // e.loaded 当前加载量
      // e.total 总量
      console.log(e.loaded, e.total);
    });
    xhr.open(method, url);
    xhr.send(data);
  });
}

这样就可以拿到每一个时间点的数据量,我们把这个量返回出去就可以请求外拿到了

export function request(options = {}) {
  const { url, method = "GET", onProgress, data = null } = options;
  return new Promise((resolve) => {
    const xhr = new XMLHttpRequest();
    xhr.addEventListener("readystatechange", () => {
      if (xhr.readyState === xhr.DONE) {
        resolve(xhr.responseText);
      }
    });
    xhr.addEventListener("progress", (e) => {
      // 调用 onProgress 并将数据传递给它
      onProgress &&
        onProgress({
          loaded: e.loaded,
          total: e.total,
        });
    });
    xhr.open(method, url);
    xhr.send(data);
  });
}

xhr的就完成了,下面我们看下fetch的封装

还是来一个简易的封装

export function request(options = {}) {
  const { url, method = "GET", data = null } = options;
  return new Promise(async (resolve) => {
    const resp = await fetch(url, {
      method,
      body: data,
    });
    const body = await resp.text();
    resolve(body);
  });
}

因为fetch返回是一个Promise,没有任何事件方法,最终返回只有两种状态,成功或者失败,所以想要获取加载量是非常困难的。但是我们可以从请求头的Content-Length知道我们预告了我们所需数据的总量大小
所以对fetch的改造主要重点是加载量,需要引入一个概念,流。可以假设可读流是一桶水,读流就是不停的一杯一杯的舀水直到水被取完了。
下面我们改造下fetch

export function request(options = {}) {
  const { url, method = "GET", data = null } = options;
  return new Promise(async (resolve) => {
    const resp = await fetch(url, {
      method,
      body: data,
    });
    // 因为我们不知道 Promise 中间发生了什么,所以就不能使用这样的方便时解析响应体了
    // const body = await resp.text();
    // 如果说你熟悉 fetch Api 应该知道,
    // resp 对象里有个属性叫 body 它代表的就是响应体
    // resp.body 的类型是一个 ReadableStream<Uint8Array> 也就是可读流
    // 那既然是一个可读流,我们就通过 getReader() 读取一下,拿到流的读取器
    const reader = resp.body.getReader();
    // 我们使用循环来读取流的数据
    while (1) {
      // 读取流是需要时间的,所以我们等待一下
      // 返回值是一个对象,我们结构出来得到两个值
      // value 是当前流的数据,done 是流数据我们是否读取完毕
      const { value, done } = await reader.read();
      // 如果说取完了就不再循环了
      if (done) {
        break;
      }
      // 我们打印一下流的数据
      console.log("value >>> ", value);
    }
    // 暂时禁用,不让 Promise 完成
    // resolve(body);
  });

需要注意是 每次输出的结果是当前的读取量并非当前已经读取的总值,就需要把当前时间之前的值累加

export function request(options = {}) {
  // 在配置里加入一个 onProgress
  const { url, method = "GET", onProgress, data = null } = options;
  return new Promise(async (resolve) => {
    const resp = await fetch(url, {
      method,
      body: data,
    });
    // 通过 content-length 得到总量
    const total = +resp.headers.get("content-length");
    const reader = resp.body.getReader();
    // 声明一个变量用来储存读取的量
    let loaded = 0;
    // promise 最后的完成需要把所有的数据拼接起来返回
    // 所以定一个变量用来储存数据拼接的值
    let body = "";
    // 这个数据可能是二进制,那就要使用 arrayBuffer
    // 也可能是文本,就要使用文本解码器
    // 比如说我们这里是文本,我们先定一个解码器
    const decoder = new TextDecoder();
    while (1) {
      const { value, done } = await reader.read();
      if (done) {
        break;
      }
      // 每一次读取都累加起来
      loaded += value.length;
      // 每一次读取都对数据解码并拼接起来
      body += decoder.decode(value);
      // 当然在每一次读取的时候都要像 xhr 一样,把总量和读取量返回
      onProgress &&
        onProgress({
          loaded,
          total,
        });
    }
    // Promise 完成并返回数据
    resolve(body);
  });
}

这样就已经下载搞定了。那上传该怎么办呢,讲道理逻辑上上传和下载是一样的,反着来而已。
在xhr的上传是很简单的

// xhr 中给我们提供了一个事件叫 upload
// upload 里有一个事件叫 progress, upload 里的 progress 事件只监听请求。
// 它的事件 e 里仍然提供了
// e.loaded 和 e.total
// 所以 xhr 中实现上传就比较简单
xhr.upload.addEventListener("progress", (e) => {});

然而在fetch就很遗憾了,没有办法实现。因为fetch的思路是流,然而流有一个特点就是只能被一个人读区,请求里面的流被浏览器读区了,浏览器读出来发送给了服务端,我们就读不到了(上传服务器和事件里面获取是两次操作),浏览器在读的过程中不会告诉我们读区了多少。
目前W3C也在计划一个方案,附带在ServiceWork里面有一个API:BackgroundFetchManager。这个API可以实现请求进度的监听,不过目前还在实验中,正式环境还没有开放。

最后做一个总结

1、封装 Ajax 请求并实现下载进度的效果展示;
2、监听 xhr 中的 progress 事件,获取当前加载量和总量;
3、使用 fetch 的 ReadableStream 读取器,实现流式数据读取;
4、通过响应头中的 Content-Length 字段获取总量,并计算当前加载量;

内容来源

https://juejin.cn/post/7253969759191023675

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值