关于Excel文档导出的那些事

230 阅读5分钟

介绍

关于前端实现导出excel的方法,我目前觉得不错的就是xlsx这个库和exceljs

如果你只是需要简简单单的导出一个excel就完事了,没什么其他需求,那么我推荐使用xlsx这个库,这个应该是sheetJS,如果你想个性化你导出的excel文档,比如excel文档里面的间距和表头多数据合并等等,那么可以继续看我下面关于exceljs的踩坑之旅

xlsx这个配合xlsx-style好像也是可以自定义样式的,vue的相关文档可以百度搜索到的。

下面的方法适用于React,Vue3,Vue2都支持, Angular

基础表格实现

image.png

npm install exceljs file-saver

// 代码逻辑
import ExcelJS from 'exceljs'
// 注意如果你是Angular 导入 ExcelJS 可能报警告,可以 import { 'xxx' } from exceljs 导入你需要的元素
import { saveAs } from 'file-saver'

// 这里我就是保持数据的一致性
const _pushSameList = (len: number, text: string) => {
 let item = []
 for (let i = 0; i < len; i++) {
   if (i === 0) item.push(text)
   else item.push('占位符')
 }
 return item
}

/**
 * 导出基本的表格 excel
 * @param {Array} data  二维数组数据 [value, value] 
 * @param {string} filename  导出的excel文件名
 * @param {string} title  表格标题名称 可为空
 * @param {string} desc   表格底部的描述信息 可为空
 */
export const outBaseExcel = (data: Array<string[]>, filename: string, title?: string, desc?: string) => {
  const workbook = new ExcelJS.Workbook()  // 生成工作簿
  const worksheet = workbook.addWorksheet('Sheet')  // 就是sheet 可以是多个。
  // 比如我生成5个sheet 就 使用 workbook.addWorksheet('name') 可以循环的 
  // 这个需求是对于那些需要一个xlsx文件里面包括多个表格的同学
  worksheet.properties.defaultColWidth = 25   // 设置excel文档的每一个单元格的宽度
  worksheet.properties.defaultRowHeight = 30  // 设置excel文档的每一个单元格的高度
  
  let copyData = JSON.parse(JSON.stringify(data))  // 深拷贝数据,避免污染数据,建议自己实现深拷贝还是而不是使用JSON.parse这种方法
  
  let len = copyData[0].length // 如果你需要合并表头需要知道它的长度
  
  if (title) {
    copyData.unshift(_pushSameList(len, title))
  }
  
  if (desc) {
    copyData.push(_pushSameList(len, desc))
  }
  
  
  // data(copydata) 数据格式为二维数组 [['老师', '时间', '数量'], ['张老师', '2022/10/01', '23']]
  let rows: any[] = worksheet.addRows(copyData)
  
  rows.forEach((row: any, key: number) => {
    if (title && key === 0) {
      let arr = row._cells
      let start = arr[0].address, end = arr[arr.length - 1].address
      worksheet.mergeCells(start, end)
      worksheet.getCell(start, end).value = title
    }

    if (desc && key === copyData.length - 1) {
        let arr = row._cells
        let start = arr[0].address, end = arr[arr.length - 1].address
        worksheet.mergeCells(start, end)
        worksheet.getCell(start, end).value = desc
    }

    // 关于参数小伙伴们可以去exceljs官网查看,有中文的文档
    row.font = {
      size: 12,
      name: '微软雅黑'
    }
    row.height = 30
    row.alignment = { vertical: 'middle', horizontal: 'center', wrapText: true }
  })
  
  const item = await workbook.xlsx.writeBuffer()
  // 如果需要批量导出excel同时压缩到一个文件夹里面就需要返回一个blob流,如果不需要就最近使用saveAs导出
  if (zip) {
    const blob = new Blob([item], {type: ''});
    // zip.zips.file(`${filename}.xlsx`, blob);
    return blob
  } else {
    saveAs(new Blob([item], { type: 'application/octet-stream' }), `${filename}.xlsx`)
  }
}

上面代码可以实现基本的表格导出导出的样子是这样,如果你需要标题就传入title不需要就不给,desc就是底部的合并需要就传不需要就不传

多级表头合并实现

image.png

数据类型是截图里面这样

image.png

// 这样的数据我也是无语,后端处理返回的,只能按照后端老大哥的来,tBody我不需要处理,我只需要处理tHeader生成一样的跟tBody然后合并同名的就可以实现了

/**
 * 多级复杂表格头
 * @param data   { tHeader: any[], tBody: [[]] } 数据
 * @param fileName 文件名
 * @param title  标题
 * @param desc  底部描述内容
 * @param zip  开启返回blob流
 * @returns
 */
