您的当前位置:首页正文

xlsx库实现纯前端导入导出Excel

2024-09-15 来源:爱go旅游网

有网友碰到这样的问题“xlsx库实现纯前端导入导出Excel”。小编为您整理了以下解决方案,希望对您有帮助:

解决方案1:

前言

最近做了前端导入、导出Excel的需求,用到了js-xlsx这个库,该库文档提供的用例很少,并不是很友好。本文总结一下我是如何实现需求的。

需求

提供一个Excel文件,将里面的内容转成JSON导入数据

提供一个JSON文件,生成Excel文件并导出

导入与导出既可以前端做,也可以后端做。本文主要探讨前端通过SheetJS/js-xlsx这个库实现Excel导入、导出功能。

技术选型

市面上的报表类产品大抵可以分为以下两种:

云文档类型产品

控件类型产品

像SheetJS/js-xlsx、LuckySheet、Handsontable、SpreadJS都是标准的纯前端表格控件且都支持Excel的功能特性和JSON数据绑定。

最后选择SheetJS/js-xlsx这个库主要因为以下两个原因:

社区版开源免费。也可选择性能增强的专业版,专业版提供样式和专业支持的附加功能。

有30kstar,维护频率高,笔者在写这篇文章时(5月10日)该项目的上一次提交在5月9日。

基础知识

新建一个Excel文档,这个文档就是workbook,而一个workbook下可以有多个sheet。

SheetJS/js-xlsx安装$?yarn?add?xlsx@0.16.9

建议跟上版本号,我第一次装的时候没跟上版本号没有安装成功。

常用的数据表格式(CommonSpreadsheetFormat)

js-xlsx符合常用的数据表格式(CSF)。

一般结构

单元格地址对象的存储格式为{c:C,r:R},其中C和R分别代表的是0索引列和行号。例如单元格地址B5用对象{c:1,r:4}表示。

单元格范围对象存储格式为{s:S,e:E},其中S是第一个单元格,E是最后一个单元格。范围是包含关系。例如范围A3:B7用对象{s:{c:0,r:2},e:{c:1,r:6}}表示。

单元格对象

单元格对象是纯粹的JS对象,它的keys和values遵循下列的约定:

KeyDescriptionv原始值(查看数据类型部分获取更多的信息)w格式化文本(如果可以使用)t内行:bBoolean,eError,nNumber,dDate,sText,zStubf单元格公式编码为A1样式的字符串(如果可以使用)F如果公式是数组公式,则包围数组的范围(如果可以使用)r富文本编码(如果可以使用)h富文本渲染成HTML(如果可以使用)c与单元格关联的注释z与单元格关联的数字格式字符串(如果有必要)l单元格的超链接对象(.Target长联接,.Tooltip是提示消息)s单元格的样式/主题(如果可以使用)

如果w文本可以使用,内置的导出工具(比如CSV导出方法)就会使用它。要想改变单元格的值,在打算导出之前确保删除cell.w(或者设置cell.w为undefined)。工具函数会根据数字格式(cell.z)和原始值(如果可用)重新生成w文本。

真实的数组公式存储在数组范围中第一个单元个的f字段内。此范围内的其他单元格会省略f字段。

更多详细信息请查看文档

