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

javascript/jquery

浏览数:876

2020-5-26

在做之前一个项目的时候,需要有生成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对象列表)

worksheet对象则包含了一个sheet表格的详细内容。

对象中包含的内容包括:

*    !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.占位与合并单元格:

我们要输出的表格,并不都是单一罗列式的,往往会有复杂的格式,这就需要我们通过占位与合并单元格来控制表格的格式。
比如,我们希望输出的表格是这个样子的:

表格中的表头,存在二级分类,比如GPS位置下还包含了纬度、经度两个字段。
这个时候,构建数组表头部分时,我们需要在一些地方,比如表头第二行的开头,使用null来占位,保证表头上下两行列的一一对应。

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

这个时候,我们得到的结果是这个样子的:

接下来,我们要做的就是合并单元格,比如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数组,保证单元格合并的正确。

作者:wei4118268