[React Note] — 前端 Excel 匯出加上樣式之 xlsx-style 的踩坑深入淺出

Introduction & 前言

React Excel 踩坑之路

  • 如果你有需要使用前端匯出 Excel 且需要將 Excel 加上樣式,推薦參考這篇文章!

  • 如果你剛好使用 xlsx-style 碰到了套件本身的 Error 問題,更推薦參考這篇文章!

  • 如果你只是想單純看看前端怎麼使用套件將 Data、Table、JSON 匯出,超級推薦這篇,但請各路大神手下留情。

本篇文章為筆記使用,如果有任何錯誤的地方,還請手下留情,也歡迎在下方留言指教。


Summary & 摘要

這篇文章不會特別去比較各種 Excel 匯出的套件差異,單純會講解如何從這個需求一路到使用 xlsx-style 這個套件且踩坑一段時間,一路到成功解決這個問題。

如果你只想知道關於 xlsx-style./cptable 坑,歡迎跳至下方使用指南瀏覽。

不得不說前端之路真的很孤獨啊,筆者察覺到台中(不確定是不是只有台中,因為目前只在台中就職)的前端職缺超級超級超級缺,但職缺待遇卻都超級超級超級差,不曉得到底是沒有人才?!還是沒有好公司呢?!因為這個原因,所以每間公司的前端都少得可憐,能討論的人也都很有限…有空在寫一篇文章來談談這個狀況吧


需求