前端导入Excel数据/**?*?将?file?转为一个?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('入参不是?File?类型'));????}??});};

这里先用FileReader将file转换成ArrayBuffer,再用xlsx.read()转换成workbook。由于FileReader是异步读取,所以用promise处理了一下。最终可以看到Excel处理后生成了这样的一个Json,图片如下:

![ExcelDemo]()![ExcelToJson]()

注意workbook可能会有多个sheet,我们在Demo中加入一个Sheet2,如下图所示:

打断点看到,在方法中转换后的workbook如下所示:

所以需要对analyseExcelToJson这个方法做一些修改,修改后如下:

/**?*?将?file?转为一个?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('入参不是?File?类型'));????}??});};

读取数据按钮方法如下:

????async?analyseUpload()?{??????if?(!this.fileList.length)?return;??????console.log('读取数据');??????this.$refs.container.innerHTML?=?'';??????const?promises?=?this.fileList.map(({?file?})?=>?analyseExcelToJson(file));??????const?result?=?await?Promise.all(promises);??????this.result?=?result;??????console.log(result);??????result.forEach((workbook)?=>?{????????workbook.forEach((sheet)?=>?{??????????this.$refs.container.innerHTML?+=?generateExcelBySheet(sheet);????????});??????});????}

最终拿到的是多个sheet的集合,如下图所示:

由于我用了Promise.all用来处理读取多个Excel,所以看到外面又用数组包了一层。至此,简单的前端导入Excel数据已经全部实现了。

顺带一提,如果想要在页面中展示sheet,可以使用XLSX.utils.sheet_to_html。

前端导出Excel文件

导出一般分为两种:

数据导出Excel

页面表格导出Excel

数据导出Excel

前端在写前端导入Excel数据方法,最后返回的其实是workbook中sheet的集合。那么导出Excel文件便是将sheet拼成一个workbook导出即可。另外,导出的难点在于写成Excel之后要立马下载,而XLSX.writeFile直接帮我们实现这一步了。

/**?*?*?@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'?});};

假设数据并非CSF而是如下的二维数组:

const?ddArray?=?[??['S',?'h',?'e',?'e',?'t',?'J',?'S'],??[1,?2,?3,?4,?5],];

可以使用方法如下:

/**?*?*?@param?{Array}?workSheetData?二维数组?*?@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'?});};页面表格导出Excel

将页面中的表格导出Excel,应该是更加常见的情况。我们增加一个Element-ui的基础表格如下:

导出方法如下:

/**?*?将?table?转换成?Excel?导出?*?@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'?});};

页面中使用的话,通过ref拿到组件实例,将$el即Vue实例使用的根DOM元素作为入参即可。

exportExcelByTable(this.$refs.table.$el);踩坑

只用简单表格作为示例的话,似乎一切都很完美。然而,我在使用Element-uitable做复杂表格时,踩了一些坑。

当且不仅当表的内容为input、select这类组件而非普通的数据时,导出的Excel内容为空

将表头合并后,导出Excel仍能看到被合并的表头那一列。

使用fixed属性固定列时,导出的Excel数据会重复。

由于XLSX.utils.table_to_book这个方法实际上是将dom元素转化为workbook,这些坑都可以归类为获取到的dom元素不对。

表头合并

为了更好理解,我先讲表头合并的问题。由于Element-uitable并没有提供表头合并的方法,我实际是通过修改rowspan和colspan来实现跨行跨列,再使用display:none;这个css属性将原先位置的元素隐藏。如下图所示:

图中“ID”的colspan为2,“姓名”被我设置了display:none;。如果直接用我们之前表格导出Excel的方法,会发现虽然导出"ID"正确地变为了两列,但是“姓名”列并没有隐藏。由此可以得出结论:display:none;并不会影响Excel的获取。

所以我在项目中对于被隐藏的表头会添加cell-hide这个css类来隐藏被合并的表头。

.cell-hide?{??display:?none;}

然后在下载报表前,将合并的表头dom删除。

/**?*?将?file?转为一个?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('入参不是?File?类型'));????}??});};0内容为组件

同样利用display:none;并不会影响Excel的获取的特性可以解决问题1,只需在table-column中通过插槽增加被隐藏的dom,就可以正常拿到值了。代码如下:

/**?*?将?file?转为一个?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('入参不是?File?类型'));????}??});};1使用fixed属性固定表格列

先来看下,如果完全不处理,直接使用导出会是什么结果。以下面的table2为例,“日期”列被固定,导出的excel内容重复。

/**?*?将?file?转为一个?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('入参不是?File?类型'));????}??});};2

原因还是出在dom上,打印出table和table2的dom比较发现,table2多了css类为el-table__fixed的这个节点。

我的处理方法是先克隆节点,确保后续操作不会影响页面中的table2。通过遍历克隆出的新节点,找到.el-table__fixed这个节点并删除,最后返回新节点,发现可以输出正常的Excel文件。具体代码如下:

/**?*?将?file?转为一个?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('入参不是?File?类型'));????}??});};3总结

js-xlsx这个库功能很强大且使用简单,足以应付一般的导出导出需求,如果有美化导出Excel样式的需求需要选择pro版本。开发的难度主要在于阅读提供用例不足且冗长的文档。使用时注意维护好Workbook和Sheet对象即可,LuckySheet、SpreadJS也是类似的思路。

Demo地址

以上代码全部放在GitHub:ivestszheng/xlsx-study中。

参考

掘金:数据可视化探索之SpreadJS

掘金:十分钟上手xlsx,4种方法实现Excel导入导出

GitHub:Sheetjs/sheetjs

原文:https://juejin.cn/post/7097426696365670430

显示全文