前端使用SheetJS的xlsx.js实现excel表格生成

孙皎
• 阅读 29319

在做之前一个项目的时候,需要有生成excel表格的功能,在网上查询一番后,发现很多人都推荐Sheetjs的xlsx.js这款工具,自己使用了一下,感觉也不错,上手简单,功能结构清晰。因此,在这里就我自己使用到的功能部分和使用时候出现问题简单总结一下。

1.为什么由前端生成excel

过去的时候,excel表格的生成通常是由后端完成的,主要原因是之前个人PC的性能较低,完成数据量过大的excel表格生成工作比较困难,耗时太长,会造成长时间浏览器的卡顿,对用户体验造成很差的影响。
而现在,个人PC的性能已经有很大的提升,数据量较大的excel表格生成不会对用户体验造成较大的影响(多少还是会有影响的,但是已经没有那么夸张了,而且生成大数据量excel的功能一般是在网站的后台,用户往往能够接受一定时间的等待)。让用户PC机去处理excel的生成,能够解放服务器的很大压力,所以现在很多网站选择把生成excel的工作交付给前端去完成。

2.SheetJS的xlsx.js简介

纯js即可读取/生成excel,功能强大,支持多种格式,兼容性高。
xlsx.js有core和full两个版本,使用xlsx.core.min.js版本基本上就能满足大部分需求,我在项目中选择了core的版本。
其他详细介绍可以去看官方github:https://github.com/SheetJS/sheetjs

3.XLSX对象

引入js文件后,会在window对象上挂载一个全局对象XLSX,所有的操作函数都通过这个全局对象调用。

4.workbook对象与worksheet对象

workbook对象是描述一个excel文件的基本对象。
workbook对象中,主要包含了以下内容:

*      SheetNames数组:excel文件包含的所有sheets名称列表
*      Sheets数组:excel文件包含的所有sheets的内容(worksheet对象列表)

前端使用SheetJS的xlsx.js实现excel表格生成

worksheet对象则包含了一个sheet表格的详细内容。
前端使用SheetJS的xlsx.js实现excel表格生成
对象中包含的内容包括:

*    !ref:单元格范围
*    !rows:表格行属性
*    !cols:单元格属性
*    !merges:单元格合并
*    A1/B1/C1...:对应excel中的每一个具体的单元格

想要生成一个excel文件,需要首先构建出一个workbook对象,然后再对这个对象进行其他的操作。
workbook对象与worksheet对象看起来比较复杂,很多人看到后会比较焦虑:这些难道要我自己去组织吗?其实,其中包含的很多项都是可以通过内置方法自动生成的,所以不要太担心。

5.构建一个worksheet对象

在我的项目中,需求是生成一个带有格式要求的excel表格,这里咱们先来说说表格内容部分的生成。
xlsx.js提供了多种函数,将不同结构的数据转换成为worksheet对象,包括:

*    XLSX.utils.aoa_to_sheet:数组转换为worksheet
*    XLSX.utils.json_to_sheet:json对象转换为worksheet
*    XLSX.utils.table_to_sheet:表格转换为worksheet

我的项目中,根据项目情况选择了由数组转换为worksheet的方式,这里我就详细说一下如何构建符合要求的数据数组并将其转换为worksheet对象,其他的方式大家可以移步官方github去查看。

1.内容部分:

基本的数组结构为:

var arr = [
  [单元格A1内容,单元格A2内容,单元格A3内容,...],
  [单元格B1内容,单元格B2内容,单元格B3内容,...],
  [单元格B1内容,单元格B2内容,单元格B3内容,...],
  ...
]

所以,理论上,我们只要不断的向数组里push内容就可以了,数组内容填充完成之后,直接调用var ws = aoa_to_sheet(arr),就能够输出一个worksheet对象到变量ws中了。ws对象中的!ref属性是自动生成的。

2.占位与合并单元格:

我们要输出的表格,并不都是单一罗列式的,往往会有复杂的格式,这就需要我们通过占位与合并单元格来控制表格的格式。
比如,我们希望输出的表格是这个样子的:
前端使用SheetJS的xlsx.js实现excel表格生成
表格中的表头,存在二级分类,比如GPS位置下还包含了纬度、经度两个字段。
这个时候,构建数组表头部分时,我们需要在一些地方,比如表头第二行的开头,使用null来占位,保证表头上下两行列的一一对应。

var arr = [
    ['户主姓名', '性别', '文化程度', '手机号码', 'GPS位置', 'null', '户籍所在地', 'null', 'null', 'null'],
    [null, null, null, null, '纬度', '经度', '盟', '旗', '苏木', '嘎查']
];

这个时候,我们得到的结果是这个样子的:
前端使用SheetJS的xlsx.js实现excel表格生成

接下来,我们要做的就是合并单元格,比如A1和B1、A2和B2、A5和A6等。
我们需要向生成的ws对象中手动的加入属性!merges及其对应的数组。

ws['!merges'] = [
    {s: {r: 0, c: 0}, e: {r: 1, c: 0}},
    {s: {r: 0, c: 1}, e: {r: 1, c: 1}},
    {s: {r: 0, c: 2}, e: {r: 1, c: 2}},
    {s: {r: 0, c: 3}, e: {r: 1, c: 3}},
    {s: {r: 0, c: 4}, e: {r: 0, c: 5}},
    {s: {r: 0, c: 6}, e: {r: 0, c: 9}}
]

数组的每一项,代表其中一个单元格的合并要求。每一个对象中:s代表合并的起始位置,e代表合并的结束位置。
s中,r代表行数,c代表列数。
比如数组的第一个对象,表达的含义为:以0行0列(对应单元格A1)作为起始,以1行0列(对应单元格A2)作为结束,合并这些单元格。

