前段時間有個需求,需要前端匯出excel。一般來說,對於匯出大量資料的功能,最好還是交給後端來做,然而後端老哥並不想做(撕逼失敗),只能自力更生。
前端匯出excel本身已經有很成熟的庫了,比如js-xlsx, js-export-excel,所以實現起來並不難。但是,當匯出的資料達到幾萬條時,就會發現頁面產生了明顯的卡頓。原因也很簡單: 一般我們都是基於後端返回的json資料來生成excel,但是後端返回的資料一般都不能直接用來生成資料,我們還需要進行一些格式化:
const list = await request('/api/getExcelData');
const format = list.map((item) => {
// 對返回的json資料進行格式化
item.time = moment(item.time).format('YYYY-MM-DD HH:mm');
// ... 省略其他各種操作
});
// 根據json生成excel
const toExcel = new ExportJsonExcel(format).saveExcel();
複製程式碼
卡頓就發生在對大量資料進行map
操作。由於JS是單執行緒的,所以在進行大量複雜運算時會獨佔主執行緒,導致頁面的其他事件無法及時響應,造成頁面假死的現象。
那我們能不能把複雜的迴圈操作單獨放在一個執行緒裡呢?這時就要請出web worker了
Web Worker
首先看個簡單的例子
<button id="btn1">js</button>
<button id="btn2">worker</button>
<input type="text">
複製程式碼
index.js
const btn1 = document.getElementById('btn1');
btn1.addEventListener('click', function () {
let total = 1;
for (let i = 0; i < 5000000000; i++) {
total += i;
}
console.log(total);
})
複製程式碼
點選btn1時,js會進行大量計算,你會發現頁面卡死了,點選input不會有任何反應
我們使用web worker優化程式碼:
worker.js
onmessage = function(e) {
if (e.data === 'total') {
let total = 1;
for (let i = 0; i < 5000000000; i++) {
total += i;
}
postMessage(total);
}
}
複製程式碼
index.js
if (window.Worker) {
const myWorker = new Worker('worker.js');
myWorker.onmessage = function (e) {
console.log('total', e.data);
};
const btn1 = document.getElementById('btn1');
const btn2 = document.getElementById('btn2');
btn1.addEventListener('click', function () {
let total = 1;
for (let i = 0; i < 5000000000; i++) {
total += i;
}
console.log('total', total);
})
btn2.addEventListener('click', function () {
myWorker.postMessage('total');
});
}
複製程式碼
點選btn2時,頁面並不會卡死,你可以正常的對input進行輸入操作
我們開啟了一個單獨的worker執行緒來進行復雜操作,通過postMessage
和onmessage
來進行兩個執行緒間的通訊。
優化匯出excel表格
看過前面的例子,我們可以同理使用web worker進行復雜的map操作
worker.js
onmessage = function(e) {
const format = e.data.map((item) => {
// 對返回的json資料進行格式化
item.time = moment(item.time).format('YYYY-MM-DD HH:mm');
// ... 省略其他各種操作
});
postMessage(format);
}
複製程式碼
const myWorker = new Worker('worker.js');
myWorker.onmessage = function (e) {
// 根據json生成excel
const toExcel = new ExportJsonExcel(e.data).saveExcel();
};
const list = await request('/api/getExcelData');
myWorker.postMessage(list);
複製程式碼
當然實際專案,我們一般都是用webpack打包的,這時就要進行一些特別處理,需要使用worker-loader,可以參考《怎麼在 ES6+Webpack 下使用 Web Worker》文章學習。
進一步優化
在上面的程式碼修改中,我們只是優化了業務邏輯裡面的map操作。因為我使用的js庫是js-export-excel
,從它的原始碼裡可以看見,對於我們傳進來的資料,它還會再一次forEach迴圈操作,進行資料的二進位制轉換。因此,這一步的forEach迴圈,理論上也可以在web worker裡面進行操作。
最簡單想到的方法是:
worker.js
onmessage = function(e) {
const format = e.data.map((item) => {
// 對返回的json資料進行格式化
item.time = moment(item.time).format('YYYY-MM-DD HH:mm');
// ... 省略其他各種操作
});
// 直接在worker裡面生成excel
const toExcel = new ExportJsonExcel(format).saveExcel();
}
複製程式碼
直接在worker.js
裡面生成excel。然而,saveExcel
這個方法需要用到document
物件,但是在worker裡,我們不能訪問類似window
document
的全域性物件。
因此,只能魔改原始碼了。。。
真正用到document
物件的是原始碼這一句:
// saveAs和Blob用到了document
saveAs(
new Blob([s2ab(wbout)], {
type: "application/octet-stream"
}),
_options.fileName + ".xlsx"
);
複製程式碼
saveExcel
方法只需改成:
// 不生成excel,只返回資料
return s2ab(wbout);
複製程式碼
worker.js
onmessage = function(e) {
const format = e.data.map((item) => {
// 對返回的json資料進行格式化
item.time = moment(item.time).format('YYYY-MM-DD HH:mm');
// ... 省略其他各種操作
});
// saveExcel只返回blob資料
const blob = new ExportJsonExcel(format).saveExcel();
postMessage(blob);
}
複製程式碼
index.js
myWorker.onmessage = function (e) {
// 在主執行緒生成excel
saveAs(
new Blob([e.data], {
type: "application/octet-stream"
}),
"test.xlsx"
);
};
複製程式碼
原理就是:我們只把資料轉換放在worker裡,最後生成excel仍然在主執行緒裡完成。
至此,優化完成了!
總結
我們可以把一些耗效能的操作放在worker執行緒裡(比如大檔案上傳),這樣主執行緒就能及時響應使用者操作而不會造成卡頓現象。需要注意的是,在worker裡進行的複雜計算,執行時間並不會變短,有時耗費時間甚至更長,畢竟開啟worker也需要消耗一定的效能。