本网站(662p.com)打包出售,且带程序代码数据,662p.com域名,程序内核采用TP框架开发,需要联系扣扣:2360248666 /wx:lianweikj
精品域名一口价出售:1y1m.com(350元) ,6b7b.com(400元) , 5k5j.com(380元) , yayj.com(1800元), jiongzhun.com(1000元) , niuzen.com(2800元) , zennei.com(5000元)
需要联系扣扣:2360248666 /wx:lianweikj
如何实现 xhr 和 fetch 的加载进度条功能?
sz199511 · 147浏览 · 发布于2023-07-10 +关注

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

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

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

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

那么关键点就在于封装 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 还在试验中,不能用于生产环境。

如果说哪天可以了,子辰会在写一篇文章给同学们讲一下。

总结

我们讲了如何在 Ajax 请求中监听数据加载进度,并且实现了下载进度的效果展示。

对于 xhr 和 fetch 两种方式,分别给出了实现方案,并且详细解释了其中的关键点和注意事项。

如果你是认真的看完并且理解了,那么以下技能你已经学会了:

  • 封装 Ajax 请求并实现下载进度的效果展示;

  • 监听 xhr 中的 progress 事件,获取当前加载量和总量;

  • 使用 fetch 的 ReadableStream 读取器,实现流式数据读取;

  • 通过响应头中的 Content-Length 字段获取总量,并计算当前加载量;

  • 了解请求进度的问题及目前的解决方案。

掌握这些技能,可以让你更加深入地了解数据加载和网络请求的原理,并且提高开发效率,优化用户体验。同时,也为你今后的学习和工作打下坚实的基础。

当然,这个问题其实也是面试中的高频问题,在你面试过程中如果遇到面试官问这个问题,就可以将这篇文章总结归纳一下讲给面试官,扩展部分如果你也能理解消化,那么绝对可以惊艳到面试官!

这将是你拿到更高薪资的助推力!



相关推荐

PHP实现部分字符隐藏

沙雕mars · 1325浏览 · 2019-04-28 09:47:56
Java中ArrayList和LinkedList区别

kenrry1992 · 908浏览 · 2019-05-08 21:14:54
Tomcat 下载及安装配置

manongba · 970浏览 · 2019-05-13 21:03:56
JAVA变量介绍

manongba · 963浏览 · 2019-05-13 21:05:52
什么是SpringBoot

iamitnan · 1086浏览 · 2019-05-14 22:20:36
加载中

0评论

评论
不积跬步无以至千里,不积小流无以成江海!
分类专栏
小鸟云服务器
扫码进入手机网页