js batch export excel as zip archive

This article mainly introduces the use exceljs, file-saver, and jszipimplementation of downloading a zip archive that contains multi-level folders, multiple excels, and each excel supports multiple sheets.
Previous article: Exporting complex front-end tables to excel, and exporting Antd Table with one click. This article is enough (with source code) to introduce in detail how to parse Antd Table, assemble data, and adjust the style of the table. If you are interested, you can take a look first. .
This article will continue the previous one, focusing on higher-level abstractions of methods, and downloading zip archives of multi-level folders.
Source address: github.com/cachecats/e…

achieve effect

The final download is 压缩包.zipthat after decompression, it contains multiple folders, and each folder can have infinitely nested subfolders. The excel file can be freely placed in the root directory or subfolder.
The effect is as shown in the figure:
image.png

Instructions

The usage method is also very simple. After highly encapsulation, you only need to pass in the parameters according to the rules of method parameters:

downloadFiles2ZipWithFolder({
      zipName: '压缩包',
      folders: [
        {
          folderName: '文件夹1',
          files: [
            {
              filename: 'test',
              sheets: [{
                sheetName: 'test',
                columns: columns,
                dataSource: list
              }]
            },
            {
              filename: 'test2',
              sheets: [{
                sheetName: 'test',
                columns: columns,
                dataSource: list
              }]
            },
          ]
        },
        {
          folderName: '文件夹2',
          files: [
            {
              filename: 'test',
              sheets: [{
                sheetName: 'test',
                columns: columns,
                dataSource: list
              }]
            },
            {
              filename: 'test2',
              sheets: [{
                sheetName: 'test',
                columns: columns,
                dataSource: list
              }]
            },
          ]
        },
        {
          folderName: '文件夹2/文件夹2-1',
          files: [
            {
              filename: 'test',
              sheets: [{
                sheetName: 'test',
                columns: columns,
                dataSource: list
              }]
            },
            {
              filename: 'test2',
              sheets: [{
                sheetName: 'test',
                columns: columns,
                dataSource: list
              }]
            },
          ]
        },
        {
          folderName: '文件夹2/文件夹2-1/文件夹2-1-1',
          files: [
            {
              filename: 'test',
              sheets: [{
                sheetName: 'test',
                columns: columns,
                dataSource: list
              }]
            },
            {
              filename: 'test2',
              sheets: [{
                sheetName: 'test',
                columns: columns,
                dataSource: list
              }]
            },
          ]
        },
        {
          folderName: '',
          files: [
            {
              filename: 'test',
              sheets: [{
                sheetName: 'test',
                columns: columns,
                dataSource: list
              },
                {
                  sheetName: 'test2',
                  columns: columns,
                  dataSource: list
                }
              ]
            },
            {
              filename: 'test2',
              sheets: [{
                sheetName: 'test',
                columns: columns,
                dataSource: list
              }]
            },
          ]
        }
      ]
    })
复制代码

Three methods will be encapsulated here to meet the export requirements in different scenarios:

  • downloadExcel: Export a common single-file excel, with preset styles, which can contain multiple sheets.
  • downloadFiles2Zip: Export multiple excel files into a zip archive without nested folders.
  • downloadFiles2ZipWithFolder: Export a zip archive containing multiple levels of subfolders, each level containing multiple excel files.

1. Encapsulate the common download and export excel method

Let's encapsulate a common, pre-defined style export method that can be used directly out of the box. Users don't need to care about the specific details, just simply call:

function onExportExcel() {
  downloadExcel({
    filename: 'test',
    sheets: [{
      sheetName: 'test',
      columns: columns,
      dataSource: list
    }]
  })
}
复制代码

As above, directly call the downloadExcelmethod , which passes in an object as a parameter, with twofilename properties and respectively.sheets

  • filename: filename. Without the .xlsxsuffix , the suffix name will be added automatically.
  • sheets:sheet 数组。传入几个 sheet 对象就会创建几个 sheet 页。

Sheet对象的定义:

export interface ISheet {
  // sheet 的名字
  sheetName: string;
  // 这个 sheet 中表格的 column,类型同 antd 的 column
  columns: ColumnType<any>[];
  // 表格的数据
  dataSource: any[];
}
复制代码

