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项目的导出需求。
一、常见自定义需求
在正式开始封装之前,我们先梳理一下,我们Excel导出一般都有那些自定义需求。
- 导出的Excel文件名自定义
- 支持单sheet、多sheet导出
- sheet名自定义
- 表头内容自定义
- 表格内容自定义
- 表格样式自定义
- 支持合并单元格
- 支持冻结单元格
二、抽象导出方法名及参数
根据上述自定义需求,我们可以初步将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的打印格式如下:
从上述打印可以看出,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,
}