如何實(shí)現(xiàn) xhr 和 fetch 的加載進(jìn)度條功能?
當(dāng)前位置:點(diǎn)晴教程→知識(shí)管理交流
→『 技術(shù)文檔交流 』
想要在 xhr 和 fetch 中獲得數(shù)據(jù)加載的比例,從而實(shí)現(xiàn)一個(gè)“真”進(jìn)度條,你有什么實(shí)現(xiàn)思路嗎? 我是渡一前端子辰老師,相信認(rèn)真閱讀完這篇文章后,這將不再是一個(gè)問題! 思考首先,我們知道數(shù)據(jù)加載的比例常用在進(jìn)度條的效果上。 這就意味著我們需要監(jiān)聽從響應(yīng)開始到響應(yīng)完成,這個(gè)過程中任意一個(gè)時(shí)間點(diǎn)上目前加載數(shù)據(jù)的多少,以及總量的多少。 因?yàn)橹灰懒四壳暗牧恳约翱偭?,我們就能夠得到任意時(shí)間點(diǎn)的加載進(jìn)度。 得到進(jìn)度之后剩下的就是渲染界面了,這部分就比較簡單了。 那么關(guān)鍵點(diǎn)就在于封裝 Ajax 請(qǐng)求,我們?nèi)绾畏謩e在 xhr 與 fetch 中得到目前量與總量?會(huì)遇到什么問題呢?我們先從 xhr 開始。 xhr 中的進(jìn)度我們先看一個(gè)最常見的 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); }); } 這樣的封裝我們無法知曉目前服務(wù)器傳輸了多少數(shù)據(jù),所有我們來改造一下。 export function request(options = {}) { // 首先我們?cè)谂渲美锛尤胍粋€(gè) onProgress // 這個(gè) onProgress 要傳遞一個(gè)函數(shù) // 沒每當(dāng)服務(wù)器完成了一小段數(shù)據(jù)的加載之后,我們就會(huì)調(diào)用這個(gè)函數(shù) // 并且返回目前的加載量以及總量 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 給我們提供了一個(gè) progress 事件,這里的 progress 事件只監(jiān)聽響應(yīng)。 // 每當(dāng)服務(wù)器傳輸完一小段數(shù)據(jù)之后就會(huì)觸發(fā) progress 事件 xhr.addEventListener("progress", (e) => { // 在事件 e 里包含了總量與加載量,我們打印到控制臺(tái) // e.loaded 當(dāng)前加載量 // e.total 總量 console.log(e.loaded, e.total); }); xhr.open(method, url); xhr.send(data); }); } 可以看到,每一次加載完一小段,都會(huì)輸出加載量和總值,那么知道了這兩個(gè)數(shù)據(jù)之后,計(jì)算百分比就很簡單了。 我們只需要將數(shù)據(jù)返回給 onProgress 在界面實(shí)現(xiàn)效果就好了。 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) => { // 調(diào)用 onProgress 并將數(shù)據(jù)傳遞給它 onProgress && onProgress({ loaded: e.loaded, total: e.total, }); }); xhr.open(method, url); xhr.send(data); }); } 于是我們就得到了這樣一個(gè)效果,接下來我們看看 fetch 中如何實(shí)現(xiàn)。 fetch 中的進(jìn)度我們?cè)賮砜匆粋€(gè)非常簡單的 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); }); } 因?yàn)?fetch 返回的是一個(gè) Promise,它沒有提供任何事件,所以我們獲取到加載量是很困難的,而 Promise 最終只有兩種狀態(tài),要么成功,要么失敗。 我們無法知道從開始到成功或從開始到失敗中間發(fā)生了什么事情。 但是我們知道服務(wù)器端的響應(yīng)頭里有一個(gè) 所以說總得數(shù)據(jù)量我們是知道的。 關(guān)鍵的是當(dāng)前的加載量我們不知道,那么我們就必須改造一下這個(gè) fetch 的封裝。 在改造之前先給同學(xué)說一下流的概念,假設(shè)可讀流是一桶水,讀取流就是反復(fù)一杯一杯的從桶里盛出水,可讀流被讀取完就是桶里的水被盛完了。 好了,我們來改造一下 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, }); // 因?yàn)槲覀儾恢?Promise 中間發(fā)生了什么,所以就不能使用這樣的方便時(shí)解析響應(yīng)體了 // const body = await resp.text(); // 如果說你熟悉 fetch Api 應(yīng)該知道, // resp 對(duì)象里有個(gè)屬性叫 body 它代表的就是響應(yīng)體 // resp.body 的類型是一個(gè) ReadableStream<Uint8Array> 也就是可讀流 // 那既然是一個(gè)可讀流,我們就通過 getReader() 讀取一下,拿到流的讀取器 const reader = resp.body.getReader(); // 我們使用循環(huán)來讀取流的數(shù)據(jù) while (1) { // 讀取流是需要時(shí)間的,所以我們等待一下 // 返回值是一個(gè)對(duì)象,我們結(jié)構(gòu)出來得到兩個(gè)值 // value 是當(dāng)前流的數(shù)據(jù),done 是流數(shù)據(jù)我們是否讀取完畢 const { value, done } = await reader.read(); // 如果說取完了就不再循環(huán)了 if (done) { break; } // 我們打印一下流的數(shù)據(jù) console.log("value >>> ", value); } // 暫時(shí)禁用,不讓 Promise 完成 // resolve(body); }); } 可以看到流數(shù)據(jù)在不停的被打印,每打印一次就像是可讀流里盛出的一杯水,每一杯水的量是不同的,它會(huì)根據(jù)你的網(wǎng)絡(luò)傳輸情況和你系統(tǒng)處理速度有關(guān)系,所以我們只要得到這個(gè)每一次讀取的量相加在一起,就得到了當(dāng)前讀取的量。 我們來繼續(xù)寫一下。 export function request(options = {}) { // 在配置里加入一個(gè) 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(); // 聲明一個(gè)變量用來儲(chǔ)存讀取的量 let loaded = 0; // promise 最后的完成需要把所有的數(shù)據(jù)拼接起來返回 // 所以定一個(gè)變量用來儲(chǔ)存數(shù)據(jù)拼接的值 let body = ""; // 這個(gè)數(shù)據(jù)可能是二進(jìn)制,那就要使用 arrayBuffer // 也可能是文本,就要使用文本解碼器 // 比如說我們這里是文本,我們先定一個(gè)解碼器 const decoder = new TextDecoder(); while (1) { const { value, done } = await reader.read(); if (done) { break; } // 每一次讀取都累加起來 loaded += value.length; // 每一次讀取都對(duì)數(shù)據(jù)解碼并拼接起來 body += decoder.decode(value); // 當(dāng)然在每一次讀取的時(shí)候都要像 xhr 一樣,把總量和讀取量返回 onProgress && onProgress({ loaded, total, }); } // Promise 完成并返回?cái)?shù)據(jù) resolve(body); }); } 代碼搞定了我們看一下結(jié)果。 擴(kuò)展下載的進(jìn)度我們都實(shí)現(xiàn)了,那么你有沒有思考過,上傳怎么辦?按照邏輯來說下載和上傳應(yīng)該是一樣的,就是反著來的而已。 我們先來說 xhr,xhr 中就比較簡單。 // xhr 中給我們提供了一個(gè)事件叫 upload // upload 里有一個(gè)事件叫 progress, upload 里的 progress 事件只監(jiān)聽請(qǐng)求。 // 它的事件 e 里仍然提供了 // e.loaded 和 e.total // 所以 xhr 中實(shí)現(xiàn)上傳就比較簡單 xhr.upload.addEventListener("progress", (e) => {}); 我們?cè)趤碚f一下 fetch,遺憾的是 fetch 中實(shí)現(xiàn)不了請(qǐng)求進(jìn)度。 有的同學(xué)會(huì)說,響應(yīng)是一個(gè) response 對(duì)象,它里邊有 body 可以拿到讀取器,可以一部分一部分的讀,那么請(qǐng)求不就是一個(gè) request 對(duì)象嗎?它里邊不也有 body 嗎?不也可以一部分一部分讀嗎? 這是不行的,子辰盡量給同學(xué)解釋一下,聽不懂也沒關(guān)系。 我們知道,無論是請(qǐng)求或者響應(yīng),它的 body 屬性的類型都是一個(gè)叫做 ReadableStream 的可讀流。 這種可讀流都有一個(gè)特點(diǎn),就是在同一時(shí)間只能被一個(gè)人讀取,那么你想想,請(qǐng)求里的流是不是被瀏覽器讀取了?瀏覽器把這個(gè)流讀出來,然后發(fā)送到了服務(wù)器,所以說我們就讀不了了,就是這個(gè)問題。 而且瀏覽器在讀的過程中又不告訴我們它讀了多少,但是目前 W3C 正在討論一種方案,這種方案是附帶在 ServiceWorker 里邊的,它里邊有一套 API 叫做,BackgroundFetchManager目前這套 API 里可以實(shí)現(xiàn)請(qǐng)求進(jìn)度的監(jiān)聽,但是這套 API 還在試驗(yàn)中,不能用于生產(chǎn)環(huán)境。
該文章在 2023/11/27 11:45:02 編輯過 |
關(guān)鍵字查詢
相關(guān)文章
正在查詢... |