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

news2025/2/24 3:34:19

前言

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

需求

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

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

技术选型

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

  1. 云文档类型产品
  2. 控件类型产品

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

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

  1. 社区版开源免费。也可选择性能增强的专业版,专业版提供样式和专业支持的附加功能。
  2. 有 30k star,维护频率高,笔者在写这篇文章时(5 月 10 日)该项目的上一次提交在 5 月 9 日。

基础知识

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

SheetJS/js-xlsx

安装

$ yarn add xlsx@0.16.9
复制代码

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

常用的数据表格式(Common Spreadsheet Format)

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

一般结构

单元格地址对象的存储格式为{c:C, r:R},其中CR分别代表的是 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 遵循下列的约定:

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

如果w文本可以使用,内置的导出工具(比如 CSV 导出方法)就会使用它。要想改变单元格的值,在打算导出之前确保删除cell.w(或者设置 cell.wundefined)。工具函数会根据数字格式(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 类型'));
    }
  });
};
复制代码

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

所以需要对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 类型'));
    }
  });
};
复制代码

读取数据按钮方法如下:

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

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

前端导出 Excel 文件

导出一般分为两种:

  1. 数据导出 Excel
  2. 页面表格导出 Excel

数据导出 Excel

前端在写前端导入 Excel 数据方法,最后返回的其实是workbooksheet的集合。那么导出 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拿到组件实例,将$elVue 实例的根 DOM 元素作为入参即可。

exportExcelByTable(this.$refs.table.$el);
复制代码

踩坑

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

  1. 当且不仅当表的内容为inputselect这类组件而非普通的数据时,导出的 Excel 内容为空
  2. 将表头合并后,导出 Excel 仍能看到被合并的表头那一列。
  3. 使用fixed属性固定列时,导出的 Excel 数据会重复。

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

表头合并

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

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

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

.cell-hide {
  display: none;
}
复制代码

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

document.querySelectorAll('.cell-hide').forEach((item) => {
  item.parentNode.removeChild(item);
});

// 下面就可以正常下载了
复制代码

内容为组件

同样利用**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>
复制代码

使用fixed属性固定表格列

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

exportExcelByTable(this.$refs.table2.$el);
复制代码

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

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

 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;
 }
复制代码

总结

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/25491.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

3D-2D:PnP

直接线性变换 Perspective-n-Point 是求解3D到2D点对运动的方法。它描述了已知n个3D空间点及其投影位置时&#xff0c;如何估计相机的位姿。 2D-2D的对极几何方法需要8个或8个以上的点对(以八点法为例)&#xff0c;且存在初始化、纯旋转和尺度的问题。然而&#xff0c;如果两张…

【数据挖掘】数据预处理

Outline ChapterOverview1.为什么要对数据预处理2.数据描述性总结3.数据清洗4.数据变换5.数据整合6.数据归约7.离散化与概念层级8.总结Chapter 1. 为什么要预处理 我们从现实生活中获得的原始数据&#xff0c;或多或少会因为各种原因不能直接使用。例如&#xff1a; 不完整 …

K8S Pod Sidecar 应用场景之一-加入 NGINX Sidecar 做反代和 web 服务器

Kubernetes Pod Sidecar 简介 Sidecar 是一个独立的容器&#xff0c;与 Kubernetes pod 中的应用容器一起运行&#xff0c;是一种辅助性的应用。 Sidecar 的常见辅助性功能有这么几种&#xff1a; 服务网格 (service mesh) 代理监控 Exporter&#xff08;如 redis exporter)C…

【附源码】计算机毕业设计JAVA学校食堂订餐管理

【附源码】计算机毕业设计JAVA学校食堂订餐管理 目运行 环境项配置&#xff1a; Jdk1.8 Tomcat8.5 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; JAVA my…

Java#20(包和final)

目录 一.包 1.包的作用? 2.包名书写的规则? 3.什么是全类名? 4.什么时候需要导包? 什么时候不需要导包? 二.final 三.常量 实际开发中&#xff0c;常量一般作为系统的配置信息&#xff0c;方便维护&#xff0c;提高可读性 四.权限修饰符 五.静态代码块 一.包 1…

简述使用vue使用webpack脚手架创建项目并升级至webpack4

第一步、输入命令 vue init webpack demo2 &#xff08;demo2表示项目名称&#xff09;&#xff0c;回车后等待项目完成创建&#xff1b; 第二步、打开package.json&#xff0c;升级以下依赖&#xff08;建议升级一项依赖后运行一下项目&#xff09; webpack系列webpack 3.6.…

Vue实现下载及文件重命名

效果如下&#xff1a; 实现步骤&#xff1a; html&#xff1a; <el-table-column prop"name" label"操作" align"center" header-align"center" width"165"><template slot-scope"scope"><el-but…

MindSpore和Python中nn.Unfold的区别

在往MindSpore迁移项目中遇到了这个转换&#xff0c;以至于不得不去仔细研究一下。 Unfold是卷积操作中的一部分&#xff0c;我们来看一下描述。 Unfold()函数是从一个batch图片中&#xff0c;提取出滑动的局部区域块&#xff0c;也就是卷积操作中的提取kernel filter对应的滑…

