Skip to content

ExcelJS

针对Excel文件生成的封装,提供更友好的配置,减低上手难度。ExcelJS 官方中文文档

实现源码
ts
import ExcelJS from 'exceljs';

export interface IConfig {
  data: unknown[];
  /**
   * data里的子项为一个对象时,需要设置字段名,用于生成数据行
   */
  fields?: string[];
  /**
   * 表头
   * @example [['用户信息表'], ['姓名', '年龄', '性别']]
   */
  headers: string[][];
  /**
   * 单元格合并,row: 行,col: 列,rowspan: 合并的行数,colspan: 合并的列数
   */
  merges?: { row: number; col: number; rowspan: number; colspan: number }[];
  attrs?: {
    rowStart?: number;
    rowEnd?: number;
    colStart?: number;
    colEnd?: number;
    attr: Partial<ExcelJS.Style>;
  }[];
  views?: Partial<ExcelJS.WorksheetView>[];
  columnsWidth?: number[];
  protect?: { password: string; options: Partial<ExcelJS.WorksheetProtection> };
  sheetName?: string;
  sheetOptions?: Partial<ExcelJS.AddWorksheetOptions>;
}

/**
 * 根据配置生成excel文件
 * @param config
 * @returns 返回excel文件的二进制数据
 */
export const createExcelFile = async (config: IConfig[] | IConfig) => {
  if (!Array.isArray(config)) {
    config = [config];
  }
  if (config.length === 0) return;

  const workbook = new ExcelJS.Workbook();
  for (let idx = 0; idx < config.length; idx++) {
    const item = config[idx];
    const sheet = workbook.addWorksheet(item.sheetName || 'sheet' + (idx + 1));

    // 生成完整excel数据,包含表头和数据行。
    const results = item.data.map((obj: any) => {
      if (
        typeof obj === 'string' ||
        typeof obj === 'number' ||
        typeof obj === 'boolean' ||
        obj === null ||
        obj === undefined
      ) {
        return [obj];
      }

      return item?.fields?.map((key) => {
        return obj[key] ?? '';
      });
    });
    const excelData = [...item.headers, ...results];
    sheet.addRows(excelData);

    // 单元格合并处理
    item?.merges?.forEach((m) => {
      sheet.mergeCells(m.row, m.col, m.row + m.rowspan, m.col + m.colspan);
    });

    if (item.views) {
      sheet.views = item.views;
    }

    if (item.protect) {
      await sheet.protect(item.protect.password, item.protect.options);
    }

    // 单元格样式处理
    item?.attrs?.forEach((val) => {
      const attr = val.attr;
      const rowStart = val.rowStart;
      const rowEnd = val.rowEnd;
      const colStart = val.colStart;
      const colEnd = val.colEnd;

      if (rowStart !== undefined && rowEnd !== undefined) {
        for (let r = rowStart; r <= rowEnd; r++) {
          const row = sheet.getRow(r);

          if (colStart !== undefined && colEnd !== undefined) {
            for (let c = colStart; c <= colEnd; c++) {
              const cell = row.getCell(c);
              Object.keys(attr).forEach((key) => {
                // @ts-ignore
                cell[key] = attr[key];
              });
            }
            continue;
          }

          // 未设置列,整行设置【大纲级别】
          Object.keys(attr).forEach((key) => {
            // @ts-ignore
            row[key] = attr[key];
          });
        }
        return;
      }

      if (colStart !== undefined && colEnd !== undefined) {
        for (let c = colStart; c <= colEnd; c++) {
          // 获取当前列,整列设置【大纲级别】
          const column = sheet.getColumn(c);
          Object.keys(attr).forEach((key) => {
            // @ts-ignore
            column[key] = attr[key];
          });
        }
        return;
      }

      // 没有设置具体的行列,则为整表设置
      Object.keys(attr).forEach((key) => {
        // @ts-ignore
        sheet[key] = attr[key];
      });
    });

    item?.columnsWidth?.forEach((width, index) => {
      sheet.columns[index].width = width;
    });
  }

  return workbook.xlsx.writeBuffer();
};
downloadFile
ts
import FileSaver from 'file-saver';

export const downloadFile = (data: ArrayBuffer, fileName: string) => {
  const blob = new Blob([data], { type: 'application/octet-stream' });
  FileSaver.saveAs(blob, fileName);
};

安装

需要安装 exceljsfile-saver 依赖。

bash
pnpm add exceljs file-saver
pnpm add @types/file-saver -D

基本使用

1. 简单示例

simple
配置如下:

ts
import { type IConfig, createExcelFile } from '...';

