ExcelJS导出Ant Design Table数据为Excel文件

December 17, 2023
测试
测试
测试
测试
20 分钟阅读

我们的项目中需要导出 Excel 的需求还是挺多的,找了一个处理导出 Excel 的库 ExcelJS ,npm包。

可以满足(但不限于)以下需求:

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

ExcelJS 

ExcelJS 周下载量 430k+,github star 9k+,有中文文档。虽然文档是以README 的形式,可读性不太好,但重在内容,常用的功能基本都有覆盖。

一年内有更新,试用了一下,集成简单,文档也比较丰富。

安装:

npm install exceljs

还需要搭配另外一个库:file-saver

npm install file-saver

在页面中引用

import * as ExcelJs from 'exceljs';
import { saveAs } from 'file-saver';

常用接口

Workbook 工作簿

可以理解为整个表格。

创建工作簿:

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); // 最后打印

// 将工作簿日期设置为 1904 年日期系统
workbook.properties.date1904 = true;
worksheet 工作表

即 Excel 中的 sheet 页。

添加工作表:

const sheet = workbook.addWorksheet('My Sheet');

指定工作表的选项:

使用 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;

// 为列设置大纲级别
worksheet.getColumn(4).outlineLevel = 0;
worksheet.getColumn(5).outlineLevel = 1;

// 列支持一个只读字段,以指示基于 `OutlineLevel` 的折叠状态
expect(worksheet.getColumn(4).collapsed).to.equal(false);
expect(worksheet.getColumn(5).collapsed).to.equal(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 行

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

// 获取一个行对象。如果尚不存在,则将返回一个新的空对象
const row = worksheet.getRow(5);

// Get multiple row objects. If it doesn't already exist, new empty ones will be returned
const rows = worksheet.getRows(5, 2); // start, length (>0, else undefined is returned)

// 获取工作表中的最后一个可编辑行(如果没有,则为 `undefined`)
const row = worksheet.lastRow;

// 设置特定的行高
row.height = 42.5;

// 隐藏行
row.hidden = true;

// 为行设置大纲级别
worksheet.getRow(4).outlineLevel = 0;
worksheet.getRow(5).outlineLevel = 1;

// 行支持一个只读字段,以指示基于 `OutlineLevel` 的折叠状态
expect(worksheet.getRow(4).collapsed).to.equal(false);
expect(worksheet.getRow(5).collapsed).to.equal(true);


row.getCell(1).value = 5; // A5 的值设置为5
row.getCell('name').value = 'Zeb'; // B5 的值设置为 “Zeb” - 假设第2列仍按名称键入
row.getCell('C').value = new Date(); // C5 的值设置为当前时间

// 获取行并作为稀疏数组返回
// 注意:接口更改:worksheet.getRow(4) ==> worksheet.getRow(4).values
row = worksheet.getRow(4).values;
expect(row[5]).toEqual('Kyle');

// 通过连续数组分配行值(其中数组元素 0 具有值)
row.values = [1,2,3];
expect(row.getCell(1).value).toEqual(1);
expect(row.getCell(2).value).toEqual(2);
expect(row.getCell(3).value).toEqual(3);

// 通过稀疏数组分配行值(其中数组元素 0 为 `undefined`)
const values = []
values[5] = 7;
values[10] = 'Hello, World!';
row.values = values;
expect(row.getCell(1).value).toBeNull();
expect(row.getCell(5).value).toEqual(7);
expect(row.getCell(10).value).toEqual('Hello, World!');

// 使用列键按对象分配行值
row.values = {
  id: 13,
  name: 'Thing 1',
  dob: new Date()
};

// 在该行下方插入一个分页符
row.addPageBreak();

// 遍历工作表中具有值的所有行
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);
});

// 提交给流一个完成的行
row.commit();

// 行尺寸
const rowSize = row.cellCount;
const numValues = row.actualCellCount;
表格导出

用 Ant Design Table 写了一个简单的表格,并设置了列宽:

ExcelJS导出Ant Design Table数据为Excel文件 JavaScript 第1张
ExcelJS导出Ant Design Table数据为Excel文件 JavaScript 第1张

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

ExcelJS导出Ant Design Table数据为Excel文件 JavaScript 第2张
ExcelJS导出Ant Design Table数据为Excel文件 JavaScript 第2张

源码:

import { Table, Button } from 'antd';
import React from 'react';
import type { ColumnsType } from 'antd/es/table';
import * as ExcelJs from 'exceljs';
import { saveAs } from 'file-saver';

