在《用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已经被处理成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, }