LOGO OA教程 ERP教程 模切知识交流 PMS教程 CRM教程 开发文档 其他文档  
 
网站管理员

如何实现 xhr 和 fetch 的加载进度条功能?

freeflydom
2023年11月27日 11:45 本文热度 566

想要在 xhr 和 fetch 中获得数据加载的比例,从而实现一个“真”进度条,你有什么实现思路吗?

我是渡一前端子辰老师,相信认真阅读完这篇文章后,这将不再是一个问题!

思考

首先,我们知道数据加载的比例常用在进度条的效果上。

这就意味着我们需要监听从响应开始到响应完成,这个过程中任意一个时间点上目前加载数据的多少,以及总量的多少。

因为只要知道了目前的量以及总量,我们就能够得到任意时间点的加载进度。

得到进度之后剩下的就是渲染界面了,这部分就比较简单了。

那么关键点就在于封装 Ajax 请求,我们如何分别在 xhr 与 fetch 中得到目前量与总量?会遇到什么问题呢?我们先从 xhr 开始。

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);

  });

}

可以看到,每一次加载完一小段,都会输出加载量和总值,那么知道了这两个数据之后,计算百分比就很简单了。

我们只需要将数据返回给 onProgress 在界面实现效果就好了。

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);

  });

}

于是我们就得到了这样一个效果,接下来我们看看 fetch 中如何实现。

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,

    });

    const body = await resp.text();

    resolve(body);

  });

}

因为 fetch 返回的是一个 Promise,它没有提供任何事件,所以我们获取到加载量是很困难的,而 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 中就比较简单。

// xhr 中给我们提供了一个事件叫 upload

// upload 里有一个事件叫 progress, upload 里的 progress 事件只监听请求。

// 它的事件 e 里仍然提供了

// e.loaded 和 e.total

// 所以 xhr 中实现上传就比较简单

xhr.upload.addEventListener("progress", (e) => {});

我们在来说一下 fetch,遗憾的是 fetch 中实现不了请求进度。

有的同学会说,响应是一个 response 对象,它里边有 body 可以拿到读取器,可以一部分一部分的读,那么请求不就是一个 request 对象吗?它里边不也有 body 吗?不也可以一部分一部分读吗?

这是不行的,子辰尽量给同学解释一下,听不懂也没关系。

我们知道,无论是请求或者响应,它的 body 属性的类型都是一个叫做 ReadableStream 的可读流。

这种可读流都有一个特点,就是在同一时间只能被一个人读取,那么你想想,请求里的流是不是被浏览器读取了?浏览器把这个流读出来,然后发送到了服务器,所以说我们就读不了了,就是这个问题。

而且浏览器在读的过程中又不告诉我们它读了多少,但是目前 W3C 正在讨论一种方案,这种方案是附带在 ServiceWorker 里边的,它里边有一套 API 叫做,BackgroundFetchManager目前这套 API 里可以实现请求进度的监听,但是这套 API 还在试验中,不能用于生产环境。


作者:子辰Web草庐
链接:https://juejin.cn/post/7253969759191023675
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。



该文章在 2023/11/27 11:45:02 编辑过
关键字查询
相关文章
正在查询...
点晴ERP是一款针对中小制造业的专业生产管理软件系统,系统成熟度和易用性得到了国内大量中小企业的青睐。
点晴PMS码头管理系统主要针对港口码头集装箱与散货日常运作、调度、堆场、车队、财务费用、相关报表等业务管理,结合码头的业务特点,围绕调度、堆场作业而开发的。集技术的先进性、管理的有效性于一体,是物流码头及其他港口类企业的高效ERP管理信息系统。
点晴WMS仓储管理系统提供了货物产品管理,销售管理,采购管理,仓储管理,仓库管理,保质期管理,货位管理,库位管理,生产管理,WMS管理系统,标签打印,条形码,二维码管理,批号管理软件。
点晴免费OA是一款软件和通用服务都免费,不限功能、不限时间、不限用户的免费OA协同办公管理系统。
Copyright 2010-2024 ClickSun All Rights Reserved