胖蔡叨叨叨
你听我说

用xlsx-style玩转Excel导出——拿走即用的Excel导出方法封装

《用xlsx-style玩转Excel导出——像手动操作Excel表格一样用JS操作Excel》一文中,我们详细介绍了xlsx-style插件相关的概念、属性和方法,今天,我们将利用xlsx-style插件封装出一个通用的、可自定义的Excel导出方法,以期能够满足日常Web项目的导出需求。

一、常见自定义需求

在正式开始封装之前,我们先梳理一下,我们Excel导出一般都有那些自定义需求。

  1. 导出的Excel文件名自定义
  2. 支持单sheet、多sheet导出
  3. sheet名自定义
  4. 表头内容自定义
  5. 表格内容自定义
  6. 表格样式自定义
  7. 支持合并单元格
  8. 支持冻结单元格

二、抽象导出方法名及参数

根据上述自定义需求,我们可以初步将Excel导出方法及参数定义如下:

/**
 * 自定义导出excel
 * @param { 导出的数据 } exportData
 * @param { 工作簿配置 } workBookConfig
 * @param { 导出文件名 } fileName
 */
 
 function exportExcel(exportData, workBookConfig = defaultWorkBook, fileName = '未命名') {
 
 }

三、参数介绍及处理

(一)exportData

参数exportData是一个对象数组,可以满足单sheet、多sheet的导出,同时可以配置每个sheet的表头、表格内容、样式等,格式如下:

[
  {
    header: [], // 表头
    dataSource: [], // 数据源
    workSheetConfig: {}, // 工作表配置
    cellConfig: {}, // 单元格配置
    sheetName: ''// sheet名
  }
]
  • header

header是一个对象数组,用来设置表头,格式如下:

[
   { title: '分站', dataIndex: 'subName', width: 140 }, 
   { title: '企业总数', dataIndex: 'enterpriseCount', width: 140 },
]

接收到header后,需将header内容处理成表格语言,具体如下:

const _header = header.map((item, i) =>
    Object.assign({}, {
      key: item.dataIndex,
      title: item.title,
      // 定位单元格
      position: getCharCol(i) + 1, 
      // 设置表头样式
      s: data.cellConfig && data.cellConfig.headerStyle ? data.cellConfig.headerStyle : defaultCellStyle.headerStyle,
    })
  ).reduce((prev, next) =>
    Object.assign({}, prev, {
      [next.position]: { v: next.title, key: next.key,s: next.s },
    }), {},
 )

header已经被处理成worksheet Object,而其中很重要一步,就是定位单元格,而我们Excel是用字母+数字的形式定位单元格的,所以我们可以通过如下代码定位单元格。

position: getCharCol(i) + 1

其中,getCharCol()方法是用来生成单元格对应的字母的,代码如下:

/**
 * 生成ASCll值 从A开始
 * @param {*} n
 */
function getCharCol(n) {
  if( n > 25) {
    let s = ''
    let m = 0
   while (n > 0) {
    m = n % 26 + 1
    s = String.fromCharCode(m + 64) + s
    n = (n - m) / 26
   }
    return s
  }
  return String.fromCharCode(65 + n) 
}

  • cellConfig

cellConfig是一个对象,用来配置单元格的样式,格式如下:

{
  headerStyle: { // 表头区域样式配置
    border: {},
    font: {}, 
    alignment: {}, 
    fill: {},
  },
  dataStyle: { // 内容样式配置
    border: {},
    font: {}, 
    alignment: {},
    fill: {}, 
  } 
}

  • dataSource

dataSource是一个对象数组,用来表示表格内容,格式如下:

[
   { subName:'合肥' }, 
   { enterpriseCount: '100' },
]

同样,接收到dataSource后,需将dataSource内容处理成表格语言,具体如下:

let _data = {};

 // 处理表格内容及样式
dataSource.forEach((item, i) => {
  header.forEach((obj, index) => {
    const key = getCharCol(index) + (i + 2);
    const key_t = obj.dataIndex;
    _data[key] = {
      v: item[key_t],
      s: cellConfig.dataStyle? cellConfig.dataStyle : defaultCellStyle.dataStyle
    }
  })
});


  • workSheetConfig

workSheetConfig是一个对象,用来配置工作表的合并单元格、冻结单元格等功能,格式如下:

{
  merges: [ // 合并单元格
    { s: { c: 1, r: 1 },e: { c: 3, r: 3 } }
  ],
  freeze:{ // 冻结单元格
    xSplit: '1',
    ySplit: '1',
    topLeftCell: 'B2',
    state: 'frozen',
  } 
}