核心代码

downloadExcel方法关键源码:

export interface IDownloadExcel {
  filename: string;
  sheets: ISheet[];
}

export interface ISheet {
  // sheet 的名字
  sheetName: string;
  // 这个 sheet 中表格的 column,类型同 antd 的 column
  columns: ColumnType<any>[];
  // 表格的数据
  dataSource: any[];
}

/**
 * 下载导出简单的表格
 * @param params
 */
export function downloadExcel(params: IDownloadExcel) {
  // 创建工作簿
  const workbook = new ExcelJs.Workbook();
  params?.sheets?.forEach((sheet) => handleEachSheet(workbook, sheet));
  saveWorkbook(workbook, `${params.filename}.xlsx`);
}


function handleEachSheet(workbook: Workbook, sheet: ISheet) {
  // 添加sheet
  const worksheet = workbook.addWorksheet(sheet.sheetName);
  // 设置 sheet 的默认行高。设置默认行高跟自动撑开单元格冲突
  // worksheet.properties.defaultRowHeight = 20;
  // 设置列
  worksheet.columns = generateHeaders(sheet.columns);
  handleHeader(worksheet);
  handleData(worksheet, sheet);
}


export function saveWorkbook(workbook: Workbook, fileName: string) {
  // 导出文件
  workbook.xlsx.writeBuffer().then((data: any) => {
    const blob = new Blob([data], {type: ''});
    saveAs(blob, fileName);
  });
}
复制代码

generateHeaders方法是设置表格的列。
handleHeader方法负责处理表头,设置表头的高度、背景色、字体等样式。
handleData方法处理每一行具体的数据。
这三个方法的实现在上篇文章都有介绍,如需了解更多请查看源码:github.com/cachecats/e…

导出的 excel 效果如下图,列宽会根据传入的 width 动态计算,单元格高度会根据内容自动撑开。
image.png

二、导出包含多个 excel 的 zip 压缩包

如果没有多级目录的需求,只想把多个 excel 文件打包到一个压缩包里,可以用 downloadFiles2Zip这个方法,得到的目录结构如下图:
image.png
参数结构如下,支持导出多个 excel 文件,每个 excel 文件又可以包含多个 sheet。

export interface IDownloadFiles2Zip {
  // 压缩包的文件名
  zipName: string;
  files: IDownloadExcel[];
}

export interface IDownloadExcel {
  filename: string;
  sheets: ISheet[];
}

export interface ISheet {
  // sheet 的名字
  sheetName: string;
  // 这个 sheet 中表格的 column,类型同 antd 的 column
  columns: ColumnType<any>[];
  // 表格的数据
  dataSource: any[];
}
复制代码

使用示例

function onExportZip() {
  downloadFiles2Zip({
    zipName: '压缩包',
    files: [
      {
        filename: 'test',
        sheets: [
          {
            sheetName: 'test',
            columns: columns,
            dataSource: list
          },
          {
            sheetName: 'test2',
            columns: columns,
            dataSource: list
          }
        ]
      },
      {
        filename: 'test2',
        sheets: [{
          sheetName: 'test',
          columns: columns,
          dataSource: list
        }]
      },
      {
        filename: 'test3',
        sheets: [{
          sheetName: 'test',
          columns: columns,
          dataSource: list
        }]
      }
    ]
  })
}
复制代码

核心代码

通过 handleEachFile()方法处理每个 fille 对象,每个 file 其实就是一个 excel 文件,即一个 workbook。给每个 excel 创建 workbook并将数据写入,然后通过 JsZip库写入到压缩文件内,最终用 file-saver库提供的 saveAs方法导出压缩文件。
注意 12、13行,handleEachFile()方法返回的是一个 Promise,需要等所有异步方法都执行完之后再执行下面的生成 zip 方法,否则可能会遗漏文件。

import {saveAs} from 'file-saver';
import * as ExcelJs from 'exceljs';
import {Workbook, Worksheet, Row} from 'exceljs';
import JsZip from 'jszip'

