<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          前端提效 Magic,導出多個Excel文件并打包為壓縮包下載

          共 12419字,需瀏覽 25分鐘

           ·

          2022-04-01 20:33

          本篇文章主要介紹使用 exceljsfile-saverjszip實現(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)效果如圖:
          8fba46afdf4fbe65d4fa41cfc0815158.webp

          使用方法

          使用方式也很簡單,經過高度封裝后,只需按照方法參數的規(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方法,它傳入一個對象作為參數,分別有 filenamesheets兩個屬性。

          • 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)計算,單元格高度會根據內容自動撐開。
          b74e007cd0598f3050f94f63453fd48a.webp

          二、導出包含多個 excel 的 zip 壓縮包

          如果沒有多級目錄的需求,只想把多個 excel 文件打包到一個壓縮包里,可以用 downloadFiles2Zip這個方法,得到的目錄結構如下圖:
          f8088910e25406228b8deaf2aed09f5a.webp
          參數結構如下,支持導出多個 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,如下圖:
          20aa0ed5909adcd27d865da857e3dc9a.webp
          這時導出的 excel 應該跟頁面上顯示的一模一樣,這樣才是正確的。
          點擊【導出zip】按鈕,解壓后打開下載的其中一個 excel,驗證顯示的內容跟在線表格完全一致。
          b74e007cd0598f3050f94f63453fd48a.webp
          那么是如何做到的呢?
          主要看 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。
          結構如下圖所示:
          3f0978edbf19a7be1c94d53eefdd568a.webp
          循環(huán) dataSourcecolumns,就得到了每個單元格要顯示的內容,通過執(zhí)行 render 函數,得到 render 執(zhí)行后的結果:
          const renderResult = column?.render?.(data[column.dataIndex], data);
          注意 render 需要傳入兩個參數,一個是 text,一個是這行的數據對象,我們都能確定參數的值,所以直接傳入。
          然后判斷 renderResult的類型,如果是 object 類型,說明是個由 html 標簽包裹的 ReactNode,需要遞歸取出最終渲染的值。如果是非 object 類型,說明是 boolean 或者 string 這樣的基本類型,即沒有被標簽包裹,可以直接展示。
          由于我們采用了遞歸來取最后渲染的值,所以無論嵌套了多少層標簽,都可以正確的取到值。

          三、導出包含多個子文件夾、多個excel文件的 zip 壓縮包

          如果文件、文件夾嵌套比較深,可以使用 downloadFiles2ZipWithFolder()方法。
          文件結構如下圖:
          c803a926622a5980d8dc6b028bde3ddb.webp

          核心代碼

          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]


          關于本文

          作者:solocoder

          https://juejin.cn/post/7080169896209809445


          最后


          歡迎關注【前端瓶子君】??ヽ(°▽°)ノ?
          回復「算法」,加入前端編程源碼算法群領取最新最熱的前端算法小書、面試小書以及海量簡歷模板,期待與你共進步!回復「交流」,吹吹水、聊聊技術、吐吐槽!回復「閱讀」,每日刷刷高質量好文!如果這篇文章對你有幫助,在看」是最大的支持?》》面試官也在看的算法資料《《
          “在看和轉發(fā)”就是最大的支持



          瀏覽 70
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  日本爱爱一区二区视频 | 翔田千里在线视频 | 69视频在线免费观看 | 大香蕉婷婷五月天 | 国产网站在线 |