前端提效 Magic,导出多个Excel文件并打包为压缩包下载
前端提效 Magic,导出多个 Excel 文件并打包为压缩包下载
solocoder 前端瓶子君;)
前端瓶子君
微信号 pinzi_com
功能介绍 瓶子君,2021 致力于帮助前端开启技术专项 + 算法之路!
2022-04-01 08:40
收录于话题
本篇文章主要介绍使用 exceljs
、file-saver
、jszip
实现下载包含多层级文件夹、多个 excel、每个 excel 支持多个 sheet 的 zip 压缩包。
上一篇文章:前端复杂表格导出 excel,一键导出 Antd Table 看这篇就够了(附源码)[1]详细介绍了如何实现解析 Antd Table、组装数据和调整表格的样式,感兴趣的可以先看看。
本篇将接着上一篇,重点讲方法的更高级抽象,和下载多层级文件夹的 zip 压缩包。
源码地址:github.com/cachecats/e…[2]
实现效果
最终下载的是 压缩包. zip
,解压之后包含多个文件夹,每个文件夹下又可以无限嵌套子文件夹,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 方法
我们来封装一个常用的,预定义好样式,直接能开箱即用的导出方法,使用者不用关心具体细节,只管简单的调用:
function onExportExcel() { downloadExcel({ filename: 'test', sheets: [{ sheetName: 'test', columns: columns, dataSource: list }] })}复制代码
如上,直接调用 downloadExcel
方法,它传入一个对象作为参数,分别有 filename
和 sheets
两个属性。
- filename:文件名。不用带
.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…[3]
导出的 excel 效果如下图,列宽会根据传入的 width 动态计算,单元格高度会根据内容自动撑开。
二、导出包含多个 excel 的 zip 压缩包
如果没有多级目录的需求,只想把多个 excel 文件打包到一个压缩包里,可以用 downloadFiles2Zip
这个方法,得到的目录结构如下图:
参数结构如下,支持导出多个 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,如下图:
这时导出的 excel 应该跟页面上显示的一模一样,这样才是正确的。
点击【导出 zip】按钮,解压后打开下载的其中一个 excel,验证显示的内容跟在线表格完全一致。
那么是如何做到的呢?
主要看 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-ignorefunction getValueFromRender(renderResult: any) { if (renderResult?.type) { let children = renderResult?.props?.children; if (children?.type) { return getValueFromRender(children); } else { return children; } } return ''}复制代码
worksheet.addRows()
可以添加数据对象,也可以添加由每行的每列组成的二维数组。由于我们要自己控制每个单元格显示的内容,所以采用第二种方式,传入一个二维数组来构造 row。
结构如下图所示:
循环 dataSource
和 columns
,就得到了每个单元格要显示的内容,通过执行 render 函数,得到 render 执行后的结果:
const renderResult = column?.render?.(data[column.dataIndex], data);
注意 render 需要传入两个参数,一个是 text,一个是这行的数据对象,我们都能确定参数的值,所以直接传入。
然后判断 renderResult
的类型,如果是 object 类型,说明是个由 html 标签包裹的 ReactNode,需要递归取出最终渲染的值。如果是非 object 类型,说明是 boolean 或者 string 这样的基本类型,即没有被标签包裹,可以直接展示。
由于我们采用了递归来取最后渲染的值,所以无论嵌套了多少层标签,都可以正确的取到值。
三、导出包含多个子文件夹、多个 excel 文件的 zip 压缩包
如果文件、文件夹嵌套比较深,可以使用 downloadFiles2ZipWithFolder()
方法。
文件结构如下图:
核心代码
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
,则以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
最后
欢迎关注【前端瓶子君】✿✿ヽ (°▽°) ノ✿
回复「算法」,加入前端编程源码算法群!领取最新最热的前端算法小书、面试小书以及海量简历模板,期待与你共进步!
回复「交流」,吹吹水、聊聊技术、吐吐槽!
回复「阅读」,每日刷刷高质量好文!
如果这篇文章对你有帮助,「在看」是最大的支持
前端瓶子君
瓶子君,2021 致力于帮助前端开启技术专项 + 算法之路!
39 篇原创内容
公众号
》》面试官也在看的算法资料《《
“在看和转发” 就是最大的支持
预览时标签不可点
收录于话题 #
个
上一篇 下一篇
喜欢此内容的人还喜欢
C 函数指针别再停留在语法,得上升到软件设计~
C 函数指针别再停留在语法,得上升到软件设计~
嵌入式资讯精选
不喜欢
不看的原因
确定
- 内容质量低
- 不看此公众号
前端请装上这个 Chrome 插件
前端请装上这个 Chrome 插件
前端下午茶
不喜欢
不看的原因
确定
- 内容质量低
- 不看此公众号
嵌入式软件编程规范
嵌入式软件编程规范
玩转嵌入式
不喜欢
不看的原因
确定
- 内容质量低
- 不看此公众号
写留言
我的留言
正在加载
展开我的留言
以上留言被精选后,将对所有人可见
精选留言
正在加载
已无更多数据
关闭
写留言
提交更多
正在加载
正在加载
留言
微信扫一扫
关注该公众号
微信扫一扫
使用小程序
: , 。 视频 小程序 赞 ,轻点两下取消赞 在看 ,轻点两下取消在看
该账号因违规无法跳转
https://mp.weixin.qq.com/s/aHtaBUh2xST9Oq83HeEr9Q