/**
 * 导出多个文件为zip压缩包
 */
export async function downloadFiles2Zip(params: IDownloadFiles2Zip) {
  const zip = new JsZip();
  // 待每个文件都写入完之后再生成 zip 文件
  const promises = params?.files?.map(async param => await handleEachFile(param, zip, ''))
  await Promise.all(promises);
  zip.generateAsync({type: "blob"}).then(blob => {
    saveAs(blob, `${params.zipName}.zip`)
  })
}

async function handleEachFile(param: IDownloadExcel, zip: JsZip, folderName: string) {
  // 创建工作簿
  const workbook = new ExcelJs.Workbook();
  param?.sheets?.forEach((sheet) => handleEachSheet(workbook, sheet));
  // 生成 blob
  const data = await workbook.xlsx.writeBuffer();
  const blob = new Blob([data], {type: ''});
  if (folderName) {
    zip.folder(folderName)?.file(`${param.filename}.xlsx`, blob)
  } else {
    // 写入 zip 中一个文件
    zip.file(`${param.filename}.xlsx`, blob);
  }
}

function handleEachSheet(workbook: Workbook, sheet: ISheet) {
  // 添加sheet
  const worksheet = workbook.addWorksheet(sheet.sheetName);
  // 设置 sheet 的默认行高。设置默认行高跟自动撑开单元格冲突
  // worksheet.properties.defaultRowHeight = 20;
  // 设置列
  worksheet.columns = generateHeaders(sheet.columns);
  handleHeader(worksheet);
  handleDataWithRender(worksheet, sheet);
}
复制代码

render 渲染的单元格处理

数据处理还有一点需要注意,因为有的单元格是通过 render 函数渲染的,render 函数里可能进行了一系列复杂的计算,所以如果 column 中有 render 的话不能直接以 dataIndex 为 key 进行取值,要拿到 render 函数执行后的值才是正确的。
比如 Table 的 columns 如下:

const columns: ColumnsType<any> = [
    {
      width: 50,
      dataIndex: 'id',
      key: 'id',
      title: 'ID',
      render: (text, row) => <div><p>{row.id + 20}</p></div>,
    },
    {
      width: 100,
      dataIndex: 'name',
      key: 'name',
      title: '姓名',
    },
    {
      width: 50,
      dataIndex: 'age',
      key: 'age',
      title: '年龄',
    },
    {
      width: 80,
      dataIndex: 'gender',
      key: 'gender',
      title: '性别',
    },
  ];
复制代码

第一列传入了 render 函数 render: (text, row) => <div><p>{row.id + 20}</p></div>,经过计算后,ID 列显示的值应该是原来的 id + 20。
构造的数据原来的 id 是 0-4,页面上显示的应该是 20-24,如下图:
image.png
这时导出的 excel 应该跟页面上显示的一模一样,这样才是正确的。
点击【导出zip】按钮,解压后打开下载的其中一个 excel,验证显示的内容跟在线表格完全一致。
image.png
那么是如何做到的呢?
主要看 handleDataWithRender()方法:

/**
 * 如果 column 有 render 函数,则以 render 渲染的结果显示
 * @param worksheet
 * @param sheet
 */
function handleDataWithRender(worksheet: Worksheet, sheet: ISheet) {
  const {dataSource, columns} = sheet;
  const rowsData = dataSource?.map(data => {
    return columns?.map(column => {
      // @ts-ignore
      const renderResult = column?.render?.(data[column.dataIndex], data);
      if (renderResult) {
        // 如果不是 object 说明没包裹标签,是基本类型直接返回
        if (typeof renderResult !== "object") {
          return renderResult;
        }
        // 如果是 object 说明包裹了标签,逐级取出值
        return getValueFromRender(renderResult);
      }
      // @ts-ignore
      return data[column.dataIndex];
    })
  })
  // 添加行
  const rows = worksheet.addRows(rowsData);
  // 设置每行的样式
  addStyleToData(rows);
}


// 递归取出 render 里的值
// @ts-ignore
function getValueFromRender(renderResult: any) {
  if (renderResult?.type) {
    let children = renderResult?.props?.children;
    if (children?.type) {
      return getValueFromRender(children);
    } else {
      return children;
    }
  }
  return ''
}
复制代码

