大厂技术  高级前端  Node进阶

点击上方 程序员成长指北,关注公众号

回复1,加入高级Node交流群

前端导出 excel 的需求很多,但市面上好用的库并不多,讲明白复杂使用场景的文章更少。

本文将以文字 + demo 源码的形式,力求讲清楚满足 99% 使用场景的终极 excel 导出方案。

如果项目中用到了 AntD,那就更简单了,因为 Table 本身已经设置好了 column 和 dataSource,只需解析 column 和 dataSource 即可快速导出 Excel。

实现功能:

  • 简单表格导出

  • 为表格添加样式(更改背景色、更换字体、字号、颜色)

  • 设置行高、列宽

  • 解析 ant-design 的 Table 直接导出excel,根据 antd 页面中设置的列宽动态计算 excel 中的列宽

  • 多级表头(行合并、列合并)

  • 一个 sheet 中放多张表,并实现每张表的列宽不同

源码地址:github.com/cachecats/excel-export-demo[1]

第二篇文章:js 批量导出 excel 为zip压缩包:https://juejin.cn/post/7080169896209809445/[2], 对导出方法进行了封装,还实现了使用 exceljsfile-saverjszip实现下载包含多层级文件夹、多个 excel、每个 excel 支持多个 sheet 的 zip 压缩包。

一、技术选型

xlsx

呼声最高的是 xlsx[3],又叫 SheetJS,也是下载量最高和 star最多的库。试用了一下很强大,但是!默认不支持改变样式,想要支持改变样式,需要使用它的收费版本。

本着勤俭节约的原则,很多人使用了另一个第三方库:xlsx-style[4],但是使用起来极其复杂,还需要改 node_modules 源码,这个库最后更新时间也定格在了 6年前。还有一些其他的第三方样式拓展库,质量参差不齐。

使用成本和后期的维护成本很高,不得不放弃。

ExcelJS

ExcelJS[5] 周下载量 450k,github star 9k,并且拥有中文文档,对国内开发者很友好。虽然文档是以README 的形式,可读性不太好,但重在内容,常用的功能基本都有覆盖。

最近更新时间是6个月内,试用了一下,集成很简单,再加之文档丰富,就选它了。

安装:

npm install exceljs
复制代码

下载到本地还需要另一个库:file-saver

npm install file-saver
复制代码

二、基本概念

先了解下基本概念,更详细的介绍参考官方文档:github.com/exceljs/exc…[6]

workbook

workbook:工作簿,可以理解为整个 excel 表格。

通过 const workbook = new ExcelJS.Workbook() 创建工作簿,还可以设置工作簿的属性:

workbook.creator = 'Me';
workbook.lastModifiedBy = 'Her';
workbook.created = new Date(1985, 8, 30);
workbook.modified = new Date();
workbook.lastPrinted = new Date(2016, 9, 27);
复制代码

worksheet

工作表,即 Excel 表格中的 sheet 页。

通过 const sheet = workbook.addWorksheet('My Sheet')创建工作表,每个 workbook 可添加多个 worksheet。

使用 addWorksheet 函数的第二个参数来指定工作表的选项。

// 创建带有红色标签颜色的工作表
const sheet = workbook.addWorksheet('My Sheet', {properties:{tabColor:{argb:'FFC0000'}}});// 创建一个隐藏了网格线的工作表
const sheet = workbook.addWorksheet('My Sheet', {views: [{showGridLines: false}]});// 创建一个第一行和列冻结的工作表
const sheet = workbook.addWorksheet('My Sheet', {views:[{xSplit: 1, ySplit:1}]});// 使用A4设置的页面设置设置创建新工作表 - 横向
const worksheet =  workbook.addWorksheet('My Sheet', {pageSetup:{paperSize: 9, orientation:'landscape'}
});// 创建一个具有页眉页脚的工作表
const sheet = workbook.addWorksheet('My Sheet', {headerFooter:{firstHeader: "Hello Exceljs", firstFooter: "Hello World"}
});// 创建一个冻结了第一行和第一列的工作表
const sheet = workbook.addWorksheet('My Sheet', {views:[{state: 'frozen', xSplit: 1, ySplit:1}]});
复制代码

columns

列,通过 worksheet.columns可设置表头。

// 添加列标题并定义列键和宽度
// 注意:这些列结构仅是构建工作簿的方便之处,除了列宽之外,它们不会完全保留。
worksheet.columns = [{ header: 'Id', key: 'id', width: 10 },{ header: 'Name', key: 'name', width: 32 },{ header: 'D.O.B.', key: 'DOB', width: 10, outlineLevel: 1 }
];// 通过键,字母和基于1的列号访问单个列
const idCol = worksheet.getColumn('id');
const nameCol = worksheet.getColumn('B');
const dobCol = worksheet.getColumn(3);// 设置列属性// 注意:将覆盖 C1 单元格值
dobCol.header = 'Date of Birth';// 注意:这将覆盖 C1:C2 单元格值
dobCol.header = ['Date of Birth', 'A.K.A. D.O.B.'];// 从现在开始,此列将以 “dob” 而不是 “DOB” 建立索引
dobCol.key = 'dob';dobCol.width = 15;// 如果需要,隐藏列
dobCol.hidden = true;
复制代码