图片如何加水印?教你几招轻松加

相信很多喜欢出门游玩的小伙伴&#xff0c;会习惯将旅途中遇到的风景、趣事将其拍照记录下来&#xff0c;然后将图片分享到社交账号上去&#xff0c;但是互联网上什么人都有&#xff0c;总会有不怀好意的人&#xff0c;会在网上拿别人辛苦拍摄的照片&#xff0c;据为己有去发布…

开放式运动耳机哪款好,盘点几款目前最好的开放式耳机分享

如果是在户外运动的时候喜欢戴耳机&#xff0c;那么必然是要选择骨传导耳机&#xff0c;开放式耳道的设计在跑步的过程中&#xff0c;可以很快察觉到来往行人车辆&#xff0c;在安全方面能够极大缩小隐患的发声&#xff0c;无需入耳的佩戴设计能够以最大程度的释放我们的耳道长…

[翻译] 使用FXGL创建一个简单游戏 Pong (FXGL 11)

在本文中&#xff0c;我们将复刻经典的Pong游戏。要完成本教程&#xff0c;你首先需要获取FXGL要么通过Maven / Gradle&#xff0c;要么作为uber-jar。确保你使用FXGL 11 (例如11.3)。 本教程大部分是独立的&#xff0c;但是完成以前的基本教程将对一般理解非常有帮助。完整的…

刷题日记【第十三篇】-笔试必刷题【数根+星际密码+跳台阶扩展问题+快到碗里来】

刷题日记【第十三篇】-笔试必刷题【数根星际密码跳台阶扩展问题快到碗里来】 1.方法区在JVM中也是一个非常重要的区域&#xff0c;它与堆一样&#xff0c;是被【线程共享】的区域。 下面有关JVM内存&#xff0c;说法错误的是&#xff1f;(c) A.程序计数器是一个比较小的内存区…

GO语言之Goroutine和channel

1&#xff0c;goroutine-看一个需求 需求&#xff1a;要求统计1-90000000000的数字中&#xff0c;哪些是素数哦&#xff1f; 分析思路&#xff1a; 1&#xff09;传统的方法&#xff0c;就是使用一个循环&#xff0c;循环的判断各个数是不是素数。 2&#xff09;使用并发或…

Metabase学习教程:提问-5

多级聚合 如何使用查询生成器对多个部分提出问题。 许多分析问题只需四个步骤即可回答&#xff1a; 连接需要的表得到需要的信息。过滤数据使其仅包含期望的记录。分组和聚合这些数据&#xff0c;创造你所需要的价值。可视化结果&#xff0c;方便直观的理解数据告诉了你什么…

SpringBoot 集成JWT实现登录认证

如果文章对你有帮助欢迎【关注❤️❤️❤️点赞&#x1f44d;&#x1f44d;&#x1f44d;收藏⭐⭐⭐】一键三连&#xff01;一起努力&#xff01; 一、JWT简介 JSON Web Token&#xff08;JWT&#xff09;是一个非常轻巧的规范。这个规范允许我们使用JWT在用户和服务器之间传递…

基于粒子群优化算法的最佳方式设置无线传感器节点的位置,以减轻由于任何能量耗尽节点而产生的覆盖空洞(Matlab代码实现)

&#x1f468;‍&#x1f393;个人主页&#xff1a;研学社的博客 &#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜…

数据脱敏的安全管理

什么是数据脱敏 我们需要先说下什么是敏感数据&#xff0c; 敏感数据泛指个人信息&#xff08;姓名、电话、住址、健康信息、证件等数据&#xff09;、涉及需要保护的数据。 数据脱敏&#xff0c;是将敏感数据按照一定的规则对敏感数据进行变形&#xff0c;达到保护用户信息安…

提升80%上云集成效率, TA是如何做到的

摘要&#xff1a;基于华为云开天aPaaS&#xff0c;提升80%上云集成效率&#xff0c;降低50%集成成本没有充足资金&#xff0c;没有足够的项目规划和过渡时间&#xff0c;也没有经验丰富的IT团队支持&#xff0c;中小企业的上云路可谓是困难重重。如何帮助企业高效上云、实现降本…

【Globalmapper中文入门到精通系列实验图文教程】(附配套实验数据持续更新)

【Globalmapper中文版入门到精通系列实验图文教程】&#xff08;附配套实验数据持续更新&#xff09; 文章目录一、专栏简介二、文章目录三、数据目录四、传送门一、专栏简介 本专栏为GlobalMapper中文入门实战精品教程&#xff0c;内容主要涉及&#xff1a;Globalmapper23软件…

【Oracle】数据库账号频繁被锁问题解决

文中使用的Oracle版本为11g。 今天在测试环境中遇到了一个问题&#xff0c;如下图&#xff1a; 所有的数据库客户端访问Oracle11g都出现了上面的提示“ORA-28000: the account is locked”&#xff0c;一开始其实并不知道是什么原因引起的问题&#xff0c;到后面才发现是登录错…