這裡給大家分享我在網上總結出來的一些知識,希望對大家有所幫助 問題描述:最近工作中出現一個需求,純前端下載 Excel 數據,並且有的下載內容很多,這時需要給下載增加一個 loading 效果。 代碼如下: // utils.js const XLSX = require('xlsx') // 將一 ...
這裡給大家分享我在網上總結出來的一些知識,希望對大家有所幫助
問題描述:最近工作中出現一個需求,純前端下載 Excel 數據,並且有的下載內容很多,這時需要給下載增加一個 loading 效果。
代碼如下:
// utils.js const XLSX = require('xlsx') // 將一個sheet轉成最終的excel文件的blob對象,然後利用URL.createObjectURL下載 export const sheet2blob = (sheet, sheetName) => { sheetName = sheetName || 'sheet1' var workbook = { SheetNames: [sheetName], Sheets: {} } workbook.Sheets[sheetName] = sheet // 生成excel的配置項 var wopts = { bookType: 'xlsx', // 要生成的文件類型 bookSST: false, // 是否生成Shared String Table,官方解釋是,如果開啟生成速度會下降,但在低版本IOS設備上有更好的相容性 type: 'binary' } var wbout = XLSX.write(workbook, wopts) var blob = new Blob([s2ab(wbout)], { type: 'application/octet-stream' }) // 字元串轉ArrayBuffer function s2ab(s) { var buf = new ArrayBuffer(s.length) var view = new Uint8Array(buf) for (var i = 0; i !== s.length; ++i) view[i] = s.charCodeAt(i) & 0xff return buf } return blob } /** * 通用的打開下載對話框方法,沒有測試過具體相容性 * @param url 下載地址,也可以是一個blob對象,必選 * @param saveName 保存文件名,可選 */ export const openDownloadDialog = (url, saveName) => { if (typeof url === 'object' && url instanceof Blob) { url = URL.createObjectURL(url) // 創建blob地址 } var aLink = document.createElement('a') aLink.href = url aLink.download = saveName + '.xlsx' || '1.xlsx' // HTML5新增的屬性,指定保存文件名,可以不要尾碼,註意,file:///模式下不會生效 var event if (window.MouseEvent) event = new MouseEvent('click') else { event = document.createEvent('MouseEvents') event.initMouseEvent( 'click', true, false, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null ) } aLink.dispatchEvent(event) } <el-button @click="clickExportBtn" > <i class="el-icon-download"></i>下載數據 </el-button> <div class="mongolia" v-if="loadingSummaryData"> <el-icon class="el-icon-loading loading-icon"> <Loading /> </el-icon> <p>loading...</p> </div> clickExportBtn: _.throttle(async function() { const downloadDatas = [] const summaryDataForDownloads = this.optimizeHPPCDownload(this.summaryDataForDownloads) summaryDataForDownloads.map(summaryItem => downloadDatas.push(this.parseSummaryDataToBlobData(summaryItem)) ) // donwloadDatas 數組是一個三維數組,而 json2sheet 需要的數據是一個二維數組 this.loadingSummaryData = true const downloadBlob = aoa2sheet(downloadDatas.flat(1)) openDownloadDialog(downloadBlob, `${this.testItem}報告數據`) this.loadingSummaryData = false }, 2000), // css .mongolia { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background-color: rgba(0, 0, 0, 0.9); display: flex; justify-content: center; align-items: center; font-size: 1.5rem; color: #409eff; z-index: 9999; } .loading-icon { color: #409eff; font-size: 32px; }
解決方案探究:
-
在嘗試了使用 $nextTick、將 openDownloadDialog 改寫成 Promise 非同步函數,或者使用 async/await、在 openDownloadDialog 中添加 loadingSummaryData 邏輯,發現依舊無法解決問題,因此懷疑是 document 添加新元素與 vue 的 v-if 渲染產生衝突,即 document 添加新元素會阻塞 v-if 的執性。查閱資料發現,問題可能有以下幾種:
-
openDownloadDialog 在執行過程中執行了較為耗時的同步操作,阻塞了主線程,導致了頁面渲染的停滯。
-
openDownloadDialog 的 click 事件出發邏輯存在問題,阻塞了事件迴圈(Event Loop)。
-
瀏覽器在執行 openDownloadDialog 時,將其腳本任務的優先順序設置得較高,導致占用主線程時間片,推遲了其他渲染任務。
-
Vue 的批量更新策略導致了 v-if 內容的顯示被延遲。
-
-
查閱資料後找到瞭如下幾種方案:
1.使用 setTimeout 使 openDownloadDialog 非同步執行
clickExport() { this.loadingSummaryData = true; setTimeout(() => { openDownloadDialog(downloadBlob, `${this.testItem}報告數據`); this.loadingSummaryData = false; }); }
2.對 openDownloadDialog 內部進行優化
-
避免大迴圈或遞歸邏輯
-
將計算工作分批進行
-
使用 Web Worker 隔離耗時任務
在編寫 downloadWorker.js 中的代碼時,要明確這部分代碼是運行在一個獨立的 Worker 線程內部,而不是主線程中。
-
不要直接依賴或者訪問主線程的全局對象,比如 window、document 等。這些在 Worker 內都無法直接使用。
-
不要依賴 DOM 操作,比如獲取某個 DOM 元素。Worker 線程無法訪問頁面的 DOM。
-
代碼執行的入口是 onmessage 回調函數,在其中編寫業務邏輯。
-
和主線程的通信只能通過 postMessage 和 onmessage 發送消息事件。
-
代碼應該是自包含的,不依賴外部變數或狀態。
-
可以導入其他腳本依賴,比如用 import 引入工具函數等。
-
避免修改或依賴全局作用域,比如定義全局變數等。
總之,編寫 downloadWorker.js 時要註意它執行在一個獨立環境中,只能通過消息事件與主線程通信。
// 創建 Worker const worker = new Worker('downloadWorker.js'); // 點擊下載時向 Worker 發送消息 function clickDownload() { showLoading(); worker.postMessage({ url: fileURL, filename: 'report.xlsx' }); worker.onmessage = function(e) { // 收到下載完成的消息 hideLoading(); } } // 顯示 loading function showLoading() { loading.style.display = 'block'; } // 隱藏 loading function hideLoading() { loading.style.display = 'none'; } // downloadWorker.js onmessage = function(e) { const url = e.data.url; const filename = e.data.filename; // 創建並點擊鏈接觸發下載 const a = document.createElement('a'); a.href = url; a.download = filename; a.click(); postMessage('下載完成'); } <div id="loading" style="display:none;"> Downloading... </div> <button onclick="clickDownload()"> Download </button> <script src="downloadWorker.js"></script>
3.使用 requestIdleCallback 進行調度
clickExport() { this.loadingSummaryData = true; requestIdleCallback(() => { openDownloadDialog(downloadBlob, `${this.testItem}報告數據`); this.loadingSummaryData = false; }); }
當使用上述方案時發現裡面的內容變得異常的慢,於是去問GPT後,得到以下答案:
使用 requestIdleCallback 來執行 openDownloadDialog 確實可能會需要更多時間。
因為 requestIdleCallback 會在主線程空閑時才執行回調函數,所以不像 setTimeout 可以直接按指定時間非同步執行。
requestIdleCallback 的優先順序較低,只在主線程空閑時才會執行,所以 download 的速度會比 setTimeout 慢一些。
但使用 requestIdleCallback 的優點是它不會占用主線程時間,可以避免阻塞頁面渲染。
所以需要權衡執行速度和避免阻塞之間的關係:
-
如果下載速度是優先考慮的因素,可以繼續使用 setTimeout 非同步執行。
-
如果避免阻塞渲染更重要,則 requestIdleCallback 是更好的選擇。
偶爾下載任務被延遲不是大問題,但頻繁的下載任務阻塞會嚴重影響頁面體驗。
此外,可以結合兩者試試:
clickExport() { this.loadingSummaryData = true; setTimeout(() => { requestIdleCallback(() => { openDownloadDialog(downloadBlob); }); this.loadingSummaryData = false; }, 200); }
setTimeout 先非同步調度,然後在回調中使用 requestIdleCallback 避免阻塞。這樣就可以兼顧下載速度和避免渲染阻塞了。
分析線程模型,找到具體的阻塞點
使用 Performance 工具分析線程
debugger 及 console.log 列印關鍵函數的執行時間
檢查是否有非主線程的任務阻塞了主線程
調整 vue 的批量更新策略
new Vue({ config: { // 修改批量更新的隊列長度 batchUpdateDuration: 100 } })