使用ExcelJS实现excel的前端导入导出功能(Vue3+TS)

公司项目需要导出表格为报表,尝试了xlsx、sheetjs以及js-xlsx各种库,但是总有不满足需求的点(比如设置行高列宽、居中、合并单元格、添加背景色等等)或者API太少写起来太繁琐,最后找到ExcelJS能够实现所有需求。

ExcelJS :读取,操作并写入电子表格数据和样式到 XLSX 和 JSON 文件。一个 Excel 电子表格文件逆向工程项目。
github中文文档: https://github.com/exceljs/exceljs/blob/master/README_zh.md

我把用到的exceljs的导入导出功能按照需求封装成一个类,具体可以根据自己的项目需求去调整。

一、定义Excel类

export class Excel {
  blob?: Blob;                      // 导入的blob文件
  worksheet?: ExcelJS.Worksheet;  // 当前工作表
  header: string[];             // 表头字段数组
  constructor(blob?: Blob) {
    this.blob = blob;
    this.worksheet = undefined;
    this.header = [];
   * @description: blob转ArrayBuffer(用于后续生成文件数据)
   * @return {Promise<ArrayBuffer>} ArrayBuffer
  private readFile(): Promise<ArrayBuffer> {
    return new Promise<ArrayBuffer>((resolve, reject) => {
      let reader = new FileReader();
      if (!this.blob) {
        reject('上传文件异常!');
      } else {
        reader.readAsArrayBuffer(this.blob);
        reader.onload = (ev) => {
          resolve(ev.target!.result as ArrayBuffer);
   * @description: 导入excel文件获取workbook(workbook属性方法参考exceljs文档)
   * @return {Promise<ExcelJS.Workbook>} 
  public async getWorkBook(): Promise<ExcelJS.Workbook> {
    let buffer = await this.readFile();
    const workbook = new ExcelJS.Workbook();
    return await workbook.xlsx.load(buffer);
   * @description: 将 excel 第i张工作表的数据转为对象数据
   * @param {number} i 工作表序号
   * @param {string[]} header 表头字段数组
   * @return {Promise<Record<(typeof header)[number], string>[]>} 
   * 传入表头作为字段的对象数组(每个元素对象对应每一行)
  public async importExcel(options: importExcelType): Promise<Record<(typeof header)[number], string>[]> {
    const { i = 1, header } = options;
    const workbook = await this.getWorkBook();
    const worksheet = workbook.getWorksheet(i);
    // excel导入后返回的数组
    const excelList: Record<(typeof header)[number], string>[] = [];
    worksheet.getSheetValues().filter(temp => !!temp?.length).forEach(item => {   
      // 移除空行
      // 移除每行首个空元素
      (item as string[]).shift();
      // 定义临时对象存储每一行内容
      let tempObj: Record<(typeof header)[number], string> = {};
      (item as string[]).forEach((item2, index2) => {
        tempObj[header[index2]] = item2;
      excelList.push(tempObj);
    return excelList
   * @param {*} password 冻结表格密码
   * @param {*} mergeList 合并单元格数组
   * @param {*} titleStyle 标题样式(按需补充方法)
   * @param {*} subTitleStyle 小标题样式(按需补充方法)
   * @param {*} headerStyle 表头字段样式(按需补充方法)
   * @param {*} cellStyle 单元格样式(按需补充方法)
   * @return {*}
  public async exportExcel(options: exportExcelType): Promise<void> {
    const { 
      data, 
      name, 
      header, 
      customHeader, 
      sheetName = 'sheet1', 
      title = '', 
      subTitle = '', 
      password = '', 
      mergeList = [],
      titleStyle,
      subTitleStyle, 
      headerStyle, 
      cellStyle 
  } = options;
    // 创建工作簿
    const workbook = new ExcelJS.Workbook();
    workbook.creator = '侃侃';
    workbook.created = new Date();
    // 添加sheet
    this.worksheet = workbook.addWorksheet(sheetName, { properties: { tabColor: { argb: 'FF00FF00' } } });
    this.header = header;
    // 表头行序号
    let headerRowId = 1;
    if (!!title) headerRowId++;
    if (!!subTitle) headerRowId++;
    // 插入单元格数据
    this.setCells(data, customHeader, cellStyle);
    // 插入大标题
    this.getTitle(title, titleStyle);
    // 插入小标题
    this.getSubTitle(subTitle, subTitleStyle);
    // 处理表头
    this.setHeaderStyle(headerRowId, data, headerStyle);
    // 更多处理
    this.handleDealExcel(password, mergeList, headerRowId);
    // 导出excel(此处也可用file-saver将blob导出到excel文件  fs.saveAs(blob, name+'.xlsx');)
    workbook.xlsx.writeBuffer().then((data) => {
      let blob = new Blob([data], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
      const a = document.createElement('a');
      a.href = URL.createObjectURL(blob);
      a.download = name + '.xlsx';
      document.body.appendChild(a);
      a.click();
      document.body.removeChild(a);
      window.URL.revokeObjectURL(a.href);

四、导出数据及格式方法汇总(按需补充)

1.插入单元格数据

* @description:单元格数据处理 * @param {any} data 表格数据 * @param {string} customHeader 表头中文字段 * @return {*} private setCells(data: exportExcelType['data'], customHeader: string[], style?: Partial<ExcelJS.Row & ExcelJS.Column>): void { // 设置列,插入中文表头 const column: Partial<ExcelJS.Column>[] = []; this.header.forEach((item, index) => { column.push({ header: customHeader[index], key: item, width: style?.width || 25, this.worksheet!.columns = column; // 设置行,添加数据 this.worksheet?.addRows(data); // 设置行高 this.worksheet?.eachRow({ includeEmpty: true }, (row, rowNumber) => { row.height = style?.height || 20; // 获取每一列数据,再依次对齐 this.worksheet!.columns.forEach(column => column.alignment = style?.alignment || { vertical: 'middle', horizontal: 'center', wrapText: true })

2.添加大标题

* @description: 添加大标题 * @param {string} title 标题 * @param {any} style 标题样式 * @return {*} private getTitle(title: string, style?: Partial<ExcelJS.Row>): void { if (!!title) { // 插入标题 this.worksheet?.spliceRows(1, 0, [title]); this.worksheet?.mergeCells(1, 1, 1, this.header.length); // 调整标题样式 const titleRow = this.worksheet!.getRow(1); // 高度 titleRow.height = style?.height || 40; // 字体设置 titleRow.font = style?.font || { size: 20, bold: true // 背景色 titleRow.fill = style?.fill || { bgColor: { 'argb': 'FFFFFF00' type: 'pattern', pattern: 'none' // 对齐方式 titleRow.alignment = style?.alignment || { horizontal: "center", vertical: "middle"

3.添加小标题

* @description: 添加小标题 * @param {string} subTitle 标题 * @param {Partial} style 小标题样式 * @return {*} private getSubTitle(subTitle: string, style?: Partial<ExcelJS.Row>): void { if (!!subTitle) { this.worksheet?.spliceRows(2, 0, [subTitle]); this.worksheet?.mergeCells(2, 1, 2, this.header.length); // 调整标题样式 const subtitleRow = this.worksheet!.getRow(2); // 高度 subtitleRow.height = style?.height || 20; // 字体设置 subtitleRow.font = style?.font || { size: 14, // 背景色 subtitleRow.fill = style?.fill || { bgColor: { 'argb': 'FFFFFF00' type: 'pattern', pattern: 'none' // 对齐方式 subtitleRow.alignment = style?.alignment || { horizontal: "right", vertical: "middle"

4.设置表头样式

* @description: 设置表头样式 * @param {number} num 表头在第几行 * @param {number} data 总数据 * @param {number} style 表头样式 * @return {*} private setHeaderStyle(num: number, data: any, style?: Partial<ExcelJS.Row>): void { // 自动筛选器 this.worksheet!.autoFilter = { from: { row: num, column: 1 to: { row: data.length, column: this.header.length // 给表头添加背景色 let headerRow = this.worksheet!.getRow(num); headerRow!.height = style?.height || 30; // 通过 cell 设置背景色,更精准 headerRow?.eachCell((cell) => { cell.fill = style?.fill || { type: 'pattern', pattern: 'solid', fgColor: { argb: 'dde0e7' }, cell.font = style?.font || { size: 12 * @description: 其他内容处理 * @param {string} password 密码 * @param {mergeListType} mergeList 合并行列数组 * @param {number} headerRowId 表头行序号 * @return {*} private handleDealExcel(password: string, mergeList: mergeListType[], headerRowId: number) { // 添加工作表保护 if (!!password) { this.worksheet?.protect(password, { autoFilter: true, selectLockedCells: false, // 合并单元格 mergeList.forEach(item => { // 行数为表格数据所在行行数+表头行序号headerRowId const startRow = item.startRow + headerRowId; const endRow = item.endRow + headerRowId; this.worksheet?.mergeCells(startRow, item.startColumn, endRow, item.endColumn); // 冻结前几行 this.worksheet!.views = [ { state: 'frozen', xSplit: 0, ySplit: headerRowId }

6.合并单元格结构处理方法

* @description: 合并单元格数组 * @param {Partial} merge1 需要合并的行/列起止对象数组1 * 例:[{ startRow: 1, endRow: 1},{ startRow: 10, endRow: 10}] * @param {Partial} merge2 需要合并的列/行起止对象数组2 * 例:[{ startColumn: 1, endColumn: 2 }] * @return {*} mergeArr 合并后数组 * 例:[{ startRow: 1, endRow: 1, startColumn: 1, endColumn: 2},{ startRow: 10, endRow: 10, startColumn: 1, endColumn: 2}] public merge(merge1: Partial<mergeListType>[], merge2: Partial<mergeListType>[]): mergeListType[] { const mergeArr: any[] = []; merge1.forEach(item1 => { mergeArr.push(...merge2.map(item2 => { return { ...item2, ...item1 }; return mergeArr; <i class="el-icon-upload"></i> <div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div> </el-upload> </template> <script setup lang="ts"> import { Excel } from '@/utils/excel'; import { ElMessage } from 'element-plus'; import { ref } from 'vue'; // 上传组件ref const uploadExcelRef = ref(); const uploadChange = async (file: any) => { // 表头字段数组 const header = ['id', 'name', 'age']; if (file.raw?.type !== 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' && file.raw?.type !== 'application/vnd.ms-excel') { ElMessage.error('文件格式错误,请重新上传!') // 移除上传的文件 uploadExcelRef.value!.handleRemove(file); return false // 对excel文件进行处理 try { const excel = new Excel(file.raw); // 导入文件获取数据 const res = await excel.importExcel({ header }); console.log(res); } catch (err) { console.log(err); </script>

excel文件

{ id: "id", name: "姓名", age: "年龄" }, { id: 1, name: "张三", age: 18 }, { id: 2, name: "李四", age: 20 }, { id: 3, name: "王五", age: 24 }, { id: 4, name: "赵六", age: 26 }, { id: 5, name: "刘七", age: 28 }

2.导出excel

// export.vue
<template>
  <div class="export">
    <button @click="exportExcel">导出</button>
</template>
<script setup lang="ts">
import { Excel, type mergeListType } from '@/utils/excel';
import { reactive } from 'vue';
const tableData = reactive([
  { id: 1, name: "张三", age: 18 },
  { id: 2, name: "张三", age: 18 },
  { id: 3, name: "王五", age: 24 },
  { id: 4, name: "赵六", age: 24 },
  { id: 5, name: "刘七", age: 24 }
const header = ['id', 'name', 'age'];
const customHeader = ['id', '姓名', '年龄'];
const mergeList: mergeListType[] = reactive([
    { startRow: 1, endRow: 2, startColumn: 3, endColumn: 3 }, 
    { startRow: 3, endRow: 5, startColumn: 3, endColumn: 3 }
const exportExcel = () => {
  const excel = new Excel();
  excel.exportExcel({
    name: '个人信息',
    title: '标题',
    data: tableData,
    header,
    customHeader,
    mergeList
</script>

最终生成的excel:

blob?: Blob; // 导入的blob文件 worksheet?: ExcelJS.Worksheet; // 当前工作表 header: string[]; // 表头字段数组 constructor(blob?: Blob) { this.blob = blob; this.worksheet = undefined; this.header = []; * @description: blob转ArrayBuffer(用于后续生成文件数据) * @return {Promise<ArrayBuffer>} ArrayBuffer private readFile(): Promise<ArrayBuffer> { return new Promise<ArrayBuffer>((resolve, reject) => { let reader = new FileReader(); if (!this.blob) { reject('上传文件异常!'); } else { reader.readAsArrayBuffer(this.blob); reader.onload = (ev) => { resolve(ev.target!.result as ArrayBuffer); * @description: 导入excel文件获取workbook(workbook属性方法参考exceljs文档) * @return {Promise<ExcelJS.Workbook>} public async getWorkBook(): Promise<ExcelJS.Workbook> { let buffer = await this.readFile(); const workbook = new ExcelJS.Workbook(); return await workbook.xlsx.load(buffer); * @description: 获取第i张工作表 * @param {number} i 工作表序号 * @return {Promise<ExcelJS.Worksheet>} 返回第 i 个工作表 public async getWorkSheet(i: number = 0): Promise<ExcelJS.Worksheet> { const workbook = await this.getWorkBook(); return workbook.getWorksheet(i); * @description: 将 excel 第i张工作表的数据转为对象数据 * @param {number} i 工作表序号 * @param {string[]} header 表头字段数组 * @return {Promise<Record<(typeof header)[number], string>[]>} 传入表头作为字段的对象数组(每个元素对象对应每一行) public async importExcel(options: importExcelType): Promise<Record<(typeof header)[number], string>[]> { const { i = 1, header } = options; const workbook = await this.getWorkBook(); const worksheet = workbook.getWorksheet(i); // excel导入后返回的数组 const excelList: Record<(typeof header)[number], string>[] = []; worksheet.getSheetValues().filter(temp => !!temp?.length).forEach(item => { // 移除空行 // 移除每行首个空元素 (item as string[]).shift(); // 定义临时对象存储每一行内容 let tempObj: Record<(typeof header)[number], string> = {}; (item as string[]).forEach((item2, index2) => { tempObj[header[index2]] = item2; excelList.push(tempObj); return excelList * @description: 导出excel,参数信息参考exceljs * @param {*} data 数据 * @param {*} name 文件名 * @param {*} header 表头字段 * @param {*} customHeader 表头字段对应中文 * @param {*} sheetName 工作表名 * @param {*} title 标题 * @param {*} subTitle 副标题(日期) * @param {*} password 冻结表格密码 * @param {*} mergeList 合并单元格数组 * @param {*} titleStyle 标题样式(按需补充方法) * @param {*} subTitleStyle 小标题样式(按需补充方法) * @param {*} headerStyle 表头字段样式(按需补充方法) * @param {*} cellStyle 单元格样式(按需补充方法) * @return {*} public async exportExcel(options: exportExcelType): Promise<void> { const { data, name, header, customHeader, sheetName = 'sheet1', title = '', subTitle = '', password = '', mergeList = [], titleStyle, subTitleStyle, headerStyle, cellStyle } = options; // 创建工作簿 const workbook = new ExcelJS.Workbook(); workbook.creator = '侃侃'; workbook.created = new Date(); // 添加sheet this.worksheet = workbook.addWorksheet(sheetName, { properties: { tabColor: { argb: 'FF00FF00' } } }); this.header = header; // 表头行序号 let headerRowId = 1; if (!!title) headerRowId++; if (!!subTitle) headerRowId++; // 插入单元格数据 this.setCells(data, customHeader, cellStyle); // 插入大标题 this.getTitle(title, titleStyle); // 插入小标题 this.getSubTitle(subTitle, subTitleStyle); // 处理表头 this.setHeaderStyle(headerRowId, data, headerStyle); // 更多处理 this.handleDealExcel(password, mergeList, headerRowId); // 导出excel(此处也可用file-saver将blob导出到excel文件 fs.saveAs(blob, name+'.xlsx');) workbook.xlsx.writeBuffer().then((data) => { let blob = new Blob([data], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' }); const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = name + '.xlsx'; document.body.appendChild(a); a.click(); document.body.removeChild(a); window.URL.revokeObjectURL(a.href); * @description: 合并单元格数组 * @param {Partial} merge1 需要合并的行/列起止对象数组1 * 例:[{ startRow: 1, endRow: 1},{ startRow: 10, endRow: 10}] * @param {Partial} merge2 需要合并的列/行起止对象数组2 * 例:[{ startColumn: 1, endColumn: 2 }] * @return {*} mergeArr 合并后数组 * 例:[{ startRow: 1, endRow: 1, startColumn: 1, endColumn: 2},{ startRow: 10, endRow: 10, startColumn: 1, endColumn: 2}] public merge(merge1: Partial<mergeListType>[], merge2: Partial<mergeListType>[]): mergeListType[] { const mergeArr: any[] = []; merge1.forEach(item1 => { mergeArr.push(...merge2.map(item2 => { return { ...item2, ...item1 }; return mergeArr; * @description:单元格数据处理 * @param {any} data 表格数据 * @param {string} customHeader 表头中文字段 * @return {*} private setCells(data: exportExcelType['data'], customHeader: string[], style?: Partial<ExcelJS.Row & ExcelJS.Column>): void { // 设置列,插入中文表头 const column: Partial<ExcelJS.Column>[] = []; this.header.forEach((item, index) => { column.push({ header: customHeader[index], key: item, width: style?.width || 25, this.worksheet!.columns = column; // 设置行,添加数据 this.worksheet?.addRows(data); // 设置行高 this.worksheet?.eachRow({ includeEmpty: true }, (row, rowNumber) => { row.height = style?.height || 20; // 获取每一列数据,再依次对齐 this.worksheet!.columns.forEach(column => { column.alignment = style?.alignment || { vertical: 'middle', horizontal: 'center', wrapText: true } * @description: 添加大标题 * @param {string} title 标题 * @param {any} style 标题样式 * @return {*} private getTitle(title: string, style?: Partial<ExcelJS.Row>): void { if (!!title) { // 插入标题 this.worksheet?.spliceRows(1, 0, [title]); this.worksheet?.mergeCells(1, 1, 1, this.header.length); // 调整标题样式 const titleRow = this.worksheet!.getRow(1); // 高度 titleRow.height = style?.height || 40; // 字体设置 titleRow.font = style?.font || { size: 20, bold: true // 背景色 titleRow.fill = style?.fill || { bgColor: { 'argb': 'FFFFFF00' type: 'pattern', pattern: 'none' // 对齐方式 titleRow.alignment = style?.alignment || { horizontal: "center", vertical: "middle" * @description: 添加小标题 * @param {string} subTitle 标题 * @param {Partial} style 小标题样式 * @return {*} private getSubTitle(subTitle: string, style?: Partial<ExcelJS.Row>): void { if (!!subTitle) { this.worksheet?.spliceRows(2, 0, [subTitle]); this.worksheet?.mergeCells(2, 1, 2, this.header.length); // 调整标题样式 const subtitleRow = this.worksheet!.getRow(2); // 高度 subtitleRow.height = style?.height || 20; // 字体设置 subtitleRow.font = style?.font || { size: 14, // 背景色 subtitleRow.fill = style?.fill || { bgColor: { 'argb': 'FFFFFF00' type: 'pattern', pattern: 'none' // 对齐方式 subtitleRow.alignment = style?.alignment || { horizontal: "right", vertical: "middle" * @description: 设置表头样式 * @param {number} num 表头在第几行 * @param {number} data 总数据 * @param {number} style 表头样式 * @return {*} private setHeaderStyle(num: number, data: any, style?: Partial<ExcelJS.Row>): void { // 自动筛选器 this.worksheet!.autoFilter = { from: { row: num, column: 1 to: { row: data.length, column: this.header.length // 给表头添加背景色 let headerRow = this.worksheet!.getRow(num); headerRow!.height = style?.height || 30; // 通过 cell 设置背景色,更精准 headerRow?.eachCell((cell) => { cell.fill = style?.fill || { type: 'pattern', pattern: 'solid', fgColor: { argb: 'dde0e7' }, cell.font = style?.font || { size: 12 * @description: 其他内容处理 * @param {string} password 密码 * @param {mergeListType} mergeList 合并行列数组 * @param {number} headerRowId 表头行序号 * @return {*} private handleDealExcel(password: string, mergeList: mergeListType[], headerRowId: number) { // 添加工作表保护 if (!!password) { this.worksheet?.protect(password, { autoFilter: true, selectLockedCells: false, // 合并单元格 mergeList.forEach(item => { // 行数为表格数据所在行行数+表头行序号headerRowId const startRow = item.startRow + headerRowId; const endRow = item.endRow + headerRowId; this.worksheet?.mergeCells(startRow, item.startColumn, endRow, item.endColumn); // 冻结前几行 this.worksheet!.views = [ { state: 'frozen', xSplit: 0, ySplit: headerRowId }