最近業務上有資料大屏的需求,要求不僅能展示資料,同時能提供所選日期範圍的資料下載。本文純記錄實現方案作為筆記,實現細節十分不完備。
工具庫
xlsx電子表格的標準規範詳見:Full XML Schema。下面兩個庫都基於這個規範實現了這類格式檔案的讀寫(salute!∠(°ゝ°))。
SheetJS
SheetJS是用於多種電子表格格式的解析器和編寫器。通過官方規範、相關文件以及測試檔案實現簡潔的JS方法。SheetJS強調解析和編寫的穩健,其跨格式的特點和統一的JS規範相容,並且ES3/ES5瀏覽器向後相容IE6。
excelize
Go語言編寫的可以讀寫電子表格型別檔案的公共庫
更靜默:webworker
Web Worker為Web內容在後臺執行緒中執行指令碼提供了一種簡單的方法。執行緒可以執行任務而不干擾使用者介面。
我們將SheetJS處理資料、生成表格資料(book型別資料)的流程另起一個執行緒實現。(雖然另起一個執行緒從體驗上不會過度影響主UI執行緒,但本身啟動成本比較高)。
該元件的目錄如下
NewDashboard
├── components
│ ├── LongCard
│ │ ├── echartsOption.ts
│ │ ├── index.tsx
│ │ └── style.module.less
│ └── ShortCard
│ ├── echartsOption.ts
│ ├── index.tsx
│ └── style.module.less
├── index.tsx # 在該檔案與webworker通訊
├── makeTable.ts # 在該檔案實現webworker
└── style.module.less
mdn給的samples的worker都是載入外部程式碼的。在我們這種組織目錄下,worker應該在一個檔案內實現,並匯出一個worker例項。這裡需要藉助URL.createObjectURL(blob)構造一個外鏈。
程式碼如下:
// @file makeTable.ts
const blob = new Blob(
[
`
importScripts('https://g.alicdn.com/code/lib/xlsx/0.17.4/xlsx.full.min.js');
const GOODS_EFFECT_TITLE = [
'開播時間',
'下播時間',
'直播間',
'商品名稱',
'商品',
'點選人數',
'成交人數',
'粉絲成交比例',
'引導成交金額',
];
// 接收主程式的表格資料
onmessage = function({ data }) {
console.log('from main routine', data);
const book = XLSX.utils.book_new();
const sheet = XLSX.utils.aoa_to_sheet([GOODS_EFFECT_TITLE, ...data]);
XLSX.utils.book_append_sheet(book, sheet, '工作表1');
// book的資料回傳給主程式
postMessage({ book });
};
`,
],
{ type: 'text/javascript' },
);
export const worker = new Worker(URL.createObjectURL(blob));
注意幾個點:
- 由於在worker內沒有DOM、windows等物件,所以沒有辦法直接使用 XLSX.utils.table_to_book 方法將table元素直接匯出為xlsx表格資料。
- importScript 方法是並行載入所有列出的資源,但執行是同步的。這裡需要將SheetJS的資源載入進worker裡。
- 主程式的方法:
// @file index.tsx
import { worker } from './makeTable';
function download() {
// aoa_to_sheet 方法需要一個二維陣列來形成電子表格
worker.postMessage([[1, 2, 3]]);
worker.onmessage = ({ data }) => {
window.XLSX.writeFile(data.book, '測試.xlsx');
};
}
更高速:WebAssembly
對於網路平臺而言,WebAssembly具有巨大的意義——它提供了一條途徑,以使得以各種語言編寫的程式碼都可以以接近原生的速度在Web中執行。在這種情況下,以前無法以此方式執行的客戶端軟體都將可以執行在Web中。
我們使用Go語言編譯為wasm檔案,核心程式碼如下:
// wasm.go
func main() {
c := make(chan struct{}, 0)
// js全域性方法makeExcel
js.Global().Set("makeExcel", js.FuncOf(jsMakeExcel))
// 確保Go程式不退出
<-c
}
func makeExcel() []uint8 {
f := excelize.NewFile()
f.SetCellValue("Sheet1", "開播時間", now.Format(time.ANSIC))
f.SetCellValue("Sheet1", "直播間", 1111)
// 在js環境中無法實現檔案的操作
// if err := f.SaveAs("simple.xlsx"); err != nil {
// log.Fatal((err))
// }
buf, _ := f.WriteToBuffer()
res := make([]uint8, buf.Len())
buf.Read(res)
return res
}
func jsMakeExcel(arg1 js.Value, arg2 []js.Value) interface{} {
buf := makeExcel()
js_uint := js.Global().Get("Uint8Array").New(len(buf))
js.CopyBytesToJS(js_uint, buf)
//go的uint8無法直接回傳,需要建立js環境的Uint8Array型別資料並回傳
return js_uint
}
將編譯好的wasm檔案載入進js環境
- 引入橋接程式碼:https://github.com/golang/go/...。此時window下會有一個全域性建構函式:Go
- 樣板程式碼——例項化webassembly:
// WebAssembly.instantiateStreaming is not currently available in Safari
if (WebAssembly && !WebAssembly.instantiateStreaming) {
// polyfill
WebAssembly.instantiateStreaming = async (resp, importObject) => {
const source = await (await resp).arrayBuffer();
return await WebAssembly.instantiate(source, importObject);
};
}
const go = new Go();
fetch('path/to/wasm.wasm')
.then((response) => response.arrayBuffer())
.then((bytes) => WebAssembly.instantiate(bytes, go.importObject))
.then((res) => go.run(res.instance))
- 實現檔案下載
function download() {
// 與普通方法一樣呼叫go寫入全域性的方法,拿到剛剛回傳的uint8array資料
const buf = makeExcel();
// 建立下載連結,注意檔案型別,並下載檔案
const blob = new Blob([buf], {
type: 'application/vnd.ms-excel',
});
const url = URL.createObjectURL(blob);
console.log({ blob, str });
const a = document.createElement('a');
a.download = 'test.xlsx';
a.href = url;
a.click();
}
既要又要
webworker和webassembly是可以一起使用的,待補充……