还可对列进行各种操作。

// 遍历此列中的所有当前单元格
dobCol.eachCell(function(cell, rowNumber) {// ...
});// 遍历此列中的所有当前单元格,包括空单元格
dobCol.eachCell({ includeEmpty: true }, function(cell, rowNumber) {// ...
});// 添加一列新值
worksheet.getColumn(6).values = [1,2,3,4,5];// 添加稀疏列值
worksheet.getColumn(7).values = [,,2,3,,5,,7,,,,11];// 剪切一列或多列(右边的列向左移动)
// 如果定义了列属性,则会相应地对其进行切割或移动
// 已知问题:如果拼接导致任何合并的单元格移动,结果可能是不可预测的
worksheet.spliceColumns(3,2);// 删除一列,再插入两列。
// 注意:第4列及以上的列将右移1列。
// 另外:如果工作表中的行数多于列插入项中的值,则行将仍然被插入,就好像值存在一样。
const newCol3Values = [1,2,3,4,5];
const newCol4Values = ['one', 'two', 'three', 'four', 'five'];
worksheet.spliceColumns(3, 1, newCol3Values, newCol4Values);
复制代码

row

行,可以添加一行或者同时添加多行数据,是使用最频繁的属性。

// 通过 json 添加一行数据,需要先设置 columns
worksheet.addRow({id: 1, name: 'John Doe', dob: new Date(1970,1,1)});
worksheet.addRow({id: 2, name: 'Jane Doe', dob: new Date(1965,1,7)});
// 通过数组添加一行数据
worksheet.addRow([3, 'Sam', new Date()]);// 同时添加多行数据
worksheet.addRows(list);// 遍历工作表中具有值的所有行
worksheet.eachRow(function(row, rowNumber) {console.log('Row ' + rowNumber + ' = ' + JSON.stringify(row.values));
});// 遍历工作表中的所有行(包括空行)
worksheet.eachRow({ includeEmpty: true }, function(row, rowNumber) {console.log('Row ' + rowNumber + ' = ' + JSON.stringify(row.values));
});// 连续遍历所有非空单元格
row.eachCell(function(cell, colNumber) {console.log('Cell ' + colNumber + ' = ' + cell.value);
});// 遍历一行中的所有单元格(包括空单元格)
row.eachCell({ includeEmpty: true }, function(cell, colNumber) {console.log('Cell ' + colNumber + ' = ' + cell.value);
});
复制代码

三、简单表格导出

本文所有示例都使用 React + AntD。

先看效果,我们用 AntD 的 Table 写个简单的表格页面,并设置不同的列宽:

点击导出 excel,然后打开得到以下结果:

可以看到,导出的 excel 列宽比例跟在线的表格是一致的。

贴源码:

