theme: channing-cyan
hightlight: channing-cyan
前言
在我們公司表格數據導出都是前端去處理。一開始數據量不大,倒沒什么問題。但隨著數據量的加大,問題也逐漸暴露出來。
一天的數據量有一來萬條,導出一定時間范圍的數據,30天就得30來萬條數據。
那會測試直接給我導出 60 萬條數據都存到一個 Excel 表中,頁面直接卡死掉,動都動不了,后面直接崩潰掉。
那會為什么導出選擇由前端去做呢?
- 多語言問題:有些內置數據(如:文件分類,計算機組等信息)需要支持多語言,以及表格 header 頭。
- 數據轉換問題:有些內置數據返回的是數值類型,需要轉成對應的真正的數據。
- 導出表格字段問題:用戶可以通過切換列來控制具體導出哪些字段。
排除原因
經過排查:導出大量數據通常涉及大量的計算、DOM 操作或文件生成等復雜操作,這些操作會在主線程中執行。如果這些操作耗時過長,主線程會被阻塞,導致頁面無法響應用戶交互(如點擊、滾動等),表現為頁面卡死。
那是否把這些大量的計算、DOM 操作或文件生成等復雜操作,放到子進進程去處理,不就解決了嗎?
這就說到了今天的主角:Web Workers
Web Workers 介紹
Web Workers 使得一個Web應用程序可以在與主線程分離的后臺線程中運行一個腳本。
這樣做的好處在于可以在一個單獨的線程中執行費時的處理任務,從而允許主(通常是UI)線程運行而不被阻塞。
它的作用就是給JS創造多線程運行環境,允許主線程創建worker線程,分配任務給后者,主線程運行的同時worker線程也在運行,相互不干擾,在worker線程運行結束后把結果返回給主線程。這樣做的好處是主線程可以把計算密集型或高延遲的任務交給worker線程執行,這樣主線程就會變得輕松,不會被阻塞或拖慢。這并不意味著JS語言本身支持了多線程能力,而是瀏覽器作為宿主環境提供了JS一個多線程運行的環境。
不過因為worker一旦新建,就會一直運行,不會被主線程的活動打斷,這樣有利于隨時響應主線程的通性,但是也會造成資源的浪費,所以不應過度使用,用完注意關閉。或者說:如果worker無實例引用,該worker空閑后立即會被關閉;如果worker實列引用不為0,該worker空閑也不會被關閉。
Web Workers 使用
- 創建 Worker 對象:通過
new Worker(url)
創建一個 Worker 對象,這里的url
指向你預先編寫的 JavaScript 文件路徑,這個文件內包含 Workers 將要執行的腳本內容。 - 發送消息:你可以使用
worker.postMessage(message)
方法從主腳本向 Worker 發送數據。 - 處理 Worker 發送的消息:在主腳本中,設置
worker.onmessage
事件監聽器來處理 Worker 發回來的數據。 - 終止 Worker:如果不再需要 Worker,可以調用
worker.terminate()
方法來停止 Worker。 - 監聽錯誤:可以通過添加
onerror
事件監聽器來處理 Worker 中可能出現的錯誤。
主線程腳本
const myWorker = new Worker('worker.js')const nums = [10, 20]myWorker.postMessage(nums)myWorker.onmessage = function(e) {result = e.dataconsole.log('主進程接收子進程傳遞回來的數據:', e.data)// 停止 Workerworker.terminate()}myWorker.onerror = function(e) {console.log('監聽錯誤')}
Worker 腳本
onmessage = function(e) {var data = e.data;var result = data[0] * data[1];postMessage(result);
}
Web Workers 實戰 Excel 導出
基本案例有了,但還是遇到一些坑。下面開始一個個填坑。
問題1:vue 項目如何配置 web worker
這里需要下載第三方 loader
, 來編譯 workers 腳本。
npm install worker-loader@3.0.8
接下來,修改 vue.config.js
文件:
// vue.config.js
module.exports = {chainWebpack(config) {config.module.rule('worker').test(/\.worker\.js$/).use('worker-loader').loader('worker-loader').options({}).end() }
}
注意:test()
設置了文件名后綴是 .worker.js
則為 worker 腳本文件
。
到這里第一個問題就解決了。。。
問題2:修改了 web worker 后,重新編譯打包沒有生效
vue項目一改動到代碼文件就會重新編譯。
但在調試過程中,修改了 worker 腳本,發現一直沒有修復到問題,一開始也是很懷疑自己是不是邏輯出錯了。
通過 debug 才發現,代碼一直沒有修改。
后面每次修改 worker 腳本,都會重新啟動 vue 項目,一開始問題是解決了。
但偶爾還是會沒有修改到代碼。
最終排查到:原來是每次重新編譯時,要刪除掉 node_modules
目錄下的 .cache
文件夾
才會重新加載新 worker 腳本代碼
問題3:主進程向子進程發送參數時,若參數存在對象,會報錯
這里主要是生產 csvData
數據(key: value
)中的 value
是一個對象結構時,發送給到 子進程,瀏覽器會報錯。
這里解決方法是:將 value
進行序列化處理
// * 判斷 csvData 中的值是否存在對象,需要序列化處理
const keys = csvHeader.map(item => item.key)
csvData = csvData.map(row => {return keys.reduce((acc, prev) => {acc[prev] = typeof row[prev] === 'object' ? JSON.stringify(row[prev]) : row[prev]return acc}, {})
})
問題4:在子進程中下載文件失敗
由主進程去結合實際業務邏輯生成 csvHeader
、csvData
數據后,發送給到子進程,由其生成 Excel 文件流,并下載下來。
// 主進程
const { csvHeader, csvData } = generateExcelData(data)// 子進程
import Excel from 'exceljs'
self.onmessage = async function(e) {const { csvData, csvHeader } = e.dataconst workbook = new Excel.Workbook()const worksheet = workbook.addWorksheet('My Sheet')worksheet.columns = csvHeadercsvData.forEach(row => worksheet.addRow(row))// 生成 Excel 文件的 Bufferconst excelBuffer = await workbook.xlsx.writeBuffer()// TODO 下載文件
}
經過調試發現文件下載不下來,查閱資料得出:
主要原因在于 Web Workers 的設計限制。具體來說,Web Workers 沒有直接訪問瀏覽器的 DOM 和一些與用戶界面交互的功能,包括文件下載。
所以這里只能將 Excel 文件的 Buffer轉成blog發送給到主進程進行文件下載。
主進程
import { saveAs } from 'file-saver'
import ExportWorker from './export.worker.js'
const worker = new ExportWorker()
worker.postMessage({csvData: csvData,csvHeader: csvHeader
})worker.onmessage = async(e) => {const { chunk: blog } = e.datasaveAs(blog, filename)
}
worker 腳本
import Excel from 'exceljs'self.onmessage = async function(e) {const { csvData, csvHeader } = e.dataconst workbook = new Excel.Workbook()const worksheet = workbook.addWorksheet('My Sheet')worksheet.columns = csvHeadercsvData.forEach(row => worksheet.addRow(row))// 生成 Excel 文件的 Bufferconst excelBuffer = await workbook.xlsx.writeBuffer()const blob = new Blob([excelBuffer], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' })self.postMessage({ chunk: blob })
}
源碼
主進程
import { saveAs } from 'file-saver'
import ExportWorker from './export.worker.js'
/**
* 導出數據為 XLSX(通過 web Worker)
* @param {Object} csvHeader XLSX 頭
* @param {Array} csvData 數據
* @param {String} filename 文件名
*/
const exportDataToXLSXByWorker = (csvHeader, csvData, filename) => {const worker = new ExportWorker()// * 判斷 csvData 中的值是否存在對象,需要序列化處理const keys = csvHeader.map(item => item.key)csvData = csvData.map(row => {return keys.reduce((acc, prev) => {acc[prev] = typeof row[prev] === 'object' ? JSON.stringify(row[prev]) : row[prev]return acc}, {})})worker.postMessage({csvData: csvData,csvHeader: csvHeader})worker.onmessage = async(e) => {const { chunk: blog } = e.datasaveAs(blog, filename)}
}
worker 腳本
import Excel from 'exceljs'self.onmessage = async function(e) {const { csvData, csvHeader } = e.dataconst workbook = new Excel.Workbook()const worksheet = workbook.addWorksheet('My Sheet')worksheet.columns = csvHeadercsvData.forEach(row => worksheet.addRow(row))// 生成 Excel 文件的 Bufferconst excelBuffer = await workbook.xlsx.writeBuffer()const blob = new Blob([excelBuffer], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' })self.postMessage({ chunk: blob })
}