如何實現 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 編輯過 |
關鍵字查詢
相關文章
正在查詢... |