前端導出 excel 的需求很多,但市面上好用的庫並不多,講明白複雜使用場景的文章更少。
本文將以文字 + demo 源碼的形式,力求講清楚滿足 99% 使用場景的終極 excel 導出方案。
如果項目中用到了 AntD,那就更簡單了,因為 Table 本身已經設置好了 column 和 dataSource,只需解析 column 和 dataSource 即可快速導出 Excel。
實現功能:
源碼地址:github.com/cachecats/excel-export-demo[1]
第二篇文章:js 批量導出 excel 為zip壓縮包:https://juejin.cn/post/7080169896209809445/[2], 對導出方法進行了封裝,還實現了使用exceljs、file-saver、jszip實現下載包含多層級文件夾、多個 excel、每個 excel 支持多個 sheet 的 zip 壓縮包。
一、技術選型xlsx
呼聲最高的是xlsx[3],又叫SheetJS,也是下載量最高和star最多的庫。試用了一下很強大,但是!默認不支持改變樣式,想要支持改變樣式,需要使用它的收費版本。
本着勤儉節約的原則,很多人使用了另一個第三方庫:xlsx-style[4],但是使用起來極其複雜,還需要改 node_modules 源碼,這個庫最後更新時間也定格在了 6年前。還有一些其他的第三方樣式拓展庫,質量參差不齊。
使用成本和後期的維護成本很高,不得不放棄。
ExcelJS
ExcelJS[5]周下載量 450k,github star 9k,並且擁有中文文檔,對國內開發者很友好。雖然文檔是以README 的形式,可讀性不太好,但重在內容,常用的功能基本都有覆蓋。
最近更新時間是6個月內,試用了一下,集成很簡單,再加之文檔豐富,就選它了。
安裝:
npminstallexceljs複製代碼
下載到本地還需要另一個庫:file-saver
npminstallfile-saver複製代碼二、基本概念
先了解下基本概念,更詳細的介紹參考官方文檔:github.com/exceljs/exc…[6]
workbook
workbook:工作簿,可以理解為整個 excel 表格。
通過const workbook = new ExcelJS.Workbook()創建工作簿,還可以設置工作簿的屬性:
workbook.creator='Me';workbook.lastModifiedBy='Her';workbook.created=newDate(1985,8,30);workbook.modified=newDate();workbook.lastPrinted=newDate(2016,9,27);複製代碼worksheet
工作表,即 Excel 表格中的 sheet 頁。
通過const sheet = workbook.addWorksheet('My Sheet')創建工作表,每個 workbook 可添加多個 worksheet。
使用 addWorksheet 函數的第二個參數來指定工作表的選項。
//創建帶有紅色標籤顏色的工作表constsheet=workbook.addWorksheet('MySheet',{properties:{tabColor:{argb:'FFC0000'}}});//創建一個隱藏了網格線的工作表constsheet=workbook.addWorksheet('MySheet',{views:[{showGridLines:false}]});//創建一個第一行和列凍結的工作表constsheet=workbook.addWorksheet('MySheet',{views:[{xSplit:1,ySplit:1}]});//使用A4設置的頁面設置設置創建新工作表-橫向constworksheet=workbook.addWorksheet('MySheet',{pageSetup:{paperSize:9,orientation:'landscape'}});//創建一個具有頁眉頁腳的工作表constsheet=workbook.addWorksheet('MySheet',{headerFooter:{firstHeader:"HelloExceljs",firstFooter:"HelloWorld"}});//創建一個凍結了第一行和第一列的工作表constsheet=workbook.addWorksheet('MySheet',{views:[{state:'frozen',xSplit:1,ySplit:1}]});複製代碼columns
列,通過worksheet.columns可設置表頭。
//添加列標題並定義列鍵和寬度//注意:這些列結構僅是構建工作簿的方便之處,除了列寬之外,它們不會完全保留。worksheet.columns=[{header:'Id',key:'id',width:10},{header:'Name',key:'name',width:32},{header:'D.O.B.',key:'DOB',width:10,outlineLevel:1}];//通過鍵,字母和基於1的列號訪問單個列constidCol=worksheet.getColumn('id');constnameCol=worksheet.getColumn('B');constdobCol=worksheet.getColumn(3);//設置列屬性//注意:將覆蓋 C1 單元格值dobCol.header='DateofBirth';//注意:這將覆蓋 C1:C2 單元格值dobCol.header=['DateofBirth','A.K.A.D.O.B.'];//從現在開始,此列將以「dob」而不是「DOB」建立索引dobCol.key='dob';dobCol.width=15;//如果需要,隱藏列dobCol.hidden=true;複製代碼
還可對列進行各種操作。
//遍歷此列中的所有當前單元格dobCol.eachCell(function(cell,rowNumber){//...});//遍歷此列中的所有當前單元格,包括空單元格dobCol.eachCell({includeEmpty:true},function(cell,rowNumber){//...});//添加一列新值worksheet.getColumn(6).values=[1,2,3,4,5];//添加稀疏列值worksheet.getColumn(7).values=[,,2,3,,5,,7,,,,11];//剪切一列或多列(右邊的列向左移動)//如果定義了列屬性,則會相應地對其進行切割或移動//已知問題:如果拼接導致任何合併的單元格移動,結果可能是不可預測的worksheet.spliceColumns(3,2);//刪除一列,再插入兩列。//注意:第4列及以上的列將右移1列。//另外:如果工作表中的行數多於列插入項中的值,則行將仍然被插入,就好像值存在一樣。constnewCol3Values=[1,2,3,4,5];constnewCol4Values=['one','two','three','four','five'];worksheet.spliceColumns(3,1,newCol3Values,newCol4Values);複製代碼row
行,可以添加一行或者同時添加多行數據,是使用最頻繁的屬性。
//通過json添加一行數據,需要先設置columnsworksheet.addRow({id:1,name:'JohnDoe',dob:newDate(1970,1,1)});worksheet.addRow({id:2,name:'JaneDoe',dob:newDate(1965,1,7)});//通過數組添加一行數據worksheet.addRow([3,'Sam',newDate()]);//同時添加多行數據worksheet.addRows(list);//遍歷工作表中具有值的所有行worksheet.eachRow(function(row,rowNumber){console.log('Row'+rowNumber+'='+JSON.stringify(row.values));});//遍歷工作表中的所有行(包括空行)worksheet.eachRow({includeEmpty:true},function(row,rowNumber){console.log('Row'+rowNumber+'='+JSON.stringify(row.values));});//連續遍歷所有非空單元格row.eachCell(function(cell,colNumber){console.log('Cell'+colNumber+'='+cell.value);});//遍歷一行中的所有單元格(包括空單元格)row.eachCell({includeEmpty:true},function(cell,colNumber){console.log('Cell'+colNumber+'='+cell.value);});複製代碼三、簡單表格導出
本文所有示例都使用 React + AntD。
先看效果,我們用 AntD 的 Table 寫個簡單的表格頁面,並設置不同的列寬:

