exceljs导出大数据到excel文件的使用笔记

功能主要是使用excejs实现的,第一需要安装exceljs(这里用的4.4.0,其他版本貌似有些函数不支持)

npm install exceljs@4.4.0 -S

附上exceljs 的官方文档 链接:

中文:http://www.npmdoc.org/exceljszhongwenwendangexceljs-jszhongwenjiaochengjiexi.html
英文: https://github.com/exceljs/exceljs#streaming-xlsx

由于中文的是直译,所以有些地方, 参数等的需要看英文的才能确认。

第一写个把目录下所有文件都合并成一个文件的模块,如下:

import * as fs from "fs"
import * as path from "path"

const basePath = path.resolve(__dirname, "/files/")

// 合并文件的方法, 把目录 dirs里的所有文件合并成一个
export function mergeFile(dirs: string, name: string = "") {
  const files = fs.readdirSync(path.resolve(__dirname, dirs))
  if (files.length === 0) return ""
  name = name || files[0].substr(files[0].lastIndexOf( / ) + 1).split("_")[0]
  const fileUrl = path.resolve(basePath, name +  .xls )
  const wStream = fs.createWriteStream(fileUrl)
  if (addFileToStream(files, dirs, wStream)) {
    return fileUrl
  } else {
    return ""
  }
}

function addFileToStream(files: string[], baseDir: string, wStream) { // 递归合并的方法
  if(files.length === 0) {
    // 合并完成
    wStream.end()
    return true
  }
  const dir = path.resolve(__dirname, baseDir, files.shift())

  const rStream = fs.createReadStream(dir)
  rStream.pipe(wStream, {
    end: false
  })
  rStream.on("end", () => {
    addFileToStream(files, baseDir, wStream)
  })
  rStream.on("error", err => {
    // 此处错误信息应该写入到log日志中
    console.log(err)
    return false
  })
}

然后是基类,这里的基类是为了处理大文件的,直接影响到文件,且不占用内存。所以是流式.这里包含了大部分的用法和公共方法

// 流式创建excel基类
import {
  exchangeWordToNum,
  exchangeColor,
} from  utils 
const Excel = require( exceljs );

interface ExcelOptions {
  sheetName?: string, // sheet名
  useStyles?: boolean, // 是否启用样式, 会增加性能开销
  useSharedStrings?: boolean, // 是否启用共享字符串,可以优化性能和空间
  creator?: string, // 制作人
  rowHeight?: number, // 默认行高
  colWidth?: number, // 默认列宽
  tabColor?: string, // 选项卡颜色
}

export default class ExcelBase {

  fileName: string; //下载得文件路径

  workbook: any;

  sheetName: string =  sheet-1 ;

  filePath: string;

  options: ExcelOptions = {};

  isCommit: boolean = false; // 文件是否生成完成

  constructor(fileName: string =   , options: ExcelOptions = {}) {
    // 真实存储文件得路径
    fileName = (fileName ? fileName +  -  :   ) + Date.now() +  .xlsx 
    this.filePath = fileName
   
    this.fileName = baseFileDir + fileName

    this.options = options

    const opt = {
      filename: this.filePath,
      useStyles: options.useStyles || true,
      useSharedStrings: options.useSharedStrings || true
    };
    const workbook = new Excel.stream.xlsx.WorkbookWriter(opt);

    workbook.creator = options.creator ||  Windows ;
    workbook.lastModifiedBy = options.creator ||  Windows ;
    workbook.created = new Date();

    this.sheetName = options.sheetName || this.sheetName

    // sheet 属性选项
    const sheetOptions = {
      properties:{
        tabColor: options.tabColor ? exchangeColor(options.tabColor) : "ffffff00", // 选项卡颜色
        defaultRowHeight: options.rowHeight || 22, // 默认行高
        defaultColWidth: options.colWidth || 20, // 默认列宽
        // outlineLevelCol:0, // 列 大纲级别
        // outlineLevelRow: 0, // 行 大纲级别
        // dyDescent: 55, // 不知道啥意思
      }
    }
    workbook.addWorksheet(this.sheetName, sheetOptions)
    this.workbook = workbook
  }

  get currentSheet() {
    return this.workbook.getWorksheet(this.sheetName)
  }

  get allSheets() {
    return this.workbook.worksheets
  }