export const outMulitLevel = async (params: Mulit) => {
  let { data: { tHeader, tBody }, title, desc, filename, zip } = params
  
  const workbook = new ExcelJS.Workbook()
  const worksheet = workbook.addWorksheet('Sheet')
  worksheet.properties.defaultColWidth = 25   // 设置excel文档的每一个单元格的宽度
  worksheet.properties.defaultRowHeight = 30  // 设置excel文档的每一个单元格的高度

  let firstCol = [], secondCol = [], top = []
    for (let item of tHeader) {
            firstCol.push({
                    name: item.name,
                    len: item.zb.length
            })
            item.zb.map((el: { id: number, name: string }) => {
                    top.push(item.name)
                    secondCol.push(el.name)
            })
    }
   secondCol.unshift('占位符')
   top.unshift('占位符')


  let arr = tBody
  let all = [...[top], ...[secondCol], ...arr]

    if (title) { all.unshift(_pushSameList(all[0].length, title)) }
    if (desc) { all.push(_pushSameList(all[0].length, desc))}
    console.warn('所有的', all)

    let rows: any = worksheet.addRows(all)
    let nums = title ? 1 : 0
	for (let [idx, row] of rows.entries()) {
		row.font = {
			size: 12,
			name: '微软雅黑'
		}
		row.height = 30
		row.alignment = { vertical: 'middle', horizontal: 'center', wrapText: true }
		if (idx === 0 && title) continue
		if (idx === nums) {
			console.log(row._cells)
			worksheet.mergeCells(`A${nums + 1}:A${nums + 2}`)
			worksheet.getCell(`A${nums + 1}:A${nums + 2}`).value = '班级名称'
			let b: any[] = []
			firstCol.map((el: any) => {
				// console.error('el.name')
				let vals = row?._cells.filter((v: any) => v.value === el.name)
				// console.log(vals[0])
				worksheet.mergeCells(`${vals[0].address}:${vals[vals.length - 1].address}`)
				worksheet.getCell(`${vals[0].address}:${vals[vals.length - 1].address}`).value = el.name
				b.push([vals[0].address, vals[vals.length - 1].address])
			})
		}
		if (idx === all.length - 1 && desc) continue
	}


	if (title) {
		let start = '', end = ''
		worksheet.getRow(1).eachCell((cell, number) => {
			if (number === 1) start = cell.address
			if (number === all[0].length) end = cell.address
			// console.warn('第一行的', cell, number)
			cell.alignment = { vertical: 'middle', horizontal: 'left' }
		})
		worksheet.mergeCells(start, end)
		worksheet.getCell(start, end).value = title
	}

	if (desc) {
		let start = '', end = ''
		// console.error('数组长度', all[0].length)
		worksheet.lastRow?.eachCell((cell, number) => {
			if (number === 1) start = cell.address
			if (number === all[0].length) end = cell.address
			cell.alignment = { vertical: 'middle', horizontal: 'left' }
		})
		worksheet.mergeCells(start, end)
		worksheet.getCell(start, end).value = desc
  }

	const item = await workbook.xlsx.writeBuffer()
	if (zip) {
		const blob = new Blob([item], { type: '' });
		return blob
	} else {
		saveAs(new Blob([item], { type: 'application/octet-stream' }), `${filename}.xlsx`)
	}

}

踩坑过程

使用exceljs会碰到问题 worksheet.properties.defaultRowHeight 这个属性可能会失效,所以我解决办法就是去循环每一个单元格然后去设置了高度,exceljs提供了专门的table方法,但我使用的时候并没有满足我的需求,所以我使用的worksheet.addRows('数据')方法去生成

其他的介绍,其实小伙伴们可以去看看exceljs文档,中文文档还是比较友好,我使用的可能并不是特别好。我整体就是把这个excel文档看成了一个一个单元格,所以我就是去按照那个格式去填充数据,然后导出。我感觉就是这样的,合并,单元格背景色,图片导到excel里面都是支持的,方法都是一样的,不过图片个人感觉还是比较丑~

压缩文件导出

原谅我的词穷,总体来说就是我需要导8个表格,生成8个excel文档,同时把这8个,放到一个文件夹里面然后把文件夹搞成压缩包下载。如果你有这种类似需求,可以看看下面的代码

npm install jszip

// 数据类型说明
// arr  二维数组
// filename 文件名
// title 是否有标题
// desc 是否有底部描述
// zip 返回blob 默认不开启
// type 导出表格类型, 如果你批量导出的表格不一样,就需要type去控制了
const downloadAll = () => {
     let downloadExcel = [
        { arr:[['数据','数据','数据数据']], filename: '文件名1', title: '', desc: '' },
        { arr:[['数据','数据','数据数据']], filename: '文件名2', title: '', desc: '' },
        { arr:[['数据','数据','数据数据']], filename: '文件名3', title: '', desc: '' },
        { arr:[['数据','数据','数据数据']], filename: '文件名4', title: '', desc: '' },
        { arr:[['数据','数据','数据数据']], filename: '文件名5', title: '', desc: '' },
        { arr:[['数据','数据','数据数据']], filename: '文件名6', title: '', desc: '' },
    ]

    import * as JsZip from 'jszip'
    let zip = new JsZip()

    const promises = downloadExcel.map(async param => await this.handleEacheFile(zip, param))
    await Promise.all(promises)

    zip.generateAsync({ type: "blob" }).then(blob => {
          saveAs(blob, `${文件名称}.zip`)
    })
}


const handleEacheFile = (zip:JsZip, param: any) => {
  // 使用对应的生成表格方法,获取参数里面的数据,文件名,是否有标题,等参数
  let { arr, filename, title, desc } = param || {}
  let item = { data: arr, filename, title, desc, zip: true }
 
  let blob = outBaseExcel(item)
  // 这里去根据type控制,如果都是一样的表格就不需要控制
  //  let blob: any = null
  // blob = outBaseExcel(item)  // outBaseExcel 方法已经实现, 传入参数就可返回blob流
  
  zip.file(`${fileName}.xlsx`, blob)
}

如果不是老大要求要导出的excel好看一点,那xlsx完全满足我的需求,直接通过dom的id去导出,简直是不要太方便

总结

因为我的代码大部分是我工作导出的表格样式总结的,但总体效果还是不错,如果有其他问题或者是更方便快捷的方法,欢迎小伙伴们评论区交流讨论。

如果碰到其他表格样式导出问题,可以评论留言,非常乐意探讨实现。基本上需求都是支持的,还有就是关于方法的封装问题了,在代码里面其实也是可以看见有很多共同的地方!

谢谢观看!记录exceljs导出excel文档方法,后续优化文章。

国庆节放假完,回来的第一天,真难受哦 ~ 🥱