worksheet.addRows()可以添加数据对象,也可以添加由每行的每列组成的二维数组。由于我们要自己控制每个单元格显示的内容,所以采用第二种方式,传入一个二维数组来构造 row。
结构如下图所示:
image.png
循环 dataSourcecolumns,就得到了每个单元格要显示的内容,通过执行 render 函数,得到 render 执行后的结果:
const renderResult = column?.render?.(data[column.dataIndex], data);
注意 render 需要传入两个参数,一个是 text,一个是这行的数据对象,我们都能确定参数的值,所以直接传入。
然后判断 renderResult的类型,如果是 object 类型,说明是个由 html 标签包裹的 ReactNode,需要递归取出最终渲染的值。如果是非 object 类型,说明是 boolean 或者 string 这样的基本类型,即没有被标签包裹,可以直接展示。
由于我们采用了递归来取最后渲染的值,所以无论嵌套了多少层标签,都可以正确的取到值。

三、导出包含多个子文件夹、多个excel文件的 zip 压缩包

如果文件、文件夹嵌套比较深,可以使用 downloadFiles2ZipWithFolder()方法。
文件结构如下图:
image.png

核心代码

export interface IDownloadFiles2ZipWithFolder {
  zipName: string;
  folders: IFolder[];
}

export interface IFolder {
  folderName: string;
  files: IDownloadExcel[];
}

export interface IDownloadExcel {
  filename: string;
  sheets: ISheet[];
}

export interface ISheet {
  // sheet 的名字
  sheetName: string;
  // 这个 sheet 中表格的 column,类型同 antd 的 column
  columns: ColumnType<any>[];
  // 表格的数据
  dataSource: any[];
}


/**
 * 导出支持多级文件夹的压缩包
 * @param params
 */
export async function downloadFiles2ZipWithFolder(params: IDownloadFiles2ZipWithFolder) {
  const zip = new JsZip();
  const outPromises = params?.folders?.map(async folder => await handleFolder(zip, folder))
  await Promise.all(outPromises);
  zip.generateAsync({type: "blob"}).then(blob => {
    saveAs(blob, `${params.zipName}.zip`)
  })
}

async function handleFolder(zip: JsZip, folder: IFolder) {
  console.log({folder})
  let folderPromises: Promise<any>[] = [];
  const promises = folder?.files?.map(async param => await handleEachFile(param, zip, folder.folderName));
  await Promise.all([...promises, ...folderPromises]);
}
复制代码

跟上一个方法 downloadFiles2Zip相比,参数的数据结构多了层 folders,其他的逻辑基本没变。
所以 downloadFiles2ZipWithFolder方法能实现downloadFiles2Zip方法的所有功能。

使用示例

如文章开头的使用示例,为了方便看清结构,将每个对象的 files 值删除,精简之后得到如下结构:

downloadFiles2ZipWithFolder({
      zipName: '压缩包',
      folders: [
        {
          folderName: '文件夹1',
          files: []
        },
        {
          folderName: '文件夹2',
          files: []
        },
        {
          folderName: '文件夹2/文件夹2-1',
          files: []
        },
        {
          folderName: '文件夹2/文件夹2-1/文件夹2-1-1',
          files: []
        },
        {
          folderName: '',
          files: []
        }
      ]
    })
复制代码

不管嵌套几层文件夹,folders永远是一个一维数组,每一项里面也不会嵌套 folders。多级目录是通过文件名 folderName实现的。

  • folderNameis an empty string, filesput the top-level directory of the archive, not in any subfiles.
  • folderNameis a common string, such as: 文件夹1, then create a new folder with folderNamethe file name, and put its filesin this folder.
  • folderNameIf it is a string with a slash, such as: 文件夹2/文件夹2-1/文件夹2-1-1, then create n folders in sequence and keep the nesting relationship, and finally put it under the fileslast folder.

To view the complete code of the demo, the source code address: github.com/cachecats/e…

Guess you like

Origin juejin.im/post/7080169896209809445