(二)workBookConfig

workBookConfig是一个对象,用来配置工作簿的类型等,格式如下:

{
  bookType: 'xlsx', // 工作簿的类型
  bookSST: false,
  type: 'binary', // 输出数据类型
}

四、生成workbook object

const wb = {SheetNames: [], Sheets: {}}

exportData.forEach( (data, index) => { 

  const output = Object.assign({}, _headers, _data);
  const outputPos = Object.keys(output); 
  
  // 设置单元格宽度
  const colWidth = data.header.map( item => { wpx: item.width || 60 })
  
  const merges = data.workSheetConfig && data.workSheetConfig.merges

  const freeze = data.workSheetConfig && data.workSheetConfig.freeze
  
  // 处理sheet名
  wb.SheetNames[index] = data.sheetName ? data.sheetName : 'Sheet' + (index + 1)
  
  // 处理sheet数据
  wb.Sheets[wb.SheetNames[index]] = Object.assign({}, 
     output, //导出的内容
     {
        '!ref': `${outputPos[0]}:${outputPos[outputPos.length - 1]}`,
        '!cols': [...colWidth],
        '!merges': merges ? [...merges] : undefined,
        '!freeze': freeze ? [...freeze] : undefined,
    }
  )  

})

五、new Blob转换成二进制类型的对象

 const tmpDown = new Blob(
    [ s2ab( XLSX.write(wb, workBookConfig))],
    { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'}
 )
  
// 字符串转字符流---转化为二进制的数据流
function s2ab(s) {
  if (typeof ArrayBuffer !== 'undefined') {
    const buf = new ArrayBuffer(s.length);
    const view = new Uint8Array(buf);
    for (let i = 0; i !== s.length; ++i) view[i] = s.charCodeAt(i) & 0xff;
    return buf;
  } else {
    const buf = new Array(s.length);
    for (let i = 0; i !== s.length; ++i) buf[i] = s.charCodeAt(i) & 0xff;
    return buf;
  }
}

六、下载Excel

downExcel(tmpDown, `${fileName + '.'}${workBookConfig.bookType == 'biff2' ? 'xls' : workBookConfig.bookType}`)

function downExcel(obj, fileName) {
  const a_node = document.createElement('a');
  a_node.download = fileName;

  // 兼容ie
  if ('msSaveOrOpenBlob' in navigator) {
    window.navigator.msSaveOrOpenBlob(obj, fileName);
  } else {
    // 新的对象URL指向执行的File对象或者是Blob对象.
    a_node.href = URL.createObjectURL(obj);
  }
  a_node.click();
  setTimeout(() => {
    URL.revokeObjectURL(obj);
  }, 100);
}

七、默认配置

// 默认工作簿配置

const defaultWorkBook = {
  bookType: 'xlsx',
  bookSST: false,
  type: 'binary',
}

// 默认样式配置

const borderAll = {  //单元格外侧框线
    top: {
      style: 'thin',
    },
    bottom: {
      style: 'thin'
    },
    left: {
      style: 'thin'
    },
    right: {
      style: 'thin'
    }
  };

const defaultCellStyle = {
  headerStyle: { // 表头区域样式配置
    border: borderAll,
    font: { name: '宋体', sz: 11, italic: false, underline: false, bold: true }, 
    alignment: { vertical: 'center', horizontal: 'center'}, 
    fill: { fgColor: { rgb: 'FFFFFF' } },
  },
  dataStyle: { // 内容样式配置
    border: borderAll,
    font: { name: '宋体', sz: 11, italic: false, underline: false }, 
    alignment: { vertical: 'center', horizontal: 'left', wrapText: true},
    fill: { fgColor: { rgb: 'FFFFFF' } }, 
  } 
}

八、完整代码

import XLSX from 'xlsx-style'

// 默认工作簿配置

const defaultWorkBook = {
  bookType: 'xlsx',
  bookSST: false,
  type: 'binary',

}

// 默认样式配置

const borderAll = { 
  top: {
    style: 'thin',
  },
  bottom: {
    style: 'thin',
  },
  left: {
    style: 'thin',
  },
  right: {
    style: 'thin',
  },
}

const defaultCellStyle = {
  // 表头区域样式配置
  headerStyle: { 
    border: borderAll,
    font: { name: '宋体', sz: 11, italic: false, underline: false, bold: true },
    alignment: { vertical: 'center', horizontal: 'center' },
    fill: { fgColor: { rgb: 'FFFFFF' } },
  },
  // 内容区域样式配置
  dataStyle: { 
    border: borderAll,
    font: { name: '宋体', sz: 11, italic: false, underline: false },
    alignment: { vertical: 'center', horizontal: 'left', wrapText: true },
    fill: { fgColor: { rgb: 'FFFFFF' } },
  },
}

function exportExcel(exportData, workBookConfig = defaultWorkBook, fileName = '未命名') {

  if (!(exportData && exportData.length)) {
    return
  }
  
  // 定义工作簿对象
  const wb = { SheetNames: [], Sheets: {} }

  exportData.forEach((data, index) => {
  
    // 处理sheet表头
    const _header = data.header.map((item, i) =>
      Object.assign({}, {
        key: item.dataIndex,
        title: item.title,
        // 定位单元格
        position: getCharCol(i) + 1, 
        // 设置表头样式
        s: data.cellConfig && data.cellConfig.headerStyle ? data.cellConfig.headerStyle : defaultCellStyle.headerStyle,
      })
    ).reduce((prev, next) =>
      Object.assign({}, prev, {
        [next.position]: { v: next.title, key: next.key, s: next.s },
      }), {}
    )

    // 处理sheet内容
    const _data = {}
    data.dataSource.forEach((item, i) => {
      data.header.forEach((obj, index) => {
        const key = getCharCol(index) + (i + 2)
        const key_t = obj.dataIndex
        _data[key] = {
          v: item[key_t],
          s: data.cellConfig && data.cellConfig.dataStyle ? data.cellConfig.dataStyle : defaultCellStyle.dataStyle,
        }
      })
    })

    const output = Object.assign({}, _header, _data)
    const outputPos = Object.keys(output)


    // 设置单元格宽度
    const colWidth = data.header.map(item => { return { wpx: item.width || 80 } })

    const merges = data.workSheetConfig && data.workSheetConfig.merges

    const freeze = data.workSheetConfig && data.workSheetConfig.freeze

    // 处理sheet名
    
    wb.SheetNames[index] = data.sheetName ? data.sheetName : 'Sheet' + (index + 1)
    
    // 处理sheet数据
    
    wb.Sheets[wb.SheetNames[index]] = Object.assign({},
      output, // 导出的内容
      {
        '!ref': `${outputPos[0]}:${outputPos[outputPos.length - 1]}`,
        '!cols': [...colWidth],
        '!merges': merges ? [...merges] : undefined,
        '!freeze': freeze ? [...freeze] : undefined,
      }
    )
  })
  
  // 转成二进制对象
  const tmpDown = new Blob(
    [s2ab(XLSX.write(wb, workBookConfig))],
    { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' }
  )
  
  // 下载表格
  downExcel(tmpDown, `${fileName + '.'}${workBookConfig.bookType === 'biff2' ? 'xls' : workBookConfig.bookType}`)
}

/**
 * 生成ASCll值 从A开始
 * @param {*} n
 */
function getCharCol(n) {
  if (n > 25) {
    let s = ''
    let m = 0
    while (n > 0) {
      m = n % 26 + 1
      s = String.fromCharCode(m + 64) + s
      n = (n - m) / 26
    }
    return s
  }
  return String.fromCharCode(65 + n)
}

// 字符串转字符流---转化为二进制的数据流
function s2ab(s) {
  if (typeof ArrayBuffer !== 'undefined') {
    const buf = new ArrayBuffer(s.length)
    const view = new Uint8Array(buf)
    for (let i = 0; i !== s.length; ++i) { view[i] = s.charCodeAt(i) & 0xff }
    return buf
  } else {
    const buf = new Array(s.length)
    for (let i = 0; i !== s.length; ++i) { buf[i] = s.charCodeAt(i) & 0xff }
    return buf
  }
}

function downExcel(obj, fileName) {
  const a_node = document.createElement('a')
  a_node.download = fileName

  // 兼容ie
  if ('msSaveOrOpenBlob' in navigator) {
    window.navigator.msSaveOrOpenBlob(obj, fileName)
  } else {
    // 新的对象URL指向执行的File对象或者是Blob对象.
    a_node.href = URL.createObjectURL(obj)
  }
  a_node.click()
  setTimeout(() => {
    URL.revokeObjectURL(obj)
  }, 100)
}

export {
  exportExcel,
}
赞(0) 打赏
转载请附上原文出处链接:胖蔡叨叨叨 » 用xlsx-style玩转Excel导出——拿走即用的Excel导出方法封装
分享到: 更多 (0)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

微信扫一扫打赏