---
title: 用xlsx-style玩转Excel导出——拿走即用的Excel导出方法封装          
date: 2022/5/24 09:15:20
tags:                    
- 前端
- xlsx-style
- JavaScript             
author: 李燕         
---

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

 <!-- more -->
 
**一、常见自定义需求**
 
 在正式开始封装之前，我们先梳理一下，我们Excel导出一般都有那些自定义需求。
 
1. 导出的Excel文件名自定义
2. 支持单sheet、多sheet导出
3. sheet名自定义
4. 表头内容自定义
5. 表格内容自定义
6. 表格样式自定义
7. 支持合并单元格
8. 支持冻结单元格

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

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

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

**三、参数介绍及处理**

（一）exportData

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

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

+ header

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

```js
[
   { title: '分站', dataIndex: 'subName', width: 140 }, 
   { title: '企业总数', dataIndex: 'enterpriseCount', width: 140 },
]
```
接收到header后，需将header内容处理成表格语言，具体如下：


```js
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是用字母+数字的形式定位单元格的，所以我们可以通过如下代码定位单元格。

```js
position: getCharCol(i) + 1
```
其中，getCharCol()方法是用来生成单元格对应的字母的，代码如下：
```js
/**
 * 生成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是一个对象，用来配置单元格的样式，格式如下：

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

```

+ dataSource

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

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

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


```js
  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是一个对象，用来配置工作表的合并单元格、冻结单元格等功能，格式如下：

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

（二）workBookConfig

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

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

**四、生成workbook object**

```js
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转换成二进制类型的对象**

```js
 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**

```js
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);
}
```

**七、默认配置**

```js
// 默认工作簿配置

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' } }, 
  } 
}

```

**八、完整代码**

```js

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,
}

```
