如何实现 xhr 和 fetch 的加载进度条功能?
当前位置:点晴教程→知识管理交流
→『 技术文档交流 』
想要在 xhr 和 fetch 中获得数据加载的比例,从而实现一个“真”进度条,你有什么实现思路吗? 我是渡一前端子辰老师,相信认真阅读完这篇文章后,这将不再是一个问题! 思考首先,我们知道数据加载的比例常用在进度条的效果上。 这就意味着我们需要监听从响应开始到响应完成,这个过程中任意一个时间点上目前加载数据的多少,以及总量的多少。 因为只要知道了目前的量以及总量,我们就能够得到任意时间点的加载进度。 得到进度之后剩下的就是渲染界面了,这部分就比较简单了。 那么关键点就在于封装 Ajax 请求,我们如何分别在 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); }); } 可以看到,每一次加载完一小段,都会输出加载量和总值,那么知道了这两个数据之后,计算百分比就很简单了。 我们只需要将数据返回给 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 最终只有两种状态,要么成功,要么失败。 我们无法知道从开始到成功或从开始到失败中间发生了什么事情。 但是我们知道服务器端的响应头里有一个 所以说总得数据量我们是知道的。 关键的是当前的加载量我们不知道,那么我们就必须改造一下这个 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 还在试验中,不能用于生产环境。
该文章在 2023/11/27 11:45:02 编辑过 |
关键字查询
相关文章
正在查询... |