3.行高与列宽

当某一个单元格字符数过长的时候,显示的内容会超过单元格边界,非常影响用户的阅读,这个时候,需要通过调整行高或者列宽来保证表格便于阅读。
我们需要向生成的ws对象中手动加入属性!cols来控制列宽,!rows来控制行高。
例如:

ws['!cols'] = [
    {wpx: 90},
    {wpx: 50},
    {wpx: 60},
    {wpx: 100},
    {wpx: 100},
    {wpx: 100},
    {wpx: 70},
    {wpx: 100},
    {wpx: 100},
    {wpx: 100}
];

数组的每一项对应需要控制的列,比如下标0的项代表第一列(A列)的宽度。
wpx表示屏幕像素值,wch代表英文字符数,还有一个width可选,文档描述为 width in Excel's "Max Digit Width",具体含义还没弄明白,求大佬来解释。
行高的控制类似,具体内容请自己查阅文档。

6.构建workbook对象并导出excel文件

对于workbook对象,我们只需要填充SheetNames属性和Sheets属性即可。

var workbook = {
    SheetNames: ['本地户主信息'],
    Sheets: {
        '本地户主信息': ws
    }
};

然后,通过XLSX.write函数生成excel文件的源码(具体应该叫什么我也不清楚,求大佬告知),把文件源码字符串转成arrayBuffer,再通过new Blob方法转换为二进制,最后使用URL.createObjectURL函数将blob对象创建为blob地址,赋给a标签的href属性,就能够进行下载了。
这个部分的具体代码,是我从网上直接找的(对于arrayBuffer和blob这块比较生疏),这里贴出来,感谢最初编写这段代码的大佬。

    //导出excel相关函数
 sheet2blob: function(sheet, sheetName) {
     sheetName = sheetName || 'sheet1';
     var workbook = {
         SheetNames: [sheetName],
         Sheets: {}
     };
     workbook.Sheets[sheetName] = sheet;
     // 生成excel的配置项
     var wopts = {
         bookType: 'xlsx', // 要生成的文件类型
         bookSST: false, // 是否生成Shared String Table,官方解释是,如果开启生成速度会下降,但在低版本IOS设备上有更好的兼容性
         type: 'binary'
     };
     var wbout = XLSX.write(workbook, wopts);
     var blob = new Blob([s2ab(wbout)], {type:"application/octet-stream"});
     // 字符串转ArrayBuffer
     function 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;
     }
     return blob;
 },

 openDownloadXLSXDialog: function(url, saveName){
     if(typeof url == 'object' && url instanceof Blob){
         url = URL.createObjectURL(url); // 创建blob地址
     }
     var aLink = document.createElement('a');
     aLink.href = url;
     aLink.download = saveName || ''; // HTML5新增的属性,指定保存文件名,可以不要后缀,注意,file:///模式下不会生效
     var event;
     if(window.MouseEvent) event = new MouseEvent('click');
     else{
         event = document.createEvent('MouseEvents');
         event.initMouseEvent('click', true, false, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null);
     }
     aLink.dispatchEvent(event);
 }

 var blob = sheet2blob(ws, '户主信息');
 openDownloadXLSXDialog(blob, '123.xlsx');

7.遇到的问题

一个很蛋疼的需求,需要我在一个sheet中重复输出带复杂表头的表格。
在上边构建一个worksheet对象中描述的表格,我只需要输出一个表头即可,我能够提前知道合并哪几个单元格,因此我先将数据部分直接循环插入数组就行,在数组内容分填充完毕之后,生成ws对象,再给ws对象增加的!merges属性。
而新需求无法这么做了,因为除了向数组中循环输出数据之外,我还需要循环输出表头到数组中。没有办法,这个时候只能先创建!merges属性的数组,在表格内容数组填充的循环体中,自行计算输出到的行数,缓存在变量里,然后在这个循环体中使用行数变量动态填充!merges数组,保证单元格合并的正确。

点赞
收藏
评论区
推荐文章
blmius blmius
3年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
美凌格栋栋酱 美凌格栋栋酱
6个月前
Oracle 分组与拼接字符串同时使用
SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
Jacquelyn38 Jacquelyn38
4年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
Easter79 Easter79
3年前
Twitter的分布式自增ID算法snowflake (Java版)
概述分布式系统中,有一些需要使用全局唯一ID的场景,这种时候为了防止ID冲突可以使用36位的UUID,但是UUID有一些缺点,首先他相对比较长,另外UUID一般是无序的。有些时候我们希望能使用一种简单一些的ID,并且希望ID能够按照时间有序生成。而twitter的snowflake解决了这种需求,最初Twitter把存储系统从MySQL迁移
Wesley13 Wesley13
3年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Wesley13 Wesley13
3年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。
Stella981 Stella981
3年前
Hibernate纯sql查询结果和该sql在数据库直接查询结果不一致
问题:今天在做一个查询的时候发现一个问题,我先在数据库实现了我需要的sql,然后我在代码中代码:selectdistinctd.id,d.name,COALESCE(c.count_num,0),COALESCE(c.count_fix,0),COALESCE(c
为什么mysql不推荐使用雪花ID作为主键
作者:毛辰飞背景在mysql中设计表的时候,mysql官方推荐不要使用uuid或者不连续不重复的雪花id(long形且唯一),而是推荐连续自增的主键id,官方的推荐是auto_increment,那么为什么不建议采用uuid,使用uuid究
Python进阶者 Python进阶者
1年前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这