俗話說得好 - 『工程師的成長推力是需求,是PM,是客戶,是薪水』(開個小玩笑

因為在工作上剛好碰到後台需要產出 Excel 的需求,主管詢問前端能不能做到,以往都是請後端產出,但今天剛好有時間,就研究了一下該怎麼從前端產 Excel,而因為以前實作的產出結果是 CSV,但這次需要產出 .xlsx 還要加上樣式,所以心血來潮就研究了一下。

如果你剛好有這個需求,但不想要太過複雜,可以使用 d3.jscsv api 即可達成匯入匯出,甚至可以匯出圖表。

d3.js

在最後決定使用 xlsx-style 時,其實也考慮過 **exceljs**,但因為需求上需要在輸出後加上樣式,故改選前者,如果你並不需要更改樣式,可以選擇後者,仔細去 Github 瞧瞧,後者有中文介紹(對 筆者就是膚淺),且前者說真的文件不易讀,而且後來很多 Fork 出來相關的套件有很多隱藏的坑。


關於 xlsx 及 xlsx-style 的關係

xlsx-style

xlsx 就是 SheetJS,而 xlsx-style 是從 SheetJS fork 出來的套件,光看名字很容易會被搞混。

為什麼會用到 xlsx-style 呢?因為 xlsx(aka SheetJS,以下統一以 xlsx 稱呼之,避免搞混)免費版本不支援格式變更,例如:居中、自動換行…等等,所以後來出現了 xlsx-style


使用指南

Step1:事前準備

首先我們需要安裝相關套件

1
$ npm i xlsx xlsx-style file-saver -S

小提示: -S 是為了在 package.json 紀錄你安裝了這個套件,如果沒有加上去可能之後 Clone 專案的人在 npm install 的時候會少安裝這個套件,雖然 npm v5.0.0 後已經在安裝時預設加入這個指令,但筆者習慣性還是會加上;另外也可以在某些只有開發時會用到的套件安裝時加上 -D,意思是只有開發的時候(會放到 package.json 的 devDependencies)才會用到,例如 scss,因為編譯出來已經轉為 css,其他編譯出來還需要用到的請使用 -S(會放到 package.json 的 dependencies)。

這邊提一下為何會使用這三個套件,因為我們需要把 JSON 轉為 Sheet,就是 Excel 的格式,在網頁上 console.log 會如下圖。

sheet

接著因為我們需要加上樣式,讓輸出後的 Excel 可以有顏色或者居中等等的目的(因為不想付錢解鎖 xlsx 的進階功能),所以要透過 xlsx-style 去加上樣式。

最後的最後因為我們要在前端把檔案下載下來,而筆者很懶得再寫類似下面這種程式碼:

1
2
3
4
5
6
7
8
9
10
11
12
13
function downloadFileHandler(blob) {
// 建立一個 a 標籤供 插入檔案 及 點擊
var link = document.createElement('a');
// 插入檔案
link.href = window.URL.createObjectURL(blob);
link.download = `excel - ${new Date()}.xlsx`;
// 在網頁插入這個 DOM
document.body.appendChild(link);
// 點擊下載
link.click();
// 移除網頁上這個 DOM
document.body.removeChild(link);
}

所以裝了 file-saver 這個套件,只需要將上面程式碼改為下方程式碼:

1
2
3
4
5
// 引入套件
import { saveAs } from "file-saver";

// 將你的 blob 丟進來 並 下載
saveAs(blob, `excel - ${new Date()}.xlsx`);

所以說,俗話說得好 - 『正因為懶,造就出偉大的工程師』(沒錯又是我亂照的句子)。

Step2:Know Bug

惱人的 cptable 錯誤

你沒看錯,第二步就要你認識 Bug,這也是本篇文章的重點了,筆者不曉得原套件是否有解除了這個問題,但網路上爬文幾乎都能收尋到一種解法,那就是直接去修改 node_modules,但聰明的你一定馬上覺得怪怪的,如果別人 clone 你的專案下來怎麼跑呢?每次部署或是每次其他同事要使用你都要跟他們說或者寫一份警告文件嗎?

網路上的文章甚至 xlsx-styleissues 討論區都是這種解法,詳細可參考

看到後面因為瞧到了 Vue 的解法,但筆者使用的是 React,不過兩者原理應該相去不遠,所以筆者開始研究怎麼解開這個難題。

不要再改 807 行啦

請不要再直接改 807 行了…

Step3:Debug

打開終端機安裝套件:

1
$ npm i react-app-rewired customize-cra -S

這邊要先說說為什麼需要安裝 react-app-rewired 這個套件,而這個檔案是什麼;以往寫 Vue 都是直接創建 Webpack.config.js 去修改 Webpack 的設定,但因為 React 透過 Create React App 去建立 React 應用的一個腳手架工具,他將你不需要關心的設定都配置好,就像 Apple IOS,所以直接創建並配置 Webpack.config.js 不是一個聰明的決定。

React 其實預設有提供 eject 可以噴射出(抱歉筆者字典直接翻譯,覺得很好記就用這個詞了)他們原本的封裝在 Create React App 的配置,反編譯到目前的專案,缺點是 之後 Create React App 升級後你再也享受不到升級後的好處,因為他已經是以檔案的形式存在的你專案,故這邊不推薦這個『噴射設定方式』。

Google 翻譯我就爛

為了享有 Create React App 之後升級還能跟上升級後的福利,又能客製化設定,這邊就要依靠 react-app-rewired 這個套件。

customize-cra 這個套件提供了 override 這個 api,讓程式可以吃到你創建的 config-overrides.js 這支檔案,這隻檔案可以想像成 webpack.config.js

接著讓我們創建一支檔案放在專案根目錄,命名為 config-overrides.js,然後裡面打上:

1
2
3
4
5
6
7
const { override, addWebpackExternals } = require("customize-cra");

module.exports = override(
addWebpackExternals({
"./cptable": "var cptable",
})
);

正如上方提到爬文時看見了 Vue 的配置,大致的用意就是告訴編譯時,如果碰到 ./cptable 這個關鍵字,就替換成 var cptable

然後到 package.jsonreact-scripts start 那邊的程式碼改為:

1
2
3
4
5
6
7
8
9
//...略
"scripts": {
"start": "react-app-rewired start",
"build": "react-app-rewired build",
"test": "react-app-rewired test",
// 這個先不改,我們這邊也不會用到
"eject": "react-scripts eject"
},
//...略

最後再重跑一次 npm start,就會發現沒有噴錯了(筆者計時卡關半已天多…)。

這個步驟講了這麼多,簡單快速的在敘述一下整個過程。

  1. 安裝套件 react-app-rewired 用於最後替代 package.jsonreact-scripts

  2. 安裝套件 customize-cra 用於透過該套件 api 去吃到 config-overrides.js 的設定 及 取代編譯後套件的錯誤

  3. 創建 config-overrides.js 修改 webpack 的配置。

  4. 休息一下,我們繼續。

Step4:基本概念

在使用套件前我們必須先知道 xlsx 的基礎,這大段沒興趣可跳過,雖字多,但強烈建議閱讀。

基本概念

首先要知道 Excel 的一些名稱對應,在 xlsx 設定上我們會使用到 Cell Object(單元格)、Worksheet Object(工作表)、Workbook Object(工作簿)…這三個,分別是從最小到最大單位範圍的 **單元格、工作表、工作簿(通常指包含全部的工作表)**;有興趣可以詳細參考 **JavaScript导出excel文件,并修改文件样式**,這邊只會大略說明。

  • —– Cell Object(單元格) —–

通常指的是 Worksheet Object(工作表) 裡面的其中一個格子,裡面的屬性可參考 **xlsx Github - Cell Object**,這邊基本上會用到的就是下列三個。

  • v 單元格的值
  • t 單元格的類型,”b”布爾值、”n”數字、”e”錯誤、”s”字符串、”d”日期
  • s 單元格的樣式(這個是重點)

另外重點需要知道 Cell Object(單元格) 的格式為 {c: C, r: R} 為列號、R 為行號,很簡單吧,以下圖為例 王五 就是在 {c: 1, r: 5},記得格子開頭都是 0 開始算

格式介紹

xlsx 裡面有提供 utils 可以去對單元格進行操作,例如:LSX.utils.encode_row()、XLSX.utils.encode_cell()…等等

  • —– Worksheet Object(工作表) —–

sheet

這個工作表可以透過 xlsxworksheet 去對單元格做 合併、凍結、樣式…等等的設定,一樣可以詳細參考 **xlsx Github - Worksheet Object**。

  • Worksheet[‘!ref’]:**!ref** 代表這個工作表裡面的 Cell Object(單元格) 範圍。
  • Worksheet[‘!merges’]:可以合併儲存格,裡面需要帶入物件 {s: { c: C }, e: { r: R }} 開始到結束的格子,裡面 CR 地方為你的格子格式,算法可以看上面的 **Cell Object(單元格)**。
  • Worksheet[‘!freeze’]:可以凍結單元格,變成類似懸浮的表頭。

Worksheet 並不是 API 而是你需要自己去抓到 sheet,下面會有範例,這邊先上圖。

Worksheet

  • —– Workbook Object(工作簿) —–

這個應該就很明白了,多個 Worksheet Object(工作表) 就是 **Workbook Object(工作簿)**,一樣可參考 **xlsx Github - Workbook Object**。

這邊只需要我們可以命名 Worksheet Object(工作表) 的名稱,然後把對應的 Worksheet Object(工作表) 丟進去,如下方範例:

1
2
3
4
5
6
const Workbook = {
SheetNames: ["sheet_1"],
Sheets: {
sheet_1: Worksheet,
},
};

Step5:快樂輸出

快樂輸出

終於可以快樂的使用套件,看了前面字這麼多筆者知道你一定不耐煩了,這邊就會快速帶過了,簡單地列出幾點概要,並且會丟一個範例上 Github,有興趣可以直接 Clone 下來玩玩。

使用概要

在前置步驟都做完之後,只需要把 JSON 透過 xlsxjson_to_sheet() 轉為 sheet 拿到 Worksheet Object,接著對想要操作的 Cell Object 裡面的 “s” 放進客製化樣式,相關可客製化內容可以參考 **xlsx Github - Cell Styles**,或是可參考其他作者的翻譯 JavaScript匯出excel檔案,並修改檔案樣式

接著透過 xlsx-stylewrite() 去把檔案編寫成 Excel 讀得懂的格式,詳細內容如下圖。

xlsx-style-write

***請一定要使用 xlsx-style 的 *write(),不然輸出後的檔案樣式不會變更,有興趣可以試看看。

最後在透過官方提供的 function 把上面的內容轉為 blobfunction 如下,詳細也可參考 **官方文件**:

1
2
3
4
5
6
const s2ab = (s) => {
var buf = new ArrayBuffer(s.length);
var view = new Uint8Array(buf);
for (var i = 0; i !== s.length; ++i) view[i] = s.charCodeAt(i) & 0xff;
return buf;
};

最後的最後透過套件 file-saver,省去自己寫 function 透過 api saveAs 去下載這個 blob,大功告成!


最終成果

因為時間關係,目前只有實作 JSON 轉為 xlsx,之後會再將這個範例補上如何將 xlsx 轉為 JSON

Source Code:點我


Conclusion & 結論

其實最一開始只是想紀錄 xlsx-style 的套件問題(cptable error),後來又碰到 Reactoverrides,不小心又獲得了一些額外技能點。

這次拜這個需求所賜,又對 React 了解了一點,雖然中途處處碰壁,一直很挫折,但最後還是成就感十足;關於前端孤獨之路之後有空再分享,如果各位有什麼建議,或是筆者在內容上有錯誤,還請不吝嗇直接提出。


參考網站