前段時間有個需求,須要前端導出excel。通常來講,對於導出大量數據的功能,最好仍是交給後端來作,然然後端老哥並不想作(撕逼失敗),只能自力更生。javascript
前端導出excel自己已經有很成熟的庫了,好比js-xlsx, js-export-excel,因此實現起來並不難。可是,當導出的數據達到幾萬條時,就會發現頁面產生了明顯的卡頓。緣由也很簡單: 通常咱們都是基於後端返回的json數據來生成excel,可是後端返回的數據通常都不能直接用來生成數據,咱們還須要進行一些格式化:html
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了java
首先看個簡單的例子webpack
<button id="btn1">js</button>
<button id="btn2">worker</button>
<input type="text">
複製代碼
index.js
git
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不會有任何反應github
咱們使用web worker優化代碼:web
worker.js
json
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
來進行兩個線程間的通訊。
看過前面的例子,咱們能夠同理使用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也須要消耗必定的性能。