首先,我们知道数据加载的比例常用在进度条的效果上。
这就意味着我们需要监听从响应开始到响应完成,这个过程中任意一个时间点上目前加载数据的多少,以及总量的多少。
因为只要知道了目前的量以及总量,我们就能够得到任意时间点的加载进度。
得到进度之后剩下的就是渲染界面了,这部分就比较简单了。
那么关键点就在于封装 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 字段获取总量,并计算当前加载量;
了解请求进度的问题及目前的解决方案。
掌握这些技能,可以让你更加深入地了解数据加载和网络请求的原理,并且提高开发效率,优化用户体验。同时,也为你今后的学习和工作打下坚实的基础。
当然,这个问题其实也是面试中的高频问题,在你面试过程中如果遇到面试官问这个问题,就可以将这篇文章总结归纳一下讲给面试官,扩展部分如果你也能理解消化,那么绝对可以惊艳到面试官!
这将是你拿到更高薪资的助推力!
发表评论 取消回复