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

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

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

实现功能:

  • 简单表格导出
  • 为表格添加样式(更改背景色、更换字体、字号、颜色)
  • 设置行高、列宽
  • 解析 ant-design 的 Table 直接导出excel,根据 antd 页面中设置的列宽动态计算 excel 中的列宽
  • 多级表头(行合并、列合并)
  • 一个 sheet 中放多张表,并实现每张表的列宽不同

源码地址:https://github.com/cachecats/excel-export-demo

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

一、技术选型

xlsx

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

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

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

ExcelJS

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

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

安装:

npm install exceljs

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

npm install file-saver

二、基本概念

先了解下基本概念,更详细的介绍参考官方文档:https://github.com/exceljs/exceljs/blob/HEAD/README_zh.md

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
  • font
  • alignment
  • border
  • fill

添加背景色

我们先给表头添加背景。因为表头是第一行,可以通过 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);}}
});

这个例子有两级表头,所以需要两行来设置每一级表头,分别命名为 names1names2,它们里面存的是展示出来的 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 还支持导出 csv格式。此外还有设置页眉页脚、操作视图、添加公式、使用富文本等功能,非常的强大。

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

源码地址:https://github.com/cachecats/excel-export-demo

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

  1. Java 中如何解决 POI 读写 excel 几万行数据时内存溢出的问题?(附源码)

    >>号外:关注"Java精选"公众号,菜单栏->聚合->干货分享,回复关键词领取视频资料.开源项目. 1. Excel2003与Excel2007 两个版本 ...

  2. 上传图片到linux返回url,Springboot 将前端传递的图片上传至Linux服务器并返回图片的url(附源码)...

    问题由来: 用户个人信息需要添加头像功能 当前端程序是微信小程序时,前端将直接将图片 url 传送至服务端 但是当前端是 Web 页面时,前端传递的参数是一张图片,服务端需要将图片保存至 Linux ...

  3. 今天不抠图,Python实现一键换底片!想换什么换什么(附源码)

    前言 本文的文字及图片来源于网络,仅供学习.交流使用,不具有任何商业用途,版权归原作者所有,如有问题请及时联系我们以作处理. 生活中我们会拍很多的证件照,有的要求红底,有的是白底,有的是蓝底,今天不通 ...

  4. Python数据分析实战-提取DataFrame(Excel)某列(字段)最全操作(附源码和实现效果)

    实现功能: Python数据分析实战-提取DataFrame(Excel)某列(字段)最全操作,代码演示了单列提取和多列提取两种情况,其中单列提取有返回series格式和dataframe两种情况,在 ...

  5. Python脚本一键找出哪些微信好友删了你(附源码)

    查看被删的微信好友 原理就是新建群组,如果加不进来就是被删好友了(不要在群组里讲话,别人是看不见的) 用的是微信网页版的接口 查询结果可能会引起一些心理上的不适,请小心使用-(逃 还有些小问题: 结果 ...

  6. 用Python玩弄微信朋友圈,一键制作好友图片墙酷炸朋友圈(附源码)

    上午发了张我微信近 2000 位好友的头像拼图,让大伙儿看能不能快速找到自己的头像,没想到反响很强烈,引得阵阵惊呼与膜拜,没有料到. 有没有犯密集恐惧症?这并不震撼,如果你有 5000 位好友的话,做 ...

  7. 前端复杂表格一键导出看这篇就够了(附源码)

    大厂技术  高级前端  Node进阶 点击上方 程序员成长指北,关注公众号 回复1,加入高级Node交流群 前端导出 excel 的需求很多,但市面上好用的库并不多,讲明白复杂使用场景的文章更少. 本 ...

  8. php页面表格导出excel表格数据类型,php页面表格导出excel表格数据类型-php导出excel是不是导出整个表的?可不可以导出指......

    php 怎么把数据导出到excel表格 昨天项目里有个新需求,客户希望把一些数据能导出成为Excel表格,刚开始用PHP原生输入Excel表格,发现效果不是很理想,于是找到一个比较著名的库:PHPEx ...

  9. C#开发的高性能EXCEL导入、导出工具DataPie(支持MSSQL、ORACLE、ACCESS,附源码下载地址)...

    作 为财务数据核算人员,面对大量的业务与财务数据,借助于传统的EXCEL表格,已经力不从心.最近几个月,利用周末及下班的空闲时间,写了一个数据库导入 导出工具,以方便业务逻辑密集型的数据处理.目前,D ...

最新文章

  1. 编写可调模板并使用Auto-tuner自动调谐器
  2. 应用交换技术的负载均衡算法
  3. Xcode 7.0正式版发布了
  4. java中wait方法使用实例_java中wait、notify和notifyAll的概念用法和例子?
  5. 20155201 网络攻防技术 实验六 信息搜集与漏洞
  6. jQuery-事件委托(基本概述+实例)
  7. option标签selected=selected属性失效的问题
  8. mybatis 打印SQL
  9. loopback接口、router ID详解
  10. python抓取贴吧_python抓取百度贴吧-校花吧,网页图片
  11. Jersey客户端API调用REST风格的Web服务
  12. [解决]RESTEASY003215: could not find writer for content-type text/html type: java.lang.String
  13. u9系统的使用方法仓库_用友ERP系统,U9操作流程图
  14. 捷联惯导系统学习7.5(简化的捷联惯导算法及误差方程 )
  15. 性能服务器闹钟功能,发一个目前功能和性能最全的时钟!
  16. centos服务器无法上网
  17. RecyclerView条目复用导致混乱的解决方案之一
  18. 凸优化工具包CVX快速入门
  19. Unity发布WebGL不显示中文字体问题
  20. 改变tiff图片像素大小

热门文章

  1. 打码色带在包装行业的应用
  2. 彩色图像-色彩空间 HSI(HSL)、HSV(HSB)
  3. 为什么学会了结构化思维,还是不会沟通?
  4. LayoutInflater的错误用法(Avoid passing null as the view root )
  5. 实时获取卫星两行数据代码
  6. ARM裸机开发:主频与时钟
  7. 牛客暑期训练营三 A题
  8. C++程序设计 (自考课程代码4737)课后编程题答案
  9. Spring中Bean对象的存储和获取
  10. MSP430使用库函数上手