前言
最近做了前端導(dǎo)入、導(dǎo)出 Excel 的需求,用到了js-xlsx
這個庫,該庫文檔提供的用例很少,并不是很友好。本文總結(jié)一下我是如何實現(xiàn)需求的。
需求
- 提供一個 Excel 文件,將里面的內(nèi)容轉(zhuǎn)成 JSON 導(dǎo)入數(shù)據(jù)
- 提供一個 JSON 文件,生成 Excel 文件并導(dǎo)出
導(dǎo)入與導(dǎo)出既可以前端做,也可以后端做。本文主要探討前端通過
SheetJS/js-xlsx
這個庫實現(xiàn) Excel 導(dǎo)入、導(dǎo)出功能。
技術(shù)選型
市面上的報表類產(chǎn)品大抵可以分為以下兩種:
- 云文檔類型產(chǎn)品
- 控件類型產(chǎn)品
像 SheetJS/js-xlsx、LuckySheet、Handsontable、SpreadJS 都是標準的純前端表格控件且都支持 Excel 的功能特性和 JSON 數(shù)據(jù)綁定。
最后選擇 SheetJS/js-xlsx 這個庫主要因為以下兩個原因:
- 社區(qū)版開源免費。也可選擇性能增強的專業(yè)版,專業(yè)版提供樣式和專業(yè)支持的附加功能。
- 有 30k star,維護頻率高,筆者在寫這篇文章時(5 月 10 日)該項目的上一次提交在 5 月 9 日。
基礎(chǔ)知識
新建一個 Excel 文檔,這個文檔就是workbook
,而一個workbook
?下可以有多個sheet
。
SheetJS/js-xlsx
安裝
$ yarn add xlsx@0.16.9
復(fù)制代碼
建議跟上版本號,我第一次裝的時候沒跟上版本號沒有安裝成功。
常用的數(shù)據(jù)表格式(Common Spreadsheet Format)
js-xlsx
符合常用的數(shù)據(jù)表格式(CSF)。
一般結(jié)構(gòu)
單元格地址對象的存儲格式為{c:C, r:R}
,其中C
和R
分別代表的是 0 索引列和行號。例如單元格地址B5
用對象{c:1, r:4}
表示。
單元格范圍對象存儲格式為{s:S, e:E}
,其中S
是第一個單元格,E
是最后一個單元格。范圍是包含關(guān)系。例如范圍?A3:B7
用對象{s:{c:0, r:2}, e:{c:1, r:6}}
表示。
單元格對象
單元格對象是純粹的 JS 對象,它的 keys 和 values 遵循下列的約定:
Key | Description |
---|---|
v |
原始值(查看數(shù)據(jù)類型部分獲取更多的信息) |
w |
格式化文本(如果可以使用) |
t |
內(nèi)行:?b ?Boolean,?e ?Error,?n ?Number,?d ?Date,?s ?Text,?z ?Stub |
f |
單元格公式編碼為 A1 樣式的字符串(如果可以使用) |
F |
如果公式是數(shù)組公式,則包圍數(shù)組的范圍(如果可以使用) |
r |
富文本編碼 (如果可以使用) |
h |
富文本渲染成 HTML (如果可以使用) |
c |
與單元格關(guān)聯(lián)的注釋 |
z |
與單元格關(guān)聯(lián)的數(shù)字格式字符串(如果有必要) |
l |
單元格的超鏈接對象 (.Target ?長聯(lián)接,?.Tooltip ?是提示消息) |
s |
單元格的樣式/主題 (如果可以使用) |
如果w
文本可以使用,內(nèi)置的導(dǎo)出工具(比如 CSV 導(dǎo)出方法)就會使用它。要想改變單元格的值,在打算導(dǎo)出之前確保刪除cell.w
(或者設(shè)置?cell.w
為undefined
)。工具函數(shù)會根據(jù)數(shù)字格式(cell.z
)和原始值(如果可用)重新生成w
文本。
真實的數(shù)組公式存儲在數(shù)組范圍中第一個單元個的f
字段內(nèi)。此范圍內(nèi)的其他單元格會省略f
字段。
更多詳細信息請查看文檔
前端導(dǎo)入 Excel 數(shù)據(jù)
/**
* 將 file 轉(zhuǎn)為一個 CSF 的 JSON
* @param {File} file
* @returns sheet
*/
const analyseExcelToJson = (file) => {
?return new Promise((resolve, reject) => {
? ?if (file instanceof File) {
? ? ?const reader = new FileReader();
?
? ? ?reader.onloadend = (progressEvent) => {
? ? ? ?const arrayBuffer = reader.result;
?
? ? ? ?const options = { type: 'array' };
? ? ? ?const workbook = XLSX.read(arrayBuffer, options);
?
? ? ? ?const sheetName = workbook.SheetNames;
? ? ? ?const sheet = workbook.Sheets[sheetName];
?
? ? ? ?resolve(sheet);
? ? };
? ? ?reader.readAsArrayBuffer(file);
? } else {
? ? ?reject(new Error('入?yún)⒉皇?File 類型'));
? }
});
};
復(fù)制代碼
這里先用FileReader
將file
轉(zhuǎn)換成ArrayBuffer
,再用xlsx.read()
轉(zhuǎn)換成workbook
。由于FileReader
是異步讀取,所以用promise
處理了一下。最終可以看到 Excel 處理后生成了這樣的一
所以需要對analyseExcelToJson
這個方法做一些修改,修改后如下:
/**
* 將 file 轉(zhuǎn)為一個 CSF 的 JSON
* @param {File} file
* @returns sheets
*/
const analyseExcelToJson = (file) => {
?return new Promise((resolve, reject) => {
? ?if (file instanceof File) {
? ? ?const reader = new FileReader();
?
? ? ?reader.onloadend = (progressEvent) => {
? ? ? ?const arrayBuffer = reader.result;
?
? ? ? ?const options = { type: 'array' };
? ? ? ?const workbook = XLSX.read(arrayBuffer, options);
?
? ? ? ?const sheetNames = workbook.SheetNames;
? ? ? ?const result = sheetNames.map((sheetName) => workbook.Sheets[sheetName]);
? ? ? ?resolve(result);
? ? };
? ? ?reader.readAsArrayBuffer(file);
? } else {
? ? ?reject(new Error('入?yún)⒉皇?File 類型'));
? }
});
};
復(fù)制代碼
讀取數(shù)據(jù)按鈕方法如下:
由于我用了Promise.all
用來處理讀取多個 Excel,所以看到外面又用數(shù)組包了一層。至此,簡單的前端導(dǎo)入 Excel 數(shù)據(jù)已經(jīng)全部實現(xiàn)了。
順帶一提,如果想要在頁面中展示
sheet
,可以使用XLSX.utils.sheet_to_html
。
前端導(dǎo)出 Excel 文件
導(dǎo)出一般分為兩種:
- 數(shù)據(jù)導(dǎo)出 Excel
- 頁面表格導(dǎo)出 Excel
數(shù)據(jù)導(dǎo)出 Excel
前端在寫前端導(dǎo)入 Excel 數(shù)據(jù)方法,最后返回的其實是workbook
中sheet
的集合。那么導(dǎo)出 Excel 文件便是將sheet
拼成一個workbook
導(dǎo)出即可。另外,導(dǎo)出的難點在于寫成 Excel 之后要立馬下載,而XLSX.writeFile
直接幫我們實現(xiàn)這一步了。
/**
*
* @param {Array} sheets sheet的集合
* @param {String} fileName 下載時文件名稱
*/
const exportExcelBySheets = (sheets, fileName = 'example.xlsx') => {
?const SheetNames = [];
?const Sheets = {};
?const workbook = { SheetNames, Sheets };
?
?sheets.forEach((sheet, i) => {
? ?const name = `sheet${i + 1}`;
? ?SheetNames.push(name);
? ?Sheets[name] = sheet;
});
?
?return XLSX.writeFile(workbook, fileName, { type: 'binary' });
};
復(fù)制代碼
假設(shè)數(shù)據(jù)并非CSF
而是如下的二維數(shù)組:
const ddArray = [
['S', 'h', 'e', 'e', 't', 'J', 'S'],
[1, 2, 3, 4, 5],
];
復(fù)制代碼
可以使用方法如下:
/**
*
* @param {Array} workSheetData 二維數(shù)組
* @param {String} fileName 下載時文件名稱
*/
const exportExcelByDoubleDimensArray = (workSheetData, fileName = 'example.xlsx') => {
?const ws = XLSX.utils.aoa_to_sheet(workSheetData);
?const workSheetName = 'MySheet';
?const workbook = XLSX.utils.book_new();
?
?XLSX.utils.book_append_sheet(workbook, ws, workSheetName);
?return XLSX.writeFile(workbook, fileName, { type: 'binary' });
};
復(fù)制代碼
頁面表格導(dǎo)出 Excel
將頁面中的表格導(dǎo)出 Excel,應(yīng)該是更加常見的情況。我們增加一個 Element-ui 的基礎(chǔ)表格如下:
導(dǎo)出方法如下:
/**
* 將 table 轉(zhuǎn)換成 Excel 導(dǎo)出
* @param {*} el table 的根 dom 元素
* @param {*} fileName 下載時文件名稱
*/
const exportExcelByTable = (el, fileName = 'example.xlsx') => {
?if (!el) {
? ?throw new Error('沒有獲取到表格的根 dom 元素');
}
?const options = { raw: true };
?const workbook = XLSX.utils.table_to_book(el, options);
?
?return XLSX.writeFile(workbook, fileName, { type: 'binary' });
};
復(fù)制代碼
頁面中使用的話,通過ref
拿到組件實例,將$el
即Vue 實例的根 DOM 元素
作為入?yún)⒓纯伞?/p>
exportExcelByTable(this.$refs.table.$el);
復(fù)制代碼
踩坑
只用簡單表格作為示例的話,似乎一切都很完美。然而,我在使用Element-ui table
做復(fù)雜表格時,踩了一些坑。
- 當(dāng)且不僅當(dāng)表的內(nèi)容為
input
、select
這類組件而非普通的數(shù)據(jù)時,導(dǎo)出的 Excel 內(nèi)容為空 - 將表頭合并后,導(dǎo)出 Excel 仍能看到被合并的表頭那一列。
- 使用
fixed
屬性固定列時,導(dǎo)出的 Excel 數(shù)據(jù)會重復(fù)。
由于XLSX.utils.table_to_book
這個方法實際上是將dom
元素轉(zhuǎn)化為workbook
,這些坑都可以歸類為獲取到的?dom
元素不對。
表頭合并
為了更好理解,我先講表頭合并的問題。由于Element-ui table
并沒有提供表頭合并的方法,我實際是通過修改rowspan
和colspan
來實現(xiàn)跨行跨列,再使用display: none;
這個css
屬性將原先位置的元素隱藏。如下圖所示:
圖中“ID”的colspan
為 2,“姓名”被我設(shè)置了display: none;
。如果直接用我們之前表格導(dǎo)出 Excel 的方法,會發(fā)現(xiàn)雖然導(dǎo)出"ID"正確地變?yōu)榱藘闪?,但是“姓名”列并沒有隱藏。由此可以得出結(jié)論:display: none;
并不會影響 Excel 的獲取。
所以我在項目中對于被隱藏的表頭會添加cell-hide
這個css
類來隱藏被合并的表頭。
.cell-hide {
display: none;
}
復(fù)制代碼
然后在下載報表前,將合并的表頭dom
刪除。
document.querySelectorAll('.cell-hide').forEach((item) => {
item.parentNode.removeChild(item);
});
// 下面就可以正常下載了
復(fù)制代碼
內(nèi)容為組件
同樣利用**display: none;
并不會影響 Excel 的獲取**的特性可以解決問題 1,只需在table-column
中通過插槽增加被隱藏的dom
,就可以正常拿到值了。代碼如下:
<el-table ref="table" :data="tableData" style="width: 600px; margin: 0 auto">
<el-table-column prop="date" label="日期" width="180"> </el-table-column>
<el-table-column prop="name" label="姓名" width="180"> </el-table-column>
<el-table-column prop="address" label="地址">
<template slot="header">
<span>地址</span>
</template>
<template slot-scope>
<el-input :value="123" />
<span style="display: none">123</span>
</template>
</el-table-column>
</el-table>
復(fù)制代碼
使用fixed
屬性固定表格列
先來看下,如果完全不處理,直接使用導(dǎo)出會是什么結(jié)果。以下面的 table2 為例,“日期”列被固定,導(dǎo)出的 excel 內(nèi)容重復(fù)。
exportExcelByTable(this.$refs.table2.$el);
復(fù)制代碼
原因還是出在dom
上,打印出 table 和 table2 的dom
比較發(fā)現(xiàn),table2 多了css
類為el-table__fixed
的這個節(jié)點。
我的處理方法是先克隆節(jié)點,確保后續(xù)操作不會影響頁面中的 table2。通過遍歷克隆出的新節(jié)點,找到.el-table__fixed
這個節(jié)點并刪除,最后返回新節(jié)點,發(fā)現(xiàn)可以輸出正常的 Excel 文件。具體代碼如下:文章來源:http://www.zghlxwxcb.cn/news/detail-401771.html
exportExcelByTable2() {
const newEl = this.removeFixedDom(this.$refs.table2.$el);
exportExcelByTable(newEl);
}
removeFixedDom(el) {
const newEl = el.cloneNode(true);
newEl.childNodes.forEach((node) => {
if (node.className === 'el-table__fixed') {
node.parentNode.removeChild(node);
}
});
return newEl;
}
復(fù)制代碼
總結(jié)
js-xlsx
這個庫功能很強大且使用簡單,足以應(yīng)付一般的導(dǎo)出導(dǎo)出需求,如果有美化導(dǎo)出 Excel 樣式的需求需要選擇 pro 版本。開發(fā)的難度主要在于閱讀提供用例不足且冗長的文檔。使用時注意維護好Workbook
和Sheet
對象即可,LuckySheet、SpreadJS 也是類似的思路。?文章來源地址http://www.zghlxwxcb.cn/news/detail-401771.html
到了這里,關(guān)于xlsx庫實現(xiàn)純前端導(dǎo)入導(dǎo)出Excel的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!