業務背景
在短影片app原始碼前後端資料互動場景下,使用最多的一種方式是客戶端發起 HTTP 請求,等待服務端處理完成後響應給客戶端結果。
但在一些場景下,短影片app原始碼服務端對資料的處理需要較長的時間,比如提交一批資料,對這批資料進行資料分析,將最終分析結果返回給前端。
如果採用一次 HTTP 請求,使用者會一直處於等待狀態,再加上介面不會有進度互動,導致使用者不知何時會處理完成;此外,一旦重新整理頁面或者其他意外情況,使用者就無從感知處理結果。
面對這類場景,可以藉助 「HTTP 輪詢方式」 對互動體驗進行最佳化,具體過程如下:
首先發起一次 HTTP 請求用於提交資料,之後啟動輪詢在一定間隔時間內查詢分析結果,在這期間後臺可將分析進度同步到前端來告知使用者處理進度;此外即使重新整理再次進入頁面還可以透過「輪詢」實時查詢進度結果。
下面,我們來看看程式碼層面看如何實現這類場景。
JS 實現輪詢的方式
在實現程式碼之前,我們需要先明確 JS 實現輪詢的方式有哪些,哪種方式最適合使用。
1. setInterval
作為前端開發人員,提起輪詢第一時間能想到的是計時器 setInterval,它會按照指定的時間間隔不間斷的輪詢執行處理函式。
let index = 1; setInterval(() => { console.log('輪詢執行: ', index ++); }, 1000);
回過頭來看我們的場景:要輪詢的是 非同步請求(HTTP),請求響應結果會受限制網路或者短影片app原始碼的伺服器處理速度,顯然 setInterval 這種固定間隔輪詢並不適合這個場景。
2. Promise + setTimeout sleep
setInterval 的不足之處在於 輪詢間隔時間 在非同步請求場景下無法保證兩個請求之間的間隔固定。要解決這個問題,可以使用 sleep 睡眠函式來控制間隔時間。
JS 中沒有提供 sleep 相關方法,但可以結合 Promise + setTimeout 來實現。
const sleep = () => { return new Promise(resolve => { setTimeout(resolve, 1000); }); }
sleep 僅控制了輪詢間隔,而輪詢的執行機制需要我們手動根據非同步請求結果來實現,比如下面透過控制 while 迴圈的條件:
const start = async () => { let i = 0; while (i < 5) { await sleep(); console.log(`第 ${++ i} 次執行`); } } start();
使用輪詢的時候可以藉助 async/await 同步的方式編寫,提高程式碼閱讀質量。
實現非同步請求輪詢
下面我們透過一個完整示例理解 輪詢非同步請求 的實現及使用注意事項。
首先我們定義兩個變數:index 用於控制何時停止輪詢,timer 則用於實現中斷輪詢。
let index = 1; let timer = 0;
這裡,我們定義 syncPromise 來模擬非同步請求,可以看作是一次 HTTP 請求,當進行 5 次非同步請求後,會返回 false 表示拿到資料分析結果,停止資料查詢輪詢:
const syncPromise = () => { return new Promise(resolve => { setTimeout(() => { console.log(`第 ${index} 次請求`); resolve(index < 5 ? true : false); index ++; }, 50); }) }
現在,我們實現 pollingPromise 作為 sleep 睡眠函式使用,去控制輪詢的間隔時間,並在指定時間執行非同步請求:
const pollingPromise = () => { return new Promise(resolve => { timer = setTimeout(async () => { const result = await syncPromise(); resolve(result); }, 1000); }); }
最後,startPolling 作為開始輪詢的入口,包含以下邏輯:
1)在輪詢前會清除正在進行的輪詢任務,避免出現多次輪詢;
2)如果需要,在開始輪詢時會立刻呼叫非同步請求查詢一次資料結果;
3)最後,透過 while 迴圈根據非同步請求的結果,決定是否繼續輪詢;
const startPolling = async () => { // 清除進行中的輪詢,重新開啟計時輪詢 clearTimeout(timer); // !!! 注意:清除計時器後,會導致整個 async/await 鏈路中斷,若計時器的位置下方還存在程式碼,將不會執行。 index = 1; // 立刻執行一次非同步請求 let needPolling = await syncPromise(); // 根據非同步請求結果,判斷是否需要開啟計時輪詢 while (needPolling) { needPolling = await pollingPromise(); } console.log('輪詢請求處理完成!'); // 若非同步請求被 clearTimeout(timer),這裡不會被執行列印輸出。 } const start = async () => { await startPolling(); console.log('若非同步請求被 clearTimeout(timer),這裡將不會被執行'); } start();
不過,需要注意的是:一旦清除計時器後,會導致整個 async/await 鏈路中斷,若計時器的位置下方還存在程式碼,將不會執行。
假設當前執行了兩次輪詢被 clearTimeout(timer) 後,從 startPolling 到 start 整個 async/await 鏈路都會中斷,且後面未執行的程式碼也不會被執行。
基於以上規則,非同步輪詢的處理邏輯儘量放在 syncPromise 非同步請求核心函式中完成,避免在開啟輪詢的輔助函式中去實現。
使用輪詢的其他場景
在短影片app原始碼中,使用輪詢的場景還有很多。
通常我們考慮首屏載入速度,會將一些非主要啟動程式的資源改用 動態非同步 的方式去載入。
如果某個頁面渲染時依賴了非同步載入的指令碼資源,就會出現無法拿到資源變數導致報錯情況,因為這個時候其實資源還沒有載入完成。
所以就需要一種手段,在資源載入完成後再讓頁面拿去使用,輪詢指令碼資源變數是否存在可以作為一種處理方式。
/** * 輪詢查詢非同步資源的載入狀態 */ export const pollingAsyncResource = (handler: Function, condition: () => unknown) => { let timer = 0; if (!!condition()) { // 靜態資源已載入完成 handler(); } else { // 啟動輪詢查詢靜態資源載入狀態 timer = window.setInterval(() => { if (condition()) { // 靜態資源已載入完成 clearInterval(timer); handler(); } }, 50); } } // 使用 pollingAsyncResource( () => // 使用資源的具體邏輯, () => window.$ );
在 pollingAsyncResource 中啟動 計時器輪詢 去查詢靜態資源是否載入完成。
引數 handler 是資源載入完成後要執行的具體邏輯;condition 則是判斷資源是否載入完成的條件,一般指令碼資源都採用 umd 模組形式在 window 物件上繫結全域性變數。
以上就是短影片app原始碼,藉助輪詢最佳化互動體驗, 更多內容歡迎關注之後的文章