  /**
   * 添加sheet
   * @param sheetName 
   */
  addSheet(sheetName =   ) {
    if (!sheetName) {
      const length = this.allSheets.length
      sheetName =  sheet-  + length
    }
     // sheet 属性选项
     const sheetOptions = {
      properties:{
        tabColor: this.options.tabColor ? exchangeColor(this.options.tabColor) : "ffffff00", // 选项卡颜色
        defaultRowHeight: this.options.rowHeight || 22, // 默认行高
        defaultColWidth: this.options.colWidth || 20, // 默认列宽
      }
    }
    this.workbook.addSheet(sheetName, sheetOptions)
    this.sheetName = sheetName
  }

  /**
   * 删除sheet
   */
  delSheet(sheetName) {
    if (!sheetName) return false
    const sheet = this.workbook.getSheet(sheetName)
    return this.workbook.removeWorksheet(sheet.id)
  }

  /**
   * 根据名字获取sheet
   * @param sheetName 
   * @returns 
   */
  getSheet(sheetName =   ) {
    return this.workbook.getWorksheet(sheetName) || this.currentSheet
  }

  /**
   * 同步添加行数据,对象也转为数组直接添加,【直接写入文件】
   */
  addRowData(data, sheetName =   ) {
    sheetName && (this.sheetName = sheetName)
    if(data[0] && data[0] instanceof Array) {
      data.forEach(cur => {
        this.currentSheet.addRow(cur)
      })
    } else {
      data.forEach(cur => {
        const arr = Object.values(cur)
        this.currentSheet.addRow(arr)
      })
    }
  }

  /**
   * 删除行
   * @param index 从哪行开始删除(注意,第一行是1)
   * @param count 删除几行
   */
  delRows(index: number, count: number = 1, sheetName: string =   ) {
    const sheet = this.getSheet(sheetName)
    sheet.spliceRows(index, count)
  }

  /**
   * 删除列
   * @param index 从哪行开始删除(注意,第一行是1)
   * @param count 删除几行
   */
  delCols(index: number, count: number = 1, sheetName: string =   ) {
    const sheet = this.getSheet(sheetName)
    sheet.spliceColumns(index, count)
  }

  /**
   * 添加行
   * @param index 从哪行开始添加(注意,第一行是1)
   * @param data 二维数组;添加几行data.length就是几
   */
  addRows(index: number, data: any[] = [], sheetName: string =   ) {
    const sheet = this.getSheet(sheetName)
    if (data[0] && data[0] instanceof Array) {
      sheet.spliceRows(index, 0, ...data)
    } else {
      sheet.spliceRows(index, 0, data)
    }
  }

  /**
   * 添加列
   * @param index 从哪列开始添加(注意,第一行是1)
   * @param data 二维数组;添加几列data.length就是几
   */
  addCols(index: number, data: any[] = [], sheetName: string =   ) {
    const sheet = this.getSheet(sheetName)
    if (data[0] && data[0] instanceof Array) {
      sheet.spliceCols(index, 0, ...data)
    } else {
      sheet.spliceCols(index, 0, data)
    }
  }

  /**
   * 删除cell
   * @param A1 A1表达式, 支持范围(A1:B5)
   * @param sheetName 
   */
  delCells(A1: string, sheetName: string =   ) {
    const sheet = this.getSheet(sheetName)
    const getAReg = /([a-z]+?)d/i
    const cellInfo: Array<{row: number, cell1: number, cell2: number }> = []
    
    const A1s = A1.split( : )
    if (A1s.some(item => {
      const res = getAReg.exec(item)
      if (!res) return true
      cellInfo.push({
        row: exchangeWordToNum(res[1]) + 1,
        cell1: Number(A1.replace(res[0],   )),
        cell2: Number(A1.replace(res[0],   ))
      })
    })) return false
    if (cellInfo[1] && cellInfo[1].cell2 !== cellInfo[0].cell1) {
      cellInfo[0].cell2 = cellInfo[1].cell2
      cellInfo[1].cell1 = cellInfo[0].cell1
    }

    cellInfo.forEach(item => {
      const row = sheet.getRow(item.row)
      row.splice(item.cell1, item.cell2 - item.cell1)
    })
  }

