前端提效 Magic,導出多個Excel文件并打包為壓縮包下載
本篇文章主要介紹使用 exceljs、file-saver、jszip實現(xiàn)下載包含多層級文件夾、多個 excel、每個 excel 支持多個 sheet 的 zip 壓縮包。
上一篇文章:前端復雜表格導出excel,一鍵導出 Antd Table 看這篇就夠了(附源碼)[1]詳細介紹了如何實現(xiàn)解析 Antd Table、組裝數據和調整表格的樣式,感興趣的可以先看看。
本篇將接著上一篇,重點講方法的更高級抽象,和下載多層級文件夾的 zip 壓縮包。
源碼地址:github.com/cachecats/e…[2]
實現(xiàn)效果
最終下載的是 壓縮包.zip,解壓之后包含多個文件夾,每個文件夾下又可以無限嵌套子文件夾,excel 文件可以自由選擇放到根目錄下,或者子文件夾下。
實現(xiàn)效果如圖:
使用方法
使用方式也很簡單,經過高度封裝后,只需按照方法參數的規(guī)則傳入參數即可:
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 方法
我們來封裝一個常用的,預定義好樣式,直接能開箱即用的導出方法,使用者不用關心具體細節(jié),只管簡單的調用:
function?onExportExcel()?{
??downloadExcel({
????filename:?'test',
????sheets:?[{
??????sheetName:?'test',
??????columns:?columns,
??????dataSource:?list
????}]
??})
}
復制代碼
如上,直接調用 downloadExcel方法,它傳入一個對象作為參數,分別有 filename和 sheets兩個屬性。
- filename:文件名。不用帶
.xlsx后綴,會自動加后綴名。 - sheets:sheet 數組。傳入幾個 sheet 對象就會創(chuàng)建幾個 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)?{
??//?創(chuàng)建工作簿
??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方法處理每一行具體的數據。
這三個方法的實現(xiàn)在上篇文章都有介紹,如需了解更多請查看源碼:github.com/cachecats/e…[3]
導出的 excel 效果如下圖,列寬會根據傳入的 width 動態(tài)計算,單元格高度會根據內容自動撐開。
二、導出包含多個 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 創(chuàng)建 workbook并將數據寫入,然后通過 JsZip庫寫入到壓縮文件內,最終用 file-saver庫提供的 saveAs方法導出壓縮文件。
注意 12、13行,handleEachFile()方法返回的是一個 Promise,需要等所有異步方法都執(zhí)行完之后再執(zhí)行下面的生成 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)?{
??//?創(chuàng)建工作簿
??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 函數執(zhí)行后的值才是正確的。
比如 Table 的 columns 如下:
const?columns:?ColumnsType<any>?=?[
????{
??????width:?50,
??????dataIndex:?'id',
??????key:?'id',
??????title:?'ID',
??????render:?(text,?row)?=>?{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) => {row.id + 20}
,經過計算后,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-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。
結構如下圖所示:

循環(huán) dataSource和 columns,就得到了每個單元格要顯示的內容,通過執(zhí)行 render 函數,得到 render 執(zhí)行后的結果:
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方法能實現(xiàn)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實現(xiàn)的。
folderName為空字符串,則將它的 files放入壓縮包的頂級目錄中,不在任何子文件內。folderName為普通字符串,如:文件夾1,則以 folderName為文件名新建一個文件夾,并將它的 files放入此文件夾下。folderName為帶斜杠的字符串,如:文件夾2/文件夾2-1/文件夾2-1-1,則按照順序依次新建 n 個文件夾并保持嵌套關系,最終將它的files放入最后一個文件夾下。
如需查看 demo 完整代碼,源碼地址:github.com/cachecats/e…[4]
我的博客即將同步至騰訊云+社區(qū),邀請大家一同入駐:cloud.tencent.com/developer/s…[5]
關于本文
作者:solocoderhttps://juejin.cn/post/7080169896209809445
最后
歡迎關注【前端瓶子君】??ヽ(°▽°)ノ?
回復「算法」,加入前端編程源碼算法群!領取最新最熱的前端算法小書、面試小書以及海量簡歷模板,期待與你共進步!回復「交流」,吹吹水、聊聊技術、吐吐槽!回復「閱讀」,每日刷刷高質量好文!如果這篇文章對你有幫助,「在看」是最大的支持?》》面試官也在看的算法資料《《
“在看和轉發(fā)”就是最大的支持
