前端导出数据量大表格为excel (前端实现excel导入导出功能)

本篇文章主要介绍使用 exceljs file-saver jszip 实现*载下**包含多层级文件夹、多个 excel、每个 excel 支持多个 sheet 的 zip 压缩包。上一篇文章: 前端复杂表格导出excel,一键导出 Antd Table 看这篇就够了(附源码) [1] 详细介绍了如何实现解析 Antd Table、组装数据和调整表格的样式,感兴趣的可以先看看。本篇将接着上一篇,重点讲方法的更高级抽象,和*载下**多层级文件夹的 zip 压缩包。源码地址: github.com/cachecats/e… [2]

实现效果

最终*载下**的是 压缩包.zip ,解压之后包含多个文件夹,每个文件夹下又可以无限嵌套子文件夹,excel 文件可以自由选择放到根目录下,或者子文件夹下。实现效果如图:

前端导出数据量大表格为excel,前端实现excel的导入导出

使用方法

使用方式也很简单,经过高度封装后,只需按照方法参数的规则传入参数即可:

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,预设样式,可包含多个 sheet。
  • downloadFiles2Zip:将多个 excel 文件导出到一个 zip 压缩包内,没有嵌套文件夹。
  • downloadFiles2ZipWithFolder:导出包含多级子文件夹、每级包含多个 excel 文件的 zip 压缩包。

一、封装普通的*载下**导出 excel 方法

我们来封装一个常用的,预定义好样式,直接能开箱即用的导出方法,使用者不用关心具体细节,只管简单的调用:

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

如上,直接调用 downloadExcel 方法,它传入一个对象作为参数,分别有 filename sheets 两个属性。

  • filename:文件名。不用带 .xlsx后缀,会自动加后缀名。
  • sheets:sheet 数组。传入几个 sheet 对象就会创建几个 sheet 页。

Sheet 对象的定义:

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

核心代码

downloadExcel 方法关键源码:

exportinterfaceIDownloadExcel{
filename:string;
sheets:ISheet[];
}

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

/**
**载下**导出简单的表格
*@paramparams
*/
exportfunctiondownloadExcel(params:IDownloadExcel){
//创建工作簿
constworkbook=newExcelJs.Workbook();
params?.sheets?.forEach((sheet)=>handleEachSheet(workbook,sheet));
saveWorkbook(workbook,`${params.filename}.xlsx`);
}


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


exportfunctionsaveWorkbook(workbook:Workbook,fileName:string){
//导出文件
workbook.xlsx.writeBuffer().then((data:any)=>{
constblob=newBlob([data],{type:''});
saveAs(blob,fileName);
});
}
复制代码

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

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

前端导出数据量大表格为excel,前端实现excel的导入导出

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

如果没有多级目录的需求,只想把多个 excel 文件打包到一个压缩包里,可以用 downloadFiles2Zip 这个方法,得到的目录结构如下图:

前端导出数据量大表格为excel,前端实现excel的导入导出

参数结构如下,支持导出多个 excel 文件,每个 excel 文件又可以包含多个 sheet。

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

exportinterfaceIDownloadExcel{
filename:string;
sheets:ISheet[];
}

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

使用示例

functiononExportZip(){
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*asExcelJsfrom'exceljs';
import{Workbook,Worksheet,Row}from'exceljs';
importJsZipfrom'jszip'

/**
*导出多个文件为zip压缩包
*/
exportasyncfunctiondownloadFiles2Zip(params:IDownloadFiles2Zip){
constzip=newJsZip();
//待每个文件都写入完之后再生成zip文件
constpromises=params?.files?.map(asyncparam=>awaithandleEachFile(param,zip,''))
awaitPromise.all(promises);
zip.generateAsync({type:"blob"}).then(blob=>{
saveAs(blob,`${params.zipName}.zip`)
})
}

asyncfunctionhandleEachFile(param:IDownloadExcel,zip:JsZip,folderName:string){
//创建工作簿
constworkbook=newExcelJs.Workbook();
param?.sheets?.forEach((sheet)=>handleEachSheet(workbook,sheet));
//生成blob
constdata=awaitworkbook.xlsx.writeBuffer();
constblob=newBlob([data],{type:''});
if(folderName){
zip.folder(folderName)?.file(`${param.filename}.xlsx`,blob)
}else{
//写入zip中一个文件
zip.file(`${param.filename}.xlsx`,blob);
}
}

functionhandleEachSheet(workbook:Workbook,sheet:ISheet){
//添加sheet
constworksheet=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 如下:

constcolumns: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,如下图:

前端导出数据量大表格为excel,前端实现excel的导入导出

这时导出的 excel 应该跟页面上显示的一模一样,这样才是正确的。点击【导出zip】按钮,解压后打开*载下**的其中一个 excel,验证显示的内容跟在线表格完全一致。

前端导出数据量大表格为excel,前端实现excel的导入导出

那么是如何做到的呢?主要看 handleDataWithRender() 方法:

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


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

worksheet.addRows() 可以添加数据对象,也可以添加由每行的每列组成的二维数组。由于我们要自己控制每个单元格显示的内容,所以采用第二种方式,传入一个二维数组来构造 row。结构如下图所示:

前端导出数据量大表格为excel,前端实现excel的导入导出

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

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

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

前端导出数据量大表格为excel,前端实现excel的导入导出

核心代码

exportinterfaceIDownloadFiles2ZipWithFolder{
zipName:string;
folders:IFolder[];
}

exportinterfaceIFolder{
folderName:string;
files:IDownloadExcel[];
}

exportinterfaceIDownloadExcel{
filename:string;
sheets:ISheet[];
}

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


/**
*导出支持多级文件夹的压缩包
*@paramparams
*/
exportasyncfunctiondownloadFiles2ZipWithFolder(params:IDownloadFiles2ZipWithFolder){
constzip=newJsZip();
constoutPromises=params?.folders?.map(asyncfolder=>awaithandleFolder(zip,folder))
awaitPromise.all(outPromises);
zip.generateAsync({type:"blob"}).then(blob=>{
saveAs(blob,`${params.zipName}.zip`)
})
}

asyncfunctionhandleFolder(zip:JsZip,folder:IFolder){
console.log({folder})
letfolderPromises:Promise<any>[]=[];
constpromises=folder?.files?.map(asyncparam=>awaithandleEachFile(param,zip,folder.folderName));
awaitPromise.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,则以 folderName为文件名新建一个文件夹,并将它的 files放入此文件夹下。
  • folderName为带斜杠的字符串,如:文件夹2/文件夹2-1/文件夹2-1-1,则按照顺序依次新建 n 个文件夹并保持嵌套关系,最终将它的files放入最后一个文件夹下。

如需查看 demo 完整代码,源码地址: github.com/cachecats/e… [4]

我的博客即将同步至腾讯云+社区,邀请大家一同入驻: cloud.tencent.com/developer/s… [5]

关于本文

作者:solocoder

https://juejin.cn/post/7080169896209809445