js 일괄 내보내기 Excel을 zip 아카이브로

이 기사에서는 주로 다중 레벨 폴더, 다중 Excel 및 각 Excel이 다중 시트를 지원하는 zip 아카이브 다운로드의 사용 및 구현 exceljs을 소개합니다. 이전 기사: 복잡한 프론트 엔드 테이블을 Excel로 내보내기 및 원 클릭으로 Antd 테이블 내보내기 Antd 테이블 을 구문 분석하고 데이터를 조합하고 테이블 스타일을 조정하는 방법을 자세히 소개하기에 충분합니다(소스 코드 포함). 관심있으신 분들은 먼저 둘러보시면 됩니다. . 이 기사는 상위 수준의 메소드 추상화에 초점을 맞추고 다중 수준 폴더의 zip 아카이브를 다운로드하는 이전 기사를 계속할 것입니다. 소스 주소: github.com/cachecats/e…file-saverjszip


효과를 얻다

최종 다운로드는 压缩包.zip압축 해제 후 여러 폴더를 포함하며 각 폴더에는 무한히 중첩된 하위 폴더가 있을 수 있습니다.엑셀 파일은 루트 디렉터리 또는 하위 폴더에 자유롭게 배치할 수 있습니다.
효과는 그림과 같습니다.
이미지.png

지침

사용 방법도 매우 간단합니다. 고도로 캡슐화한 후에는 메서드 매개변수의 규칙에 따라 매개변수를 전달하기만 하면 됩니다.

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
              }]
            },
          ]
        }
      ]
    })
复制代码

다양한 시나리오에서 내보내기 요구 사항을 충족하기 위해 세 가지 방법이 여기에 캡슐화됩니다.

  • downloadExcel: 여러 시트를 포함할 수 있는 미리 설정된 스타일을 사용하여 일반적인 단일 파일 Excel을 내보냅니다.
  • downloadFiles2Zip: 여러 엑셀 파일을 중첩 폴더 없이 zip 아카이브로 내보냅니다.
  • downloadFiles2ZipWithFolder: 여러 수준의 하위 폴더가 포함된 zip 아카이브를 내보냅니다. 각 수준에는 여러 Excel 파일이 포함되어 있습니다.

1. 일반적인 다운로드 및 Excel 내보내기 방법을 캡슐화합니다.

즉시 사용할 수 있는 미리 정의된 일반적인 스타일 내보내기 방법을 캡슐화해 보겠습니다. 사용자는 특정 세부 사항에 신경 쓸 필요 없이 다음을 호출하기만 하면 됩니다.

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

위와 같이 각각 2개의 속성을 가지고 객체를 매개변수로 전달 하는 downloadExcel메소드 .filenamesheets

  • 파일명: 파일명. .xlsx접미사가 없으면 접미사 이름이 자동으로 추가됩니다.
  • 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 动态计算,单元格高度会根据内容自动撑开。
이미지.png

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

如果没有多级目录的需求,只想把多个 excel 文件打包到一个压缩包里,可以用 downloadFiles2Zip这个方法,得到的目录结构如下图:
이미지.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,如下图:
이미지.png
这时导出的 excel 应该跟页面上显示的一模一样,这样才是正确的。
点击【导出zip】按钮,解压后打开下载的其中一个 excel,验证显示的内容跟在线表格完全一致。
이미지.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。
结构如下图所示:
이미지.png
循环 dataSourcecolumns,就得到了每个单元格要显示的内容,通过执行 render 函数,得到 render 执行后的结果:
const renderResult = column?.render?.(data[column.dataIndex], data);
注意 render 需要传入两个参数,一个是 text,一个是这行的数据对象,我们都能确定参数的值,所以直接传入。
然后判断 renderResult的类型,如果是 object 类型,说明是个由 html 标签包裹的 ReactNode,需要递归取出最终渲染的值。如果是非 object 类型,说明是 boolean 或者 string 这样的基本类型,即没有被标签包裹,可以直接展示。
由于我们采用了递归来取最后渲染的值,所以无论嵌套了多少层标签,都可以正确的取到值。

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

如果文件、文件夹嵌套比较深,可以使用 downloadFiles2ZipWithFolder()方法。
文件结构如下图:
이미지.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实现的。

  • folderName빈 문자열이면 하위 파일이 아닌 아카이브의 최상위 디렉터리에 files넣 .
  • folderName는 다음과 같은 일반적인 문자열입니다. 그런 다음 파일 이름 으로文件夹1 새 폴더를 만들고 이 폴더에 넣습니다.folderNamefiles
  • folderName다음 과 같이 슬래시가 있는 문자열인 경우 n개의 폴더를 순서대로 생성하고 중첩 관계를 유지하고 마지막으로 마지막 폴더 文件夹2/文件夹2-1/文件夹2-1-1아래에 넣습니다 .files

데모의 전체 코드를 보려면 소스 코드 주소: github.com/cachecats/e…

추천

출처juejin.im/post/7080169896209809445