前端导出 excel 的需求很多,但市面上好用的库并不多,讲明白复杂使用场景的文章更少。
本文将以文字 + demo 源码的形式,力求讲清楚满足 99% 使用场景的终极 excel 导出方案。
如果项目中用到了 AntD,那就更简单了,因为 Table 本身已经设置好了 column 和 dataSource,只需解析 column 和 dataSource 即可快速导出 Excel。
实现功能:
源码地址:https://github.com/cachecats/excel-export-demo
第二篇文章:js 批量导出 excel 为zip压缩包, 对导出方法进行了封装,还实现了使用 exceljs
、file-saver
、jszip
实现下载包含多层级文件夹、多个 excel、每个 excel 支持多个 sheet 的 zip 压缩包。
呼声最高的是 xlsx,又叫 SheetJS
,也是下载量最高和 star
最多的库。试用了一下很强大,但是!默认不支持改变样式,想要支持改变样式,需要使用它的收费版本。
本着勤俭节约的原则,很多人使用了另一个第三方库:xlsx-style,但是使用起来极其复杂,还需要改 node_modules 源码,这个库最后更新时间也定格在了 6年前。还有一些其他的第三方样式拓展库,质量参差不齐。
使用成本和后期的维护成本很高,不得不放弃。
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:工作簿,可以理解为整个 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);
工作表,即 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}]});
列,通过 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);
行,可以添加一行或者同时添加多行数据,是使用最频繁的属性。
// 通过 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 = () => {
const [list, setList] = useState([]);
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 = [
{
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();
// 添加sheet
const worksheet = workbook.addWorksheet('demo sheet');
// 设置 sheet 的默认行高
worksheet.properties.defaultRowHeight = 20;
// 设置列
worksheet.columns = generateHeaders(columns);
// 添加行
worksheet.addRows(list);
// 导出excel
saveWorkbook(workbook, 'simple-demo.xlsx');
}
return (
简单表格
);
}
export default SimpleDemo
真正导出的代码只有几行,重点看 onExportBasicExcel
方法:
因为我们是用 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 = {
// 显示的 name
header: col.title,
// 用于数据匹配的 key
key: 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()
逐行添加数据。
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 导出啦。
单元格,行和列均支持一组丰富的样式和格式,这些样式和格式会影响单元格的显示方式。
通过分配以下属性来设置样式:
我们先给表头添加背景。因为表头是第一行,可以通过 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();
// 添加sheet
const 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,};
})
// 导出excel
saveWorkbook(workbook, 'simple-demo.xlsx');
}
先看在线表格的效果:
导出的 excel:
这个表格涉及到多级表头、行合并、列合并。
涉及到以下几个重难点:
先贴出完整的代码
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 = [
{
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 = () => {
const [list, setList] = useState([]);
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();
// 添加sheet
const worksheet = workbook.addWorksheet('demo sheet');
// 设置 sheet 的默认行高
worksheet.properties.defaultRowHeight = 20;
// 解析 AntD Table 的 columns
const headers = generateHeaders(columns);
console.log({headers})
// 第一行表头
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);
}
}
});
handleHeader(worksheet, headers, names1, names2);
// 添加数据
addData2Table(worksheet, headerKeys, headers);
// 给每列设置固定宽度
worksheet.columns = worksheet.columns.map(col => ({ ...col, width: DEFAULT_COLUMN_WIDTH }));
// 导出excel
saveWorkbook(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 (
多表头表格
);
}
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 = {
// 显示的 name
header: col.title,
// 用于数据匹配的 key
key: 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
是以第二行表头为准,因为第二行才是真正显示的内容。
构造出了 names1
、names2
和headerKeys
,就可以开始生成真正的表头了:
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-reassign
const 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()
方法。最后设置每行的样式,即可得到最终的数据。
在导出多级表头表格的时候,我们写表头和数据行都是用的worksheet.addRow
方法,而没有用 worksheet.column
设置表格的表头,这样更加灵活,每一列想显示什么内容完全自己控制。
处理多个表格时,也可以用同样的方法。因为每一行数据都是自己写入的,所以不管有几张表都没有关系,我们关心的只有每一行的数据。
同时我们做了行和列合并算法,可以实现每一张表的每一列都能定制宽度。
可以将上面两个例子结合起来,导出到一个 sheet
里,就实现了一个sheet
中放多张表的需求。
除了导出 xlsx
,ExcelJS 还支持导出 csv
格式。此外还有设置页眉页脚、操作视图、添加公式、使用富文本等功能,非常的强大。
官方的文档也很详细,不懂的地方直接看文档即可。
源码地址:https://github.com/cachecats/excel-export-demo