// 简单 demo
import React, {useEffect, useState} from 'react'
import {Button, Card, Table} from "antd";
import {ColumnsType} from "antd/lib/table/interface";
import * as ExcelJs from 'exceljs';
import {generateHeaders, saveWorkbook} from "../utils";interface SimpleDemoProps {
}interface StudentInfo {id: number;name: string;age: number;gender: string;
}const SimpleDemo: React.FC<SimpleDemoProps> = () => {const [list, setList] = useState<StudentInfo[]>([]);useEffect(() => {generateData();}, [])function generateData() {let arr: StudentInfo[] = [];for (let i = 0; i < 10; i++) {arr.push({id: i,name: `小明${i}号`,age: i,gender: i % 2 === 0 ? '男' : '女'})}setList(arr);}const columns: ColumnsType<any> = [{width: 50,dataIndex: 'id',key: 'id',title: 'ID',},{width: 100,dataIndex: 'name',key: 'name',title: '姓名',},{width: 50,dataIndex: 'age',key: 'age',title: '年龄',},{width: 80,dataIndex: 'gender',key: 'gender',title: '性别',},];function onExportBasicExcel() {// 创建工作簿const workbook = new ExcelJs.Workbook();// 添加sheetconst worksheet = workbook.addWorksheet('demo sheet');// 设置 sheet 的默认行高worksheet.properties.defaultRowHeight = 20;// 设置列worksheet.columns = generateHeaders(columns);// 添加行worksheet.addRows(list);// 导出excelsaveWorkbook(workbook, 'simple-demo.xlsx');}return (<Card><h3>简单表格</h3><Button type={'primary'} style={{marginBottom: 10}} onClick={onExportBasicExcel}>导出excel</Button><Tablecolumns={columns}dataSource={list}/></Card>);
}export default SimpleDemo
复制代码

真正导出的代码只有几行,重点看 onExportBasicExcel方法:

  1. 先创建工作簿和 sheet 页,这两行是固定代码。如果需要多 sheet,则创建多个 sheet 即可。后续对表格的所有操作,都是对 worksheet 的操作。

  2. 设置表格的默认行高。这步非必要,但是设置了更美观。否则会出现有内容的行跟没有内容的行行高不一致的情况。

  3. 设置列数据(表头)和每行的数据。

  4. 导出 excel。

解析 AntD Table 的 columns 和 dataSource

因为我们是用 AntD 的 Table,其实已经构造出了表头和具体的表格数据,所以只需解析即可。

generateHeaders()方法是自己封装的,将 Table 的 columns 转换为 ExcelJS的表头格式的方法:

import {ITableHeader} from "src/types";
import {ColumnsType} from "antd/lib/table/interface";const DEFAULT_COLUMN_WIDTH = 20;// 根据 antd 的 column 生成 exceljs 的 column
export function generateHeaders(columns: any[]) {return columns?.map(col => {const obj: ITableHeader = {// 显示的 nameheader: col.title,// 用于数据匹配的 keykey: col.dataIndex,// 列宽width: col.width / 5 || DEFAULT_COLUMN_WIDTH,};return obj;})
}
复制代码

ExcelJS中,header 字段表示显示的表头内容,key 是用于匹配数据的 key,width 是列宽。在 Table 的 column 中都有对应的字段,取出来赋值即可。
注意设置列宽的时候,在线表格和 excel 的单位可能不一致,需要除以一个系数才不至于太宽。至于具体除多少,可以不断试验得出个最佳值,我试的除以 5 效果比较好。

通过 worksheet.addRows()方法可以为工作表添加多行数据,因为上面我们已经设置了表头,程序知道了每列数据应该匹配哪个字段,所以这里直接传入 Table 的 dataSource 即可。

也可以通过 worksheet.addRow()逐行添加数据。

下载 excel

saveWorkbook()也是自己封装的方法,接收 workbook 和文件名来下载 excel 到本地。

下载是使用 file-saver库。

import {saveAs} from "file-saver";
import {Workbook} from "exceljs";export function saveWorkbook(workbook: Workbook, fileName: string) {// 导出文件workbook.xlsx.writeBuffer().then((data => {const blob = new Blob([data], {type: ''});saveAs(blob, fileName);}))
}
复制代码

到此,可以通过短短几行代码实现 AntD 的 Table 导出啦。

四、修改样式

单元格,行和列均支持一组丰富的样式和格式,这些样式和格式会影响单元格的显示方式。

通过分配以下属性来设置样式:

  • numFmt[7]

  • font[8]

  • alignment[9]

  • border[10]

  • fill[11]

添加背景色

我们先给表头添加背景。因为表头是第一行,可以通过 getRow(1) 来获取表头这一行:

// 给表头添加背景色
let headerRow = worksheet.getRow(1);
headerRow.fill = {type: 'pattern',pattern: 'solid',fgColor: {argb: 'dff8ff'},
}
复制代码

可以直接用 row.fill为整行设置背景色,这样的话这一行没有内容的单元格也会有颜色,如图:

从 E 列开始其实就没有数据了,如果只想给非空单元格设置背景呢?

很遗憾 row 暴露的方法不支持直接这样设置,但可以曲线救国,遍历本行的所有非空单元格,再给每个单元格设置背景即可。

// 通过 cell 设置背景色,更精准
headerRow.eachCell((cell, colNum) => {cell.fill = {type: 'pattern',pattern: 'solid',fgColor: {argb: 'dff8ff'},}
})
复制代码

使用单元格控制会更加的精准,可以看到空的单元格已经没有背景色了。

修改字体样式

可以设置文字的字体、字号、颜色等属性,支持的属性如下表:

字体属性 描述 示例值
name 字体名称。 'Arial', 'Calibri', etc.
family 备用字体家族。整数值。 1 - Serif, 2 - Sans Serif, 3 - Mono, Others - unknown
scheme 字体方案。 'minor', 'major', 'none'
charset 字体字符集。整数值。 1, 2, etc.
size 字体大小。整数值。 9, 10, 12, 16, etc.
color 颜色描述,一个包含 ARGB 值的对象。 { argb: 'FFFF0000'}
bold 字体 粗细 true, false
italic 字体 倾斜 true, false
underline 字体 下划线 样式 true, false, 'none', 'single', 'double', 'singleAccounting', 'doubleAccounting'
strike 字体 删除线 true, false
outline 字体轮廓 true, false
vertAlign 垂直对齐 'superscript', 'subscript'

与设置背景色相同,可以通过 row 或 cell 来设置。示例将通过 cell 设置。

修改表头的字体为微软雅黑,字号12号,颜色为红色,加粗斜体。

// 通过 cell 设置样式,更精准
headerRow.eachCell((cell, colNum) => {// 设置背景色cell.fill = {type: 'pattern',pattern: 'solid',fgColor: {argb: 'dff8ff'},}// 设置字体cell.font = {bold: true,italic: true,size: 12,name: '微软雅黑',color: {argb: 'ff0000'},};
})
复制代码

设置对齐方式

有效的对齐属性:

horizontal vertical wrapText shrinkToFit indent readingOrder textRotation
left top true true integer rtl 0 to 90
center middle false false ltr -1 to -90
right bottom vertical
fill distributed
justify justify
centerContinuous
distributed

表格默认的对齐方式是靠下对齐,一般都会设置为垂直方向居中对齐,文本靠左对齐,数字靠右对齐。这里为了方便都设置为水平方向靠左对齐,垂直方向居中对齐。

// 添加行
let rows = worksheet.addRows(list);
rows?.forEach(row => {// 设置字体row.font = {size: 11,name: '微软雅黑',};// 设置对齐方式row.alignment = {vertical: 'middle', horizontal: 'left', wrapText: false,};
})
复制代码

addRows()的返回值是被添加的行的数组,然后循环对每行设置字体和对齐方式,就完成了对整个 excel 的样式自定义。

当然也可以对每个 cell 进行设置,效果是一样的。

设置边框也是同样的方法,这里不做介绍啦。

完整的导出带样式的 excel 代码:

// 导出function onExportBasicExcelWithStyle() {// 创建工作簿const workbook = new ExcelJs.Workbook();// 添加sheetconst worksheet = workbook.addWorksheet('demo sheet');// 设置 sheet 的默认行高worksheet.properties.defaultRowHeight = 20;// 设置列worksheet.columns = generateHeaders(columns);// 给表头添加背景色。因为表头是第一行,可以通过 getRow(1) 来获取表头这一行let headerRow = worksheet.getRow(1);// 直接给这一行设置背景色// headerRow.fill = {//   type: 'pattern',//   pattern: 'solid',//   fgColor: {argb: 'dff8ff'},// }// 通过 cell 设置样式,更精准headerRow.eachCell((cell, colNum) => {// 设置背景色cell.fill = {type: 'pattern',pattern: 'solid',fgColor: {argb: 'dff8ff'},}// 设置字体cell.font = {bold: true,italic: true,size: 12,name: '微软雅黑',color: {argb: 'ff0000'},};// 设置对齐方式cell.alignment = {vertical: 'middle', horizontal: 'left', wrapText: false,};})// 添加行let rows = worksheet.addRows(list);// 设置每行的样式rows?.forEach(row => {// 设置字体row.font = {size: 11,name: '微软雅黑',};// 设置对齐方式row.alignment = {vertical: 'middle', horizontal: 'left', wrapText: false,};})// 导出excelsaveWorkbook(workbook, 'simple-demo.xlsx');}
复制代码

五、行合并&列合并

先看在线表格的效果:

导出的 excel:

这个表格涉及到多级表头、行合并、列合并。

涉及到以下几个重难点:

  • Table 表头的解析。多级表头有 children,要解析 Table 的 columns 为想要的数据结构。

  • 列合并。一块内容占用了多个单元格,要进行一行中多个列的列合并,如成绩和老师评语列。

  • 行合并。表头其实是占了两行,除了成绩外,其他的列都应该把两行合并为一行。

  • 行和列同时合并。如果一个单元格合并过一次,就不能再合并,所以如果有行和列都需要合并的单元格,必须一次性同时进行行和列合并,不能拆开为两步。如老师评语列。

  • 表头和数据的样式调整。

先贴出完整的代码

import React, {useEffect, useState} from 'react'
import {Button, Card, Space, Table} from "antd";
import {ColumnsType} from "antd/lib/table/interface";
import {ITableHeader, StudentInfo} from "../types";
import * as ExcelJs from "exceljs";
import {addHeaderStyle,DEFAULT_COLUMN_WIDTH, DEFAULT_ROW_HEIGHT,generateHeaders,getColumnNumber,mergeColumnCell,mergeRowCell,saveWorkbook
} from "../utils";
import {Worksheet} from "exceljs";interface MultiHeaderProps {
}const columns: ColumnsType<any> = [{width: 50,dataIndex: 'id',key: 'id',title: 'ID',},{width: 100,dataIndex: 'name',key: 'name',title: '姓名',},{width: 50,dataIndex: 'age',key: 'age',title: '年龄',},{width: 80,dataIndex: 'gender',key: 'gender',title: '性别',},{dataIndex: 'score',key: 'score',title: '成绩',children: [{width: 80,dataIndex: 'english',key: 'english',title: '英语',},{width: 80,dataIndex: 'math',key: 'math',title: '数学',},{width: 80,dataIndex: 'physics',key: 'physics',title: '物理',},]},{width: 250,dataIndex: 'comment',key: 'comment',title: '老师评语',},
];const MultiHeader: React.FC<MultiHeaderProps> = () => {const [list, setList] = useState<StudentInfo[]>([]);useEffect(() => {generateData();}, [])function generateData() {let arr: StudentInfo[] = [];for (let i = 0; i < 5; i++) {arr.push({id: i,name: `小明${i}号`,age: 8+i,gender: i % 2 === 0 ? '男' : '女',english: 80 + i,math: 60 + i,physics: 70 + i,comment: `小明${i}号同学表现非常好,热心助人,成绩优秀,是社会主义接班人`})}setList(arr);}function onExportMultiHeaderExcel() {// 创建工作簿const workbook = new ExcelJs.Workbook();// 添加sheetconst worksheet = workbook.addWorksheet('demo sheet');// 设置 sheet 的默认行高worksheet.properties.defaultRowHeight = 20;// 解析 AntD Table 的 columnsconst headers = generateHeaders(columns);console.log({headers})// 第一行表头const names1: string[] = [];// 第二行表头const names2: string[] = [];// 用于匹配数据的 keysconst headerKeys: string[] = [];headers.forEach(item => {if (item.children) {// 有 children 说明是多级表头,header name 需要两行item.children.forEach(child => {names1.push(item.header);names2.push(child.header);headerKeys.push(child.key);});} else {const columnNumber = getColumnNumber(item.width);for (let i = 0; i < columnNumber; i++) {names1.push(item.header);names2.push(item.header);headerKeys.push(item.key);}}});handleHeader(worksheet, headers, names1, names2);// 添加数据addData2Table(worksheet, headerKeys, headers);// 给每列设置固定宽度worksheet.columns = worksheet.columns.map(col => ({ ...col, width: DEFAULT_COLUMN_WIDTH }));// 导出excelsaveWorkbook(workbook, 'simple-demo.xlsx');}function handleHeader(worksheet: Worksheet,headers: ITableHeader[],names1: string[],names2: string[],) {// 判断是否有 children, 有的话是两行表头const isMultiHeader = headers?.some(item => item.children);if (isMultiHeader) {// 加表头数据const rowHeader1 = worksheet.addRow(names1);const rowHeader2 = worksheet.addRow(names2);// 添加表头样式addHeaderStyle(rowHeader1, {color: 'dff8ff'});addHeaderStyle(rowHeader2, {color: 'dff8ff'});mergeColumnCell(headers, rowHeader1, rowHeader2, names1, names2, worksheet);return;}// 加表头数据const rowHeader = worksheet.addRow(names1);// 表头根据内容宽度合并单元格mergeRowCell(headers, rowHeader, worksheet);// 添加表头样式addHeaderStyle(rowHeader, {color: 'dff8ff'});}function addData2Table(worksheet: Worksheet, headerKeys: string[], headers: ITableHeader[]) {list?.forEach((item: any) => {const rowData = headerKeys?.map(key => item[key]);const row = worksheet.addRow(rowData);mergeRowCell(headers, row, worksheet);row.height = DEFAULT_ROW_HEIGHT;// 设置行样式, wrapText: 自动换行row.alignment = { vertical: 'middle', wrapText: false, shrinkToFit: false };row.font = { size: 11, name: '微软雅黑' };})}return (<Card><h3>多表头表格</h3><Space style={{marginBottom: 10}}><Button type={'primary'} onClick={onExportMultiHeaderExcel}>导出excel</Button></Space><Tablekey={'id'}columns={columns}dataSource={list}/></Card>);
}export default MultiHeader
复制代码

前面几步创建 workbook 和 worksheet 都是一样的,从解析表头 generateHeaders() 开始逻辑会有所不同。

表头解析

我们修改上一节的generateHeaders()方法,添加有 children 时的逻辑。多级表头时我们也构造出 children。

// 根据 antd 的 column 生成 exceljs 的 column
export function generateHeaders(columns: any[]) {return columns?.map(col => {const obj: ITableHeader = {// 显示的 nameheader: col.title,// 用于数据匹配的 keykey: col.dataIndex,// 列宽width: col.width / 5 || DEFAULT_COLUMN_WIDTH,};if (col.children) {obj.children = col.children?.map((item: any) => ({key: item.dataIndex,header: item.title,width: item.width,parentKey: col.dataIndex,}));}return obj;})
}
复制代码

构造出来的数据结构如下:

上一节简单表格中我们用 worksheet.columns = generateHeaders(columns)设置每一个表头列所要显示的信息和应该匹配的 key,但是它无法设置多级表头,所以需要换一种思路,摒弃列(表头)的概念,把表头也当成一行数据来自己写入。下面的每行数据,也都自己通过计算匹配出应该在什么位置显示什么内容。

先来看这段代码:

// 解析 AntD Table 的 columns
const headers = generateHeaders(columns);
// 第一行表头
const names1: string[] = [];
// 第二行表头
const names2: string[] = [];
// 用于匹配数据的 keys
const headerKeys: string[] = [];
headers.forEach(item => {if (item.children) {// 有 children 说明是多级表头,header name 需要两行item.children.forEach(child => {names1.push(item.header);names2.push(child.header);headerKeys.push(child.key);});} else {const columnNumber = getColumnNumber(item.width);for (let i = 0; i < columnNumber; i++) {names1.push(item.header);names2.push(item.header);headerKeys.push(item.key);}}
});
复制代码

这个例子有两级表头,所以需要两行来设置每一级表头,分别命名为 names1和 names2,它们里面存的是展示出来的 name,如:ID、姓名、年龄等。还需要一个headerKeys用来存储每一列需要匹配的 key,如:id、name、age 等 json 的 key。

注意一点,headerKeys是以第二行表头为准,因为第二行才是真正显示的内容。

构造出了 names1names2headerKeys,就可以开始生成真正的表头了:

function handleHeader(worksheet: Worksheet,headers: ITableHeader[],names1: string[],names2: string[],) {// 判断是否有 children, 有的话是两行表头const isMultiHeader = headers?.some(item => item.children);if (isMultiHeader) {// 加表头数据const rowHeader1 = worksheet.addRow(names1);const rowHeader2 = worksheet.addRow(names2);// 添加表头样式addHeaderStyle(rowHeader1, {color: 'dff8ff'});addHeaderStyle(rowHeader2, {color: 'dff8ff'});mergeColumnCell(headers, rowHeader1, rowHeader2, names1, names2, worksheet);return;}// 加表头数据const rowHeader = worksheet.addRow(names1);// 表头根据内容宽度合并单元格mergeRowCell(headers, rowHeader, worksheet);// 添加表头样式addHeaderStyle(rowHeader, {color: 'dff8ff'});}
复制代码

先判断有没有多级表头,单行表头和多行表头执行的逻辑不同。

通过 worksheet.addRow()将表头添加为一行数据,多行表头就添加两次。然后通过 addHeaderStyle()给表头添加样式,这是自己封装的方法,在 utils里。最后也是最重要的是合并单元格,

合并同一行多列

合并单元格的方法是 worksheet.mergeCells(),可以有很多种合并方式:

// 合并一系列单元格
worksheet.mergeCells('A4:B5');// ...合并的单元格被链接起来了
worksheet.getCell('B5').value = 'Hello, World!';
expect(worksheet.getCell('B5').value).toBe(worksheet.getCell('A4').value);
expect(worksheet.getCell('B5').master).toBe(worksheet.getCell('A4'));// ...合并的单元格共享相同的样式对象
expect(worksheet.getCell('B5').style).toBe(worksheet.getCell('A4').style);
worksheet.getCell('B5').style.font = myFonts.arial;
expect(worksheet.getCell('A4').style.font).toBe(myFonts.arial);// 取消单元格合并将打破链接的样式
worksheet.unMergeCells('A4');
expect(worksheet.getCell('B5').style).not.toBe(worksheet.getCell('A4').style);
expect(worksheet.getCell('B5').style.font).not.toBe(myFonts.arial);// 按左上,右下合并
worksheet.mergeCells('K10', 'M12');// 按开始行,开始列,结束行,结束列合并(相当于 K10:M12)
worksheet.mergeCells(10,11,12,13);
复制代码

先看合并同一行多列的算法,核心在于先设置一个索引,从1开始,代表第一列。然后循环 headers,如果当前 header 有 children,则每个子级占一列,然后索引值加1。如果没有 children,计算这一个数据的宽度将会占用几个单元格,也就是几列,这个列数就是需要合并的列数,合并完之后索引值加1。

// 行合并单元格
export function mergeRowCell(headers: ITableHeader[], row: Row, worksheet: Worksheet) {// 当前列的索引let colIndex = 1;headers.forEach(header => {const { width, children } = header;if (children) {children.forEach(child => {colIndex += 1;});} else {// 需要的列数,四舍五入const colNum = getColumnNumber(width);// 如果 colNum > 1 说明需要合并if (colNum > 1) {worksheet.mergeCells(Number(row.number), colIndex, Number(row.number), colIndex + colNum - 1);}colIndex += colNum;}});
}export function getColumnNumber(width: number) {// 需要的列数,四舍五入return Math.round(width / DEFAULT_COLUMN_WIDTH);
}
复制代码

合并单元格的方法是:

worksheet.mergeCells(Number(row.number), colIndex, Number(row.number), colIndex + colNum \- 1);

四个参数分别是合并的开始行、开始列、结束行、结束列。

通过 row.number得到当前行的行数,因为是同一行的多列合并,所以开始结束行一致,开始列是索引值 colIndex,结束列是 colIndex + colNum \- 1

同时合并行和列

如果是多级表头,需要同时处理行和列合并,用到了封装的 mergeColumnCell方法。

基本思路是先判断合并的类型,一共有三种情况:

  • 只有行合并

  • 只有列合并

  • 同时进行行和列合并

然后计算出起始的行和列,以及结束的行和列。

// 合并行和列,用于处理表头合并
export function mergeColumnCell(headers: ITableHeader[],rowHeader1: Row,rowHeader2: Row,nameRow1: string[],nameRow2: string[],worksheet: Worksheet,
) {// 当前 index 的指针let pointer = -1;nameRow1.forEach((name, index) => {// 当 index 小于指针时,说明这一列已经被合并过了,不能再合并if (index <= pointer) return;// 是否应该列合并const shouldVerticalMerge = name === nameRow2[index];// 是否应该行合并const shouldHorizontalMerge = index !== nameRow1.lastIndexOf(name);pointer = nameRow1.lastIndexOf(name);if (shouldVerticalMerge && shouldHorizontalMerge) {// 两个方向都合并worksheet.mergeCells(Number(rowHeader1.number),index + 1,Number(rowHeader2.number),nameRow1.lastIndexOf(name) + 1,);} else if (shouldVerticalMerge && !shouldHorizontalMerge) {// 只在垂直方向上同一列的两行合并worksheet.mergeCells(Number(rowHeader1.number), index + 1, Number(rowHeader2.number), index + 1);} else if (!shouldVerticalMerge && shouldHorizontalMerge) {// 只有水平方向同一行的多列合并worksheet.mergeCells(Number(rowHeader1.number),index + 1,Number(rowHeader1.number),nameRow1.lastIndexOf(name) + 1,);// eslint-disable-next-line no-param-reassignconst cell = rowHeader1.getCell(index + 1);cell.alignment = { vertical: 'middle', horizontal: 'center' };}});
}
复制代码

添加数据行

在计算表头时,已经得到了每列的 key 值列表 headerKeys,通过headerKeys可以取出每一列对应的具体数据。

function addData2Table(worksheet: Worksheet, headerKeys: string[], headers: ITableHeader[]) {list?.forEach((item: any) => {const rowData = headerKeys?.map(key => item[key]);const row = worksheet.addRow(rowData);mergeRowCell(headers, row, worksheet);row.height = DEFAULT_ROW_HEIGHT;// 设置行样式, wrapText: 自动换行row.alignment = { vertical: 'middle', wrapText: false, shrinkToFit: false };row.font = { size: 11, name: '微软雅黑' };})
}
复制代码

先循环数据列表,然后循环 headerKeys取出对应的值,再通过 worksheet.addRow将这一行数据添加进表格中。由于可能出现一个字段占用多列的情况,所以还需要进行合并单元格操作,可以复用 mergeRowCell()方法。最后设置每行的样式,即可得到最终的数据。

一个 sheet 中放多张表

在导出多级表头表格的时候,我们写表头和数据行都是用的worksheet.addRow方法,而没有用 worksheet.column设置表格的表头,这样更加灵活,每一列想显示什么内容完全自己控制。

处理多个表格时,也可以用同样的方法。因为每一行数据都是自己写入的,所以不管有几张表都没有关系,我们关心的只有每一行的数据。

同时我们做了行和列合并算法,可以实现每一张表的每一列都能定制宽度。

可以将上面两个例子结合起来,导出到一个 sheet里,就实现了一个sheet中放多张表的需求。

结语

除了导出 xlsx,ExcelJS[12] 还支持导出 csv格式。此外还有设置页眉页脚、操作视图、添加公式、使用富文本等功能,非常的强大。

官方的文档也很详细,不懂的地方直接看文档即可。

源码地址:github.com/cachecats/excel-export-demo[13]

关于本文

作者:solocoder

https://juejin.cn/post/7071882317953761316

Node 社群我组建了一个氛围特别好的 Node.js 社群,里面有很多 Node.js小伙伴,如果你对Node.js学习感兴趣的话(后续有计划也可以),我们可以一起进行Node.js相关的交流、学习、共建。下方加 考拉 好友回复「Node」即可。如果你觉得这篇内容对你有帮助,我想请你帮我2个小忙:1. 点个「在看」,让更多人也能看到这篇文章2. 订阅官方博客 www.inode.club 让我们一起成长点赞和在看就是最大的支持

前端复杂表格一键导出看这篇就够了(附源码)相关推荐

  1. C#使用Xamarin开发可移植移动应用(2.Xamarin.Forms布局,本篇很长,注意)附源码

    前言 源码地址:https://github.com/l2999019/DemoApp 可以Star一下,随意 - - 一点感想 很意外的,第一篇博文被博客园的编辑大哥置顶了.感谢. 评论也很多,褒贬 ...

  2. C# 30分钟完成百度人脸识别——进阶篇(文末附源码)

    距离上次入门篇时隔两个月才出这进阶篇,小编惭愧,对不住关注我的卡哇伊的小伙伴们,为此小编用这篇博来谢罪. 前面的准备工作我就不说了,注册百度账号api,创建web网站项目,引入动态链接库引入. 不了解 ...

  3. 如何解决和异地女朋友一起看电影的需求?(内附源码)

    一.这是一件悲伤的故事 我是一名程序员,我和我的女友是异地恋,so sad!!! 一次视频时,她突然来了一句:"我们已经很久没有一起看电影了吧......" "呃...好 ...

  4. List转Page对象,看这篇就够了,附代码、实验结果分页自己编程实现

    List 转 Page 对象 处理统计等数据时分页展示.第三方接口仅返回list,需分页显示等需求 public class ConvertList2PageVOUtil {/*** List 转 P ...

  5. 遥遥领先一键导出微信小程序、UNIAPP低代码源码使用介绍

    1.1添加组件至设计器 在左侧基本组件库选择要使用的组件,按住左键不放,拖动到设计器适合的位置上松开. 1.2修改组件图层名称 修改组件图层名称,方便快速定位组件位置.找到图层面板按钮,点击显示图层面 ...

  6. 2018年强化学习领域十篇重要论文(附源码)

    2019-06-06 09:47:57 来自网络 与其他机器学习方法相比,比如监督式学习.迁移学习.甚至非监督式学习学习相比,深度强化学习方法极其需要大量数据,而且常常不稳定,从性能上来说可能不是最好 ...

  7. 【超全汇总】学习数据结构与算法,计算机基础知识,看这篇就够了【ZT帅地】2020-3-7

    https://blog.csdn.net/m0_37907797/article/details/104029002 由于文章有点多,并且发的文章也不是一个系列一个系列发的,不过我的文章大部分都是围 ...

  8. C#使用Xamarin开发可移植移动应用(4.进阶篇MVVM双向绑定和命令绑定)附源码

    今天的学习内容? 今天我们讲讲Xamarin中的MVVM双向绑定,嗯..需要有一定的MVVM基础.,具体什么是MVVM - -,请百度,我就不多讲了 效果如下: 正文 1.简单的入门Demo 这个时间 ...

  9. C#使用Xamarin开发可移植移动应用进阶篇(8.打包生成安卓APK并精简大小),附源码

    我记得,之前在写安卓方面的文章的时候,有人就问过我.Xamarin.Android为什么打包出来这么大?随便一个HelloWord就20-30MB? 嗯..今天我们就来解决这个问题.. 我们先从指定一 ...

最新文章

  1. mac 香港购买分享
  2. VML编程之------VML语言入门《VML极道教程》原著:沐缘华
  3. dos下的edit命令使用详解
  4. hadoop程序实例
  5. 用Java编辑实现warshall算法_warshall算法 java实现
  6. 使用sourcetree 的git flow
  7. 【路径规划】基于matlab粒子群融合遗传算法栅格地图路径规划【含Matlab源码 526期】
  8. 从JavaEye社区被迫改名说起(转载他人博客)
  9. 谷歌浏览器Vue插件
  10. css3实现缺角四边形_利用CSS3实现折角效果实例源码
  11. pdf旋转后如何保存?操作工具有这些
  12. 建立FTP服务器的免费软件
  13. 301跳转有什么用?为什么要做301跳转?
  14. HDU6130 Kolakoski
  15. SAP MR21修改标准价
  16. java八大基本数据类型基本用法(含数据输入输出)
  17. ps切片 html作用,ps切片有什么用,PS切片是什么
  18. 肠道菌群检测粪便样本的采集保存方法
  19. 这里有一个url=https://www/.baidu.com/s?id=111name=yourname,写一个函数获取query的参数和值存放在一个对象...
  20. Hibernate Validator 后台数据规则校验拓展

热门文章

  1. Joomla模板制作教程:菜单
  2. 关于batik库在Maven中版本问题
  3. Nervos:链外扩容 + Layer 2才是区块链的未来(上)
  4. 【Anroid界面实现】通用的桌面悬浮窗口的实现
  5. 算法 - 希尔排序(Shell Sort)
  6. 拜托周杰伦,请不要强奸霍元甲!
  7. vi 编辑器 大法 之 dgg 删除 从当前光标所在行到第一行
  8. 短信发送工具,便捷开发,直接拿去用
  9. 完全碰撞问题 台球碰撞
  10. MySQL数据库基础 作者: SoumnsJ