點擊導出 excel,然後打開得到以下結果:

可以看到,導出的 excel 列寬比例跟在線的表格是一致的。
貼源碼:
//簡單demoimportReact,{useEffect,useState}from'react'import{Button,Card,Table}from"antd";import{ColumnsType}from"antd/lib/table/interface";import*asExcelJsfrom'exceljs';import{generateHeaders,saveWorkbook}from"../utils";interfaceSimpleDemoProps{}interfaceStudentInfo{id:number;name:string;age:number;gender:string;}constSimpleDemo:React.FC<SimpleDemoProps>=()=>{const[list,setList]=useState<StudentInfo[]>([]);useEffect(()=>{generateData();},[])functiongenerateData(){letarr:StudentInfo[]=[];for(leti=0;i<10;i++){arr.push({id:i,name:`小明${i}號`,age:i,gender:i%2===0?'男':'女'})}setList(arr);}constcolumns:ColumnsType<any>=[{width:50,dataIndex:'id',key:'id',title:'ID',},{width:100,dataIndex:'name',key:'name',title:'姓名',},{width:50,dataIndex:'age',key:'age',title:'年齡',},{width:80,dataIndex:'gender',key:'gender',title:'性別',},];functiononExportBasicExcel(){//創建工作簿constworkbook=newExcelJs.Workbook();//添加sheetconstworksheet=workbook.addWorksheet('demosheet');//設置sheet的默認行高worksheet.properties.defaultRowHeight=20;//設置列worksheet.columns=generateHeaders(columns);//添加行worksheet.addRows(list);//導出excelsaveWorkbook(workbook,'simple-demo.xlsx');}return(<Card><h3>簡單表格</h3><Buttontype={'primary'}style={{marginBottom:10}}onClick={onExportBasicExcel}>導出excel</Button><Tablecolumns={columns}dataSource={list}/></Card>);}exportdefaultSimpleDemo複製代碼
真正導出的代碼只有幾行,重點看onExportBasicExcel方法:
解析 AntD Table 的 columns 和 dataSource
因為我們是用 AntD 的 Table,其實已經構造出了表頭和具體的表格數據,所以只需解析即可。
generateHeaders()方法是自己封裝的,將 Table 的 columns 轉換為ExcelJS的表頭格式的方法:
import{ITableHeader}from"src/types";import{ColumnsType}from"antd/lib/table/interface";constDEFAULT_COLUMN_WIDTH=20;//根據antd的column生成exceljs的columnexportfunctiongenerateHeaders(columns:any[]){returncolumns?.map(col=>{constobj:ITableHeader={//顯示的nameheader:col.title,//用於數據匹配的keykey:col.dataIndex,//列寬width:col.width/5||DEFAULT_COLUMN_WIDTH,};returnobj;})}複製代碼
在ExcelJS中,header 字段表示顯示的表頭內容,key 是用於匹配數據的 key,width 是列寬。在 Table 的 column 中都有對應的字段,取出來賦值即可。注意設置列寬的時候,在線表格和 excel 的單位可能不一致,需要除以一個係數才不至於太寬。至於具體除多少,可以不斷試驗得出個最佳值,我試的除以 5 效果比較好。
通過worksheet.addRows()方法可以為工作表添加多行數據,因為上面我們已經設置了表頭,程序知道了每列數據應該匹配哪個字段,所以這裡直接傳入 Table 的 dataSource 即可。
也可以通過worksheet.addRow()逐行添加數據。
下載 excel
saveWorkbook()也是自己封裝的方法,接收 workbook 和文件名來下載 excel 到本地。
下載是使用file-saver庫。
import{saveAs}from"file-saver";import{Workbook}from"exceljs";exportfunctionsaveWorkbook(workbook:Workbook,fileName:string){//導出文件workbook.xlsx.writeBuffer().then((data=>{constblob=newBlob([data],{type:''});saveAs(blob,fileName);}))}複製代碼
到此,可以通過短短几行代碼實現 AntD 的 Table 導出啦。
四、修改樣式
單元格,行和列均支持一組豐富的樣式和格式,這些樣式和格式會影響單元格的顯示方式。
通過分配以下屬性來設置樣式:
添加背景色
我們先給表頭添加背景。因為表頭是第一行,可以通過 getRow(1) 來獲取表頭這一行:
//給表頭添加背景色letheaderRow=worksheet.getRow(1);headerRow.fill={type:'pattern',pattern:'solid',fgColor:{argb:'dff8ff'},}複製代碼
可以直接用row.fill為整行設置背景色,這樣的話這一行沒有內容的單元格也會有顏色,如圖:

從 E 列開始其實就沒有數據了,如果只想給非空單元格設置背景呢?
很遺憾 row 暴露的方法不支持直接這樣設置,但可以曲線救國,遍曆本行的所有非空單元格,再給每個單元格設置背景即可。
//通過cell設置背景色,更精準headerRow.eachCell((cell,colNum)=>{cell.fill={type:'pattern',pattern:'solid',fgColor:{argb:'dff8ff'},}})複製代碼
使用單元格控制會更加的精準,可以看到空的單元格已經沒有背景色了。
修改字體樣式
可以設置文字的字體、字號、顏色等屬性,支持的屬性如下表:
與設置背景色相同,可以通過 row 或 cell 來設置。示例將通過 cell 設置。
修改表頭的字體為微軟雅黑,字號12號,顏色為紅色,加粗斜體。
//通過cell設置樣式,更精準headerRow.eachCell((cell,colNum)=>{//設置背景色cell.fill={type:'pattern',pattern:'solid',fgColor:{argb:'dff8ff'},}//設置字體cell.font={bold:true,italic:true,size:12,name:'微軟雅黑',color:{argb:'ff0000'},};})複製代碼設置對齊方式
有效的對齊屬性:
表格默認的對齊方式是靠下對齊,一般都會設置為垂直方向居中對齊,文本靠左對齊,數字靠右對齊。這裡為了方便都設置為水平方向靠左對齊,垂直方向居中對齊。
//添加行letrows=worksheet.addRows(list);rows?.forEach(row=>{//設置字體row.font={size:11,name:'微軟雅黑',};//設置對齊方式row.alignment={vertical:'middle',horizontal:'left',wrapText:false,};})複製代碼
addRows()的返回值是被添加的行的數組,然後循環對每行設置字體和對齊方式,就完成了對整個 excel 的樣式自定義。
當然也可以對每個 cell 進行設置,效果是一樣的。

設置邊框也是同樣的方法,這裡不做介紹啦。
完整的導出帶樣式的 excel 代碼:
//導出functiononExportBasicExcelWithStyle(){//創建工作簿constworkbook=newExcelJs.Workbook();//添加sheetconstworksheet=workbook.addWorksheet('demosheet');//設置sheet的默認行高worksheet.properties.defaultRowHeight=20;//設置列worksheet.columns=generateHeaders(columns);//給表頭添加背景色。因為表頭是第一行,可以通過 getRow(1)來獲取表頭這一行letheaderRow=worksheet.getRow(1);//直接給這一行設置背景色//headerRow.fill={//type:'pattern',//pattern:'solid',//fgColor:{argb:'dff8ff'},//}//通過cell設置樣式,更精準headerRow.eachCell((cell,colNum)=>{//設置背景色cell.fill={type:'pattern',pattern:'solid',fgColor:{argb:'dff8ff'},}//設置字體cell.font={bold:true,italic:true,size:12,name:'微軟雅黑',color:{argb:'ff0000'},};//設置對齊方式cell.alignment={vertical:'middle',horizontal:'left',wrapText:false,};})//添加行letrows=worksheet.addRows(list);//設置每行的樣式rows?.forEach(row=>{//設置字體row.font={size:11,name:'微軟雅黑',};//設置對齊方式row.alignment={vertical:'middle',horizontal:'left',wrapText:false,};})//導出excelsaveWorkbook(workbook,'simple-demo.xlsx');}複製代碼五、行合併&列合併
先看在線表格的效果:

導出的 excel:

這個表格涉及到多級表頭、行合併、列合併。
涉及到以下幾個重難點:
先貼出完整的代碼
importReact,{useEffect,useState}from'react'import{Button,Card,Space,Table}from"antd";import{ColumnsType}from"antd/lib/table/interface";import{ITableHeader,StudentInfo}from"../types";import*asExcelJsfrom"exceljs";import{addHeaderStyle,DEFAULT_COLUMN_WIDTH,DEFAULT_ROW_HEIGHT,generateHeaders,getColumnNumber,mergeColumnCell,mergeRowCell,saveWorkbook}from"../utils";import{Worksheet}from"exceljs";interfaceMultiHeaderProps{}constcolumns:ColumnsType<any>=[{width:50,dataIndex:'id',key:'id',title:'ID',},{width:100,dataIndex:'name',key:'name',title:'姓名',},{width:50,dataIndex:'age',key:'age',title:'年齡',},{width:80,dataIndex:'gender',key:'gender',title:'性別',},{dataIndex:'score',key:'score',title:'成績',children:[{width:80,dataIndex:'english',key:'english',title:'英語',},{width:80,dataIndex:'math',key:'math',title:'數學',},{width:80,dataIndex:'physics',key:'physics',title:'物理',},]},{width:250,dataIndex:'comment',key:'comment',title:'老師評語',},];constMultiHeader:React.FC<MultiHeaderProps>=()=>{const[list,setList]=useState<StudentInfo[]>([]);useEffect(()=>{generateData();},[])functiongenerateData(){letarr:StudentInfo[]=[];for(leti=0;i<5;i++){arr.push({id:i,name:`小明${i}號`,age:8+i,gender:i%2===0?'男':'女',english:80+i,math:60+i,physics:70+i,comment:`小明${i}號同學表現非常好,熱心助人,成績優秀,是社會主義接班人`})}setList(arr);}functiononExportMultiHeaderExcel(){//創建工作簿constworkbook=newExcelJs.Workbook();//添加sheetconstworksheet=workbook.addWorksheet('demosheet');//設置sheet的默認行高worksheet.properties.defaultRowHeight=20;//解析AntDTable的columnsconstheaders=generateHeaders(columns);console.log({headers})//第一行表頭constnames1:string[]=[];//第二行表頭constnames2:string[]=[];//用於匹配數據的keysconstheaderKeys:string[]=[];headers.forEach(item=>{if(item.children){//有children說明是多級表頭,headername需要兩行item.children.forEach(child=>{names1.push(item.header);names2.push(child.header);headerKeys.push(child.key);});}else{constcolumnNumber=getColumnNumber(item.width);for(leti=0;i<columnNumber;i++){names1.push(item.header);names2.push(item.header);headerKeys.push(item.key);}}});handleHeader(worksheet,headers,names1,names2);//添加數據addData2Table(worksheet,headerKeys,headers);//給每列設置固定寬度worksheet.columns=worksheet.columns.map(col=>({...col,width:DEFAULT_COLUMN_WIDTH}));//導出excelsaveWorkbook(workbook,'simple-demo.xlsx');}functionhandleHeader(worksheet:Worksheet,headers:ITableHeader[],names1:string[],names2:string[],){//判斷是否有children,有的話是兩行表頭constisMultiHeader=headers?.some(item=>item.children);if(isMultiHeader){//加表頭數據constrowHeader1=worksheet.addRow(names1);constrowHeader2=worksheet.addRow(names2);//添加表頭樣式addHeaderStyle(rowHeader1,{color:'dff8ff'});addHeaderStyle(rowHeader2,{color:'dff8ff'});mergeColumnCell(headers,rowHeader1,rowHeader2,names1,names2,worksheet);return;}//加表頭數據constrowHeader=worksheet.addRow(names1);//表頭根據內容寬度合併單元格mergeRowCell(headers,rowHeader,worksheet);//添加表頭樣式addHeaderStyle(rowHeader,{color:'dff8ff'});}functionaddData2Table(worksheet:Worksheet,headerKeys:string[],headers:ITableHeader[]){list?.forEach((item:any)=>{constrowData=headerKeys?.map(key=>item[key]);constrow=worksheet.addRow(rowData);mergeRowCell(headers,row,worksheet);row.height=DEFAULT_ROW_HEIGHT;//設置行樣式,wrapText:自動換行row.alignment={vertical:'middle',wrapText:false,shrinkToFit:false};row.font={size:11,name:'微軟雅黑'};})}return(<Card><h3>多表頭表格</h3><Spacestyle={{marginBottom:10}}><Buttontype={'primary'}onClick={onExportMultiHeaderExcel}>導出excel</Button></Space><Tablekey={'id'}columns={columns}dataSource={list}/></Card>);}exportdefaultMultiHeader複製代碼
前面幾步創建 workbook 和 worksheet 都是一樣的,從解析表頭generateHeaders()開始邏輯會有所不同。
表頭解析
我們修改上一節的generateHeaders()方法,添加有 children 時的邏輯。多級表頭時我們也構造出 children。
//根據antd的column生成exceljs的columnexportfunctiongenerateHeaders(columns:any[]){returncolumns?.map(col=>{constobj:ITableHeader={//顯示的nameheader:col.title,//用於數據匹配的keykey:col.dataIndex,//列寬width:col.width/5||DEFAULT_COLUMN_WIDTH,};if(col.children){obj.children=col.children?.map((item:any)=>({key:item.dataIndex,header:item.title,width:item.width,parentKey:col.dataIndex,}));}returnobj;})}複製代碼
構造出來的數據結構如下:

上一節簡單表格中我們用worksheet.columns = generateHeaders(columns)設置每一個表頭列所要顯示的信息和應該匹配的 key,但是它無法設置多級表頭,所以需要換一種思路,摒棄列(表頭)的概念,把表頭也當成一行數據來自己寫入。下面的每行數據,也都自己通過計算匹配出應該在什麼位置顯示什麼內容。
先來看這段代碼:
//解析AntDTable的columnsconstheaders=generateHeaders(columns);//第一行表頭constnames1:string[]=[];//第二行表頭constnames2:string[]=[];//用於匹配數據的keysconstheaderKeys:string[]=[];headers.forEach(item=>{if(item.children){//有children說明是多級表頭,headername需要兩行item.children.forEach(child=>{names1.push(item.header);names2.push(child.header);headerKeys.push(child.key);});}else{constcolumnNumber=getColumnNumber(item.width);for(leti=0;i<columnNumber;i++){names1.push(item.header);names2.push(item.header);headerKeys.push(item.key);}}});複製代碼
這個例子有兩級表頭,所以需要兩行來設置每一級表頭,分別命名為names1和names2,它們裡面存的是展示出來的 name,如:ID、姓名、年齡等。還需要一個headerKeys用來存儲每一列需要匹配的 key,如:id、name、age 等 json 的 key。
注意一點,headerKeys是以第二行表頭為準,因為第二行才是真正顯示的內容。
構造出了names1、names2和headerKeys,就可以開始生成真正的表頭了:
functionhandleHeader(worksheet:Worksheet,headers:ITableHeader[],names1:string[],names2:string[],){//判斷是否有children,有的話是兩行表頭constisMultiHeader=headers?.some(item=>item.children);if(isMultiHeader){//加表頭數據constrowHeader1=worksheet.addRow(names1);constrowHeader2=worksheet.addRow(names2);//添加表頭樣式addHeaderStyle(rowHeader1,{color:'dff8ff'});addHeaderStyle(rowHeader2,{color:'dff8ff'});mergeColumnCell(headers,rowHeader1,rowHeader2,names1,names2,worksheet);return;}//加表頭數據constrowHeader=worksheet.addRow(names1);//表頭根據內容寬度合併單元格mergeRowCell(headers,rowHeader,worksheet);//添加表頭樣式addHeaderStyle(rowHeader,{color:'dff8ff'});}複製代碼
先判斷有沒有多級表頭,單行表頭和多行表頭執行的邏輯不同。
通過worksheet.addRow()將表頭添加為一行數據,多行表頭就添加兩次。然後通過addHeaderStyle()給表頭添加樣式,這是自己封裝的方法,在utils里。最後也是最重要的是合併單元格,
合併同一行多列
合併單元格的方法是worksheet.mergeCells(),可以有很多種合併方式:
//合併一系列單元格worksheet.mergeCells('A4:B5');//...合併的單元格被鏈接起來了worksheet.getCell('B5').value='Hello,World!';expect(worksheet.getCell('B5').value).toBe(worksheet.getCell('A4').value);expect(worksheet.getCell('B5').master).toBe(worksheet.getCell('A4'));//...合併的單元格共享相同的樣式對象expect(worksheet.getCell('B5').style).toBe(worksheet.getCell('A4').style);worksheet.getCell('B5').style.font=myFonts.arial;expect(worksheet.getCell('A4').style.font).toBe(myFonts.arial);//取消單元格合併將打破鏈接的樣式worksheet.unMergeCells('A4');expect(worksheet.getCell('B5').style).not.toBe(worksheet.getCell('A4').style);expect(worksheet.getCell('B5').style.font).not.toBe(myFonts.arial);//按左上,右下合併worksheet.mergeCells('K10','M12');//按開始行,開始列,結束行,結束列合併(相當於K10:M12)worksheet.mergeCells(10,11,12,13);複製代碼
先看合併同一行多列的算法,核心在於先設置一個索引,從1開始,代表第一列。然後循環headers,如果當前 header 有 children,則每個子級占一列,然後索引值加1。如果沒有 children,計算這一個數據的寬度將會占用幾個單元格,也就是幾列,這個列數就是需要合併的列數,合併完之後索引值加1。
//行合併單元格exportfunctionmergeRowCell(headers:ITableHeader[],row:Row,worksheet:Worksheet){//當前列的索引letcolIndex=1;headers.forEach(header=>{const{width,children}=header;if(children){children.forEach(child=>{colIndex+=1;});}else{//需要的列數,四捨五入constcolNum=getColumnNumber(width);//如果colNum>1說明需要合併if(colNum>1){worksheet.mergeCells(Number(row.number),colIndex,Number(row.number),colIndex+colNum-1);}colIndex+=colNum;}});}exportfunctiongetColumnNumber(width:number){//需要的列數,四捨五入returnMath.round(width/DEFAULT_COLUMN_WIDTH);}複製代碼
合併單元格的方法是:
worksheet.mergeCells(Number(row.number), colIndex, Number(row.number), colIndex + colNum \- 1);
四個參數分別是合併的開始行、開始列、結束行、結束列。
通過row.number得到當前行的行數,因為是同一行的多列合併,所以開始結束行一致,開始列是索引值colIndex,結束列是colIndex + colNum \- 1。
同時合併行和列
如果是多級表頭,需要同時處理行和列合併,用到了封裝的mergeColumnCell方法。
基本思路是先判斷合併的類型,一共有三種情況:
然後計算出起始的行和列,以及結束的行和列。
//合併行和列,用於處理表頭合併exportfunctionmergeColumnCell(headers:ITableHeader[],rowHeader1:Row,rowHeader2:Row,nameRow1:string[],nameRow2:string[],worksheet:Worksheet,){//當前index的指針letpointer=-1;nameRow1.forEach((name,index)=>{//當index小於指針時,說明這一列已經被合併過了,不能再合併if(index<=pointer)return;//是否應該列合併constshouldVerticalMerge=name===nameRow2[index];//是否應該行合併constshouldHorizontalMerge=index!==nameRow1.lastIndexOf(name);pointer=nameRow1.lastIndexOf(name);if(shouldVerticalMerge&&shouldHorizontalMerge){//兩個方向都合併worksheet.mergeCells(Number(rowHeader1.number),index+1,Number(rowHeader2.number),nameRow1.lastIndexOf(name)+1,);}elseif(shouldVerticalMerge&&!shouldHorizontalMerge){//只在垂直方向上同一列的兩行合併worksheet.mergeCells(Number(rowHeader1.number),index+1,Number(rowHeader2.number),index+1);}elseif(!shouldVerticalMerge&&shouldHorizontalMerge){//只有水平方向同一行的多列合併worksheet.mergeCells(Number(rowHeader1.number),index+1,Number(rowHeader1.number),nameRow1.lastIndexOf(name)+1,);//eslint-disable-next-lineno-param-reassignconstcell=rowHeader1.getCell(index+1);cell.alignment={vertical:'middle',horizontal:'center'};}});}複製代碼添加數據行
在計算表頭時,已經得到了每列的 key 值列表headerKeys,通過headerKeys可以取出每一列對應的具體數據。
functionaddData2Table(worksheet:Worksheet,headerKeys:string[],headers:ITableHeader[]){list?.forEach((item:any)=>{constrowData=headerKeys?.map(key=>item[key]);constrow=worksheet.addRow(rowData);mergeRowCell(headers,row,worksheet);row.height=DEFAULT_ROW_HEIGHT;//設置行樣式,wrapText:自動換行row.alignment={vertical:'middle',wrapText:false,shrinkToFit:false};row.font={size:11,name:'微軟雅黑'};})}複製代碼
先循環數據列表,然後循環headerKeys取出對應的值,再通過worksheet.addRow將這一行數據添加進表格中。由於可能出現一個字段占用多列的情況,所以還需要進行合併單元格操作,可以復用mergeRowCell()方法。最後設置每行的樣式,即可得到最終的數據。
一個 sheet 中放多張表
在導出多級表頭表格的時候,我們寫表頭和數據行都是用的worksheet.addRow方法,而沒有用worksheet.column設置表格的表頭,這樣更加靈活,每一列想顯示什麼內容完全自己控制。
處理多個表格時,也可以用同樣的方法。因為每一行數據都是自己寫入的,所以不管有幾張表都沒有關係,我們關心的只有每一行的數據。
同時我們做了行和列合併算法,可以實現每一張表的每一列都能定製寬度。
可以將上面兩個例子結合起來,導出到一個sheet里,就實現了一個sheet中放多張表的需求。
結語
除了導出xlsx,ExcelJS[12]還支持導出csv格式。此外還有設置頁眉頁腳、操作視圖、添加公式、使用富文本等功能,非常的強大。
官方的文檔也很詳細,不懂的地方直接看文檔即可。
源碼地址:github.com/cachecats/excel-export-demo[13]
關於本文
作者:solocoder
https://juejin.cn/post/7071882317953761316
--- EOF ---前端技術交流群
—完—