純前端生成Excel檔案騷操作——WebAssembly & web workers

Marckon發表於2021-12-04

 

最近業務上有資料大屏的需求,要求不僅能展示資料,同時能提供所選日期範圍的資料下載。本文純記錄實現方案作為筆記,實現細節十分不完備。

工具庫

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));

注意幾個點:

  1. 由於在worker內沒有DOM、windows等物件,所以沒有辦法直接使用 XLSX.utils.table_to_book 方法將table元素直接匯出為xlsx表格資料。
  2. importScript 方法是並行載入所有列出的資源,但執行是同步的。這裡需要將SheetJS的資源載入進worker裡。
  3. 主程式的方法:
  // @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環境

  1. 引入橋接程式碼:https://github.com/golang/go/...。此時window下會有一個全域性建構函式:Go
  2. 樣板程式碼——例項化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))
  1. 實現檔案下載
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是可以一起使用的,待補充……

相關文章