  /**
   * 根据A1表达式,设置具体某个单元格的样式
   * @param A1 
   */
  setStyleByA1(A1: string, style: {[key:string]: any}) {
    const cell = this.currentSheet.getCell(A1)
    // 设置颜色
    style.fill && (cell.fill = {
      type: style.fill.type ||  pattern ,
      pattern: style.fill.pattern ||  solid ,
      fgColor: { argb: exchangeColor(style.fill.color) ||  FF222222  }, // 注意,这里的颜色是 argb
    });
    // 设置border
    style.border && (cell.border = {
      // style 可选值  thin  thick  double  dotted 等
      top: {style: style.border.style ||  thin , color: {argb: exchangeColor(style.border.color) ||  FF333333 }},
      left: {style: style.border.style ||  thin , color: {argb: exchangeColor(style.border.color) ||  FF333333 }},
      bottom: {style: style.border.style ||  thin , color: {argb: exchangeColor(style.border.color) ||  FF333333 }},
      right: {style: style.border.style ||  thin , color: {argb: exchangeColor(style.border.color) ||  FF333333 }}
    })
    // 设置字体字号
    style.font && (cell.font = { 
      name: style.font.name ||  宋体 ,
      family: 4, // 字体样式,西文样式或者等宽样式等。4标识等宽,适合表格,或者1 自动适配
      size: style.font.size || 14,
      color: { argb: exchangeColor(style.font.color) ||  FF222222 },
      underline: style.font.underline ||  none ,
      italic: style.cellStyle.italic ||  none ,
      bold: style.font.bold || false
    })
  }

  /**
   * 根据index,设置一行的样式
   * @param index
   */
  setRowStyleByIndex(index: number, style: {[key:string]: any}) {
    const row = this.currentSheet.getRow(index)
    // 设置颜色
    style.fill && (row.fill = {
      type: style.fill.type ||  pattern ,
      pattern: style.fill.pattern ||  solid ,
      fgColor: { argb: exchangeColor(style.fill.color) ||  FF222222  }, // 注意,这里的颜色是 argb
    });
    // 设置border
    style.border && (row.border = {
      // style 可选值  thin  thick  double  dotted 等
      top: {style: style.border.style ||  thin , color: {argb: exchangeColor(style.border.color) ||  FF333333 }},
      left: {style: style.border.style ||  thin , color: {argb: exchangeColor(style.border.color) ||  FF333333 }},
      bottom: {style: style.border.style ||  thin , color: {argb: exchangeColor(style.border.color) ||  FF333333 }},
      right: {style: style.border.style ||  thin , color: {argb: exchangeColor(style.border.color) ||  FF333333 }}
    })
    // 设置字体字号
    style.font && (row.font = { 
      name: style.font.name ||  宋体 ,
      family: 4, // 字体样式,西文样式或者等宽样式等。4标识等宽,适合表格,或者1 自动适配
      size: style.font.size || 14,
      color: { argb: exchangeColor(style.font.color) ||  FF222222 },
      underline: style.font.underline ||  none ,
      italic: style.cellStyle.italic ||  none ,
      bold: style.font.bold || false
    })
  }

  /**
   * 根据A1 设置单元格值
   */
  setValueByA1(A1: string, value: string|number|Date) {
    this.currentSheet.getCell(A1).value = value
  }

  /**
   * 设置index这一行的数据
   * @param index 
   * @param data 值数组
   */
  setRowValuesByIndex(index: number, data: Array<string|number|Date>) {
    this.currentSheet.getRow(index).values = data
  }

  /**
   * 传入列索引或列字母,设置列值
   * @param letter 
   * @param data 值数组
   */
  setColValuesByIndex(letter: string | number, data: Array<string|number|Date>) {
    const index = typeof letter ===  number  ? letter:exchangeWordToNum(letter)
    this.currentSheet.getColumn(index).values = data
  }

  /**
   * 所有操作写入到文件,【并返回文件路径】  用于下载
   */
  writeToFile() {
    // 最后修改时间
    this.workbook.modified = new Date()
    // 所有更改提交
    this.workbook.commit()
    this.isCommit = true
    // 返回下载路径
    return this.fileName
  }

}

一些问题点

** 这里要注意 流式的写入文件就是为了减少大文件操作内存溢出的问题,所以添加完row的样式和值之后,要row.commit(),或者 worksheet.commit() 才会释放内存 **

** 样式问题:如果你给每部分赋值的样式是同一个对象,那么他们用的就是同一个样式文件,即你有某个部分单独改过这个样式,那么这个修改会作用于所有使用这个样式文件的地方,这个尤其需要注意。为了避免大多数问题deepclone是个不错的选择 **

** 合并单元格设置border问题: 如果你合并了几个单元格,然后再对这个合并单元格设置border的时候,这个border将只对这个被合并的区域中第一个单元格生效。如果想要对整个合并单元格生效,那么还是要把合并前的每个cell都设置border才行 **

© 版权声明
THE END
如果内容对您有所帮助,就支持一下吧!
点赞0 分享
鹰的头像 - 鹿快
评论 抢沙发

请登录后发表评论

    暂无评论内容