interface StudentInfo {
  id: number;
  name: string;
  age: number;
}
const tableData: StudentInfo[] = [
  {
    id: 1,
    name: '张秀英',
    age: 44,
  },
  {
    id: 2,
    name: '江勇',
    age: 20,
  },
  {
    id: 3,
    name: '余军',
    age: 32,
  },
  {
    id: 4,
    name: '曹刚',
    age: 44,
  },
  {
    id: 5,
    name: '易艳',
    age: 32,
  },
];
const columns: ColumnsType<StudentInfo> = [
  {
    key: 'id',
    dataIndex: 'id',
    title: '编号',
    width: 150,
  },
  {
    key: 'name',
    dataIndex: 'name',
    title: '姓名',
    width: 200,
  },
  {
    key: 'age',
    dataIndex: 'age',
    title: '年龄',
    width: 150,
  },
];

const List: React.FC = () => {
  // 根据 antd 的 column 生成 exceljs 的 column
  const DEFAULT_COLUMN_WIDTH = 20;
  function generateHeaders(columns: any[]) {
    return columns?.map((col) => {
      const obj = {
        // 显示的 name
        header: col.title,
        // 用于数据匹配的 key
        key: col.dataIndex,
        // 列宽
        width: col.width / 5 || DEFAULT_COLUMN_WIDTH,
      };
      return obj;
    });
  }
  function saveWorkbook(workbook: any, fileName: string) {
    // 导出文件
    workbook.xlsx.writeBuffer().then((data) => {
      const blob = new Blob([data], { type: '' });
      saveAs(blob, fileName);
    });
  }
  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(tableData);

    // 导出excel
    saveWorkbook(workbook, 'simple-demo.xlsx');
  }
  return (
    (tableData && (
      <div style={{ padding: '50px' }}>
        <Button
          type={'primary'}
          style={{ marginBottom: 10 }}
          onClick={onExportBasicExcel}
        >
          导出excel
        </Button>
        <Table columns={columns} dataSource={tableData} />
      </div>
    )) ||
    null
  );
};

export default List;

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

  1. 先创建工作簿和 sheet 页,这两行是固定代码。如果需要多 sheet,则创建多个 sheet 即可。后续对表格的所有操作,都是对 worksheet 的操作。
  2. 设置表格的默认行高。这步非必要,但是设置了更美观。否则会出现有内容的行跟没有内容的行行高不一致的情况。
  3. 设置列数据(表头)和每行的数据。
  4. 导出 excel。

解析 AntD Table 的 columnsdataSource

// 根据 antd 的 column 生成 exceljs 的 column
const DEFAULT_COLUMN_WIDTH = 20;
function generateHeaders(columns: any[]) {
  return columns?.map((col) => {
    const obj = {
      // 显示的 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() 逐行添加数据。

下载 excel

下载是使用 file-saver 库。

function saveWorkbook(workbook: any, fileName: string) {
  // 导出文件
  workbook.xlsx.writeBuffer().then((data) => {
    const blob = new Blob([data], { type: '' });
    saveAs(blob, fileName);
  });
}
设置大纲级别

Excel 支持大纲;行或列可以根据用户希望查看的详细程度展开或折叠。

大纲级别可以在列设置中定义:

worksheet.columns = [
  { header: 'Id', key: 'id', width: 10 },
  { header: 'Name', key: 'name', width: 32 },
  { header: 'D.O.B.', key: 'DOB', width: 10, outlineLevel: 1 }
];

或直接在行或列上设置:

worksheet.getColumn(3).outlineLevel = 1; // 设置列
worksheet.getRow(3).outlineLevel = 1; // 设置行

也可以在工作表上设置:

// 设置列大纲级别
worksheet.properties.outlineLevelCol = 1;

// 设置行大纲级别
worksheet.properties.outlineLevelRow = 1;

注意:调整行或列上的大纲级别或工作表上的大纲级别将产生副作用,即还修改受属性更改影响的所有行或列的折叠属性。 例如:

worksheet.properties.outlineLevelCol = 1;

worksheet.getColumn(3).outlineLevel = 1;
expect(worksheet.getColumn(3).collapsed).to.be.true;

worksheet.properties.outlineLevelCol = 2;
expect(worksheet.getColumn(3).collapsed).to.be.false;

未经允许不得转载:w3h5-Web前端开发资源网 » ExcelJS导出Ant Design Table数据为Excel文件

继续阅读

更多来自我们博客的帖子

如何安装 BuddyPress
由 测试 December 17, 2023
经过差不多一年的开发,BuddyPress 这个基于 WordPress Mu 的 SNS 插件正式版终于发布了。BuddyPress...
阅读更多
Filter如何工作
由 测试 December 17, 2023
在 web.xml...
阅读更多
如何理解CGAffineTransform
由 测试 December 17, 2023
CGAffineTransform A structure for holding an affine transformation matrix. ...
阅读更多