const onDownloadSimple = () => {
  const config = {
    fields: ['name', 'age', 'phone', 'address'],
    // 第一行为表头,合并4个单元格
    merges: [{ row: 1, col: 1, rowspan: 0, colspan: 3 }],
    data: [
      { name: '张三', age: 18, phone: '12345678901', address: '北京市' },
      { name: '李四', age: 20, phone: '12345678901', address: '上海市' },
      { name: '王五', age: 22, phone: '12345678901', address: '广州市' },
      { name: '赵六', age: 24, phone: '12345678901', address: '深圳市' }
    ],
    headers: [['用户信息表'], ['姓名', '年龄', '电话', '地址']],
    sheetName: '用户信息表',
    columnsWidth: Array.from({ length: 4 }, () => 30)
  };

  createExcelFile(config).then((buffer) => {
    downloadFile(buffer!, '用户信息表.xlsx');
  });
};

2. 复杂示例

complex

配置如下:

ts
import { type IConfig, createExcelFile } from '...';

const onDownloadMulti = () => {
  const fields = ['name', 'age', 'phone', 'address', 'sex', 'birthday', 'email'];
  const config: IConfig = {
    fields,
    data: [
      { name: '张三', age: 18, phone: '12345678901', address: '北京市', sex: '男', birthday: new Date('2000/01/01'), email: '无' },
      { name: '李四', age: 20, phone: '12345678901', address: '上海市', sex: '男', birthday: new Date('2000/01/02'), email: '无' },
      { name: '王五', age: 22, phone: '12345678901', address: '广州市', sex: '男', birthday: new Date('2000/01/03'), email: '无' },
      { name: '赵六', age: 24, phone: '12345678901', address: '深圳市', sex: '男', birthday: new Date('2000/01/04'), email: '无' }
    ],
    attrs: [],
    headers: [
      ['用户信息表'],
      ['姓名', '其他信息'],
      ['', '年龄', '电话', '地址', '性别', '生日', '邮箱']
    ],
    merges: [
      // 显示标题 '用户信息表',合并第一行所有单元格
      { row: 1, col: 1, rowspan: 0, colspan: fields.length - 1 },
      // 显示表头 '姓名',合并第二行与第三行的第一个单元格
      { row: 2, col: 1, rowspan: 1, colspan: 0 },
      // 显示表头 '其他信息'
      { row: 2, col: 2, rowspan: 0, colspan: 5 }
    ],
    sheetName: '用户信息表-复杂表头',
    columnsWidth: Array.from({ length: fields.length }, () => 20)
  };

  config?.fields?.forEach((item, index) => {
    if (item === 'age') {
      // 设置整数格式
      config?.attrs?.push({
        rowStart: 4,
        rowEnd: config.data.length + 3, // 表头占用三行
        colStart: index + 1, // 索引从0开始,列从1开始,所以加1
        colEnd: index + 1,
        attr: {
          numFmt: '#,##0'
        }
      });
      return;
    }

    if (item === 'birthday') {
      // 设置日期格式
      config?.attrs?.push({
        rowStart: 4,
        rowEnd: config.data.length + 3,
        colStart: index + 1,
        colEnd: index + 1,
        attr: {
          numFmt: 'yyyy"年"mm"月"dd"日"'
        }
      });
      return;
    }
  });

  // 设置单元格样式
  config?.attrs?.push({
    rowStart: 1,
    rowEnd: config.data.length + 3,
    colStart: 1,
    colEnd: config?.fields?.length,
    attr: {
      alignment: { vertical: 'middle', horizontal: 'center' },
      border: {
        top: { style: 'thin' },
        left: { style: 'thin' },
        bottom: { style: 'thin' },
        right: { style: 'thin' }
      }
    }
  });

  // 设置表头标题样式
  config?.attrs?.push({
    rowStart: 1,
    rowEnd: 1,
    colStart: 1,
    colEnd: config?.fields?.length,
    attr: {
      fill: {
        type: 'pattern',
        pattern: 'solid',
        fgColor: { argb: 'e2efda' }
      },
      font: {
        size: 20,
        bold: true
      }
    }
  });

  // 设置表头填充颜色,字体加粗
  config?.attrs?.push({
    rowStart: 2,
    rowEnd: 3,
    colStart: 1,
    colEnd: config?.fields?.length,
    attr: {
      fill: {
        type: 'pattern',
        pattern: 'solid',
        fgColor: { argb: 'c6e0b4' }
      },
      font: {
        bold: true
      }
    }
  });

  createExcelFile(config).then((buffer) => {
    downloadFile(buffer!, '用户信息表-复杂表头.xlsx');
  });
};

Released under the MIT License.