1. 项目背景与核心需求
最近在重构一个后台管理系统时,遇到了一个典型的需求:前端需要将表格数据导出为Excel文件,并且要求保留样式(如单元格颜色、字体加粗、边框等)。这个功能在数据报表、统计分析和数据交接等场景中非常常见。经过技术调研,最终选择了SheetJS的Pro版本(即sheetjs-style.js)来实现这个需求。
为什么选择SheetJS而不是其他库?主要原因有三点:
- 社区活跃度高,维护良好,长期来看更可靠
- 功能全面,支持xlsx、xls等多种格式
- Pro版本支持样式设置,能满足我们的设计需求
2. 环境准备与基础配置
2.1 安装依赖
首先需要在Vue项目中安装SheetJS的核心库和Pro版本:
npm install xlsx npm install xlsx-style这里有个坑需要注意:官方仓库的xlsx-style已经不再维护,我们需要使用社区维护的fork版本。在package.json中应该这样指定:
"dependencies": { "xlsx": "^0.18.5", "xlsx-style": "git+https://github.com/protobi/js-xlsx.git" }2.2 全局引入配置
为了在整个项目中方便使用,我们在main.js中全局引入并挂载到Vue原型上:
import XLSX from 'xlsx' import XLSXStyle from 'xlsx-style' Vue.prototype.$xlsx = XLSX Vue.prototype.$xlsxStyle = XLSXStyle3. 核心实现逻辑详解
3.1 数据结构准备
假设我们有一个用户数据表格需要导出,数据结构如下:
const userData = [ { id: 1, name: '张三', age: 28, department: '研发部', status: '在职' }, { id: 2, name: '李四', age: 32, department: '市场部', status: '离职' }, // 更多数据... ]3.2 创建工作簿和工作表
// 创建工作簿 const wb = this.$xlsx.utils.book_new() // 将JSON数据转换为工作表 const ws = this.$xlsx.utils.json_to_sheet(userData) // 设置表头样式 ws['A1'].s = { font: { bold: true, color: { rgb: "FFFFFF" } }, fill: { fgColor: { rgb: "4472C4" } }, alignment: { horizontal: "center" } } // 将工作表添加到工作簿 this.$xlsx.utils.book_append_sheet(wb, ws, "用户数据")3.3 样式设置详解
SheetJS的样式设置是通过单元格的s属性实现的,支持以下常用样式:
{ font: { // 字体样式 name: 'Arial', sz: 12, bold: true, italic: false, underline: true, color: { rgb: "FF0000" } // 红色 }, fill: { // 填充样式 fgColor: { rgb: "FFFF00" }, // 黄色背景 patternType: "solid" }, border: { // 边框样式 top: { style: "thin", color: { rgb: "000000" } }, bottom: { style: "thin", color: { rgb: "000000" } }, left: { style: "thin", color: { rgb: "000000" } }, right: { style: "thin", color: { rgb: "000000" } } }, alignment: { // 对齐方式 horizontal: "center", vertical: "center", wrapText: true }, numFmt: "0.00%" // 数字格式 }4. 完整导出功能实现
4.1 导出函数封装
下面是一个完整的导出函数实现:
exportExcel() { // 1. 准备数据 const data = this.getTableData() // 获取表格数据 // 2. 创建工作簿 const wb = this.$xlsx.utils.book_new() // 3. 转换数据为工作表 const ws = this.$xlsx.utils.json_to_sheet(data) // 4. 设置表头样式 const headerStyle = { font: { bold: true, color: { rgb: "FFFFFF" } }, fill: { fgColor: { rgb: "4472C4" } }, alignment: { horizontal: "center" } } // 获取表头范围 (假设有5列) for(let i = 0; i < 5; i++) { const cell = this.$xlsx.utils.encode_cell({ c: i, r: 0 }) ws[cell].s = headerStyle } // 5. 设置数据行样式 Object.keys(ws).forEach(key => { if(key !== '!ref' && key !== '!cols' && key !== '!rows') { if(!ws[key].s) ws[key].s = {} ws[key].s.alignment = { vertical: "center" } } }) // 6. 设置列宽 ws['!cols'] = [ { width: 10 }, // 第一列 { width: 20 }, // 第二列 { width: 15 }, // 第三列 { width: 25 }, // 第四列 { width: 15 } // 第五列 ] // 7. 添加到工作簿并导出 this.$xlsx.utils.book_append_sheet(wb, ws, "Sheet1") this.$xlsxStyle.writeFile(wb, "用户数据.xlsx") }4.2 性能优化技巧
当数据量较大时(超过1000行),导出可能会变慢。可以采用以下优化方案:
- 分块处理:将大数据集分成多个小块处理
- 简化样式:只对必要的单元格应用复杂样式
- 使用Web Worker:将导出操作放到后台线程
// Web Worker示例 const worker = new Worker('./excelExport.worker.js') worker.postMessage({ data: largeDataSet, fileName: '大数据导出.xlsx' }) worker.onmessage = (e) => { if(e.data.success) { this.$message.success('导出成功') } else { this.$message.error('导出失败') } }5. 常见问题与解决方案
5.1 样式不生效问题
问题现象:设置了样式但导出的Excel文件没有显示样式。
排查步骤:
- 确认使用的是xlsx-style而不是xlsx
- 检查样式对象格式是否正确
- 确保单元格引用正确(如A1、B2等)
解决方案:
// 错误示例 ws['A1'].style = { font: { bold: true } } // 错误属性名 // 正确示例 ws['A1'].s = { font: { bold: true } } // 正确属性名5.2 中文乱码问题
问题现象:导出的Excel文件中中文显示为乱码。
解决方案:
- 确保HTML页面使用UTF-8编码
- 在Blob对象中指定编码:
const blob = new Blob( [this.$xlsxStyle.write(wb, { bookType: 'xlsx', type: 'binary' })], { type: 'application/octet-stream;charset=utf-8' } )5.3 大文件导出崩溃
问题现象:当数据量很大时,浏览器可能会卡死或崩溃。
优化方案:
- 使用分页加载数据
- 采用流式导出
- 显示导出进度
// 分页导出示例 async function exportLargeData(data, chunkSize = 1000) { const wb = this.$xlsx.utils.book_new() let page = 1 for(let i = 0; i < data.length; i += chunkSize) { const chunk = data.slice(i, i + chunkSize) const ws = this.$xlsx.utils.json_to_sheet(chunk) this.$xlsx.utils.book_append_sheet(wb, ws, `Page ${page++}`) // 更新进度 this.exportProgress = Math.min(100, Math.round(i / data.length * 100)) await new Promise(resolve => setTimeout(resolve, 0)) } this.$xlsxStyle.writeFile(wb, '大数据导出.xlsx') }6. 高级功能实现
6.1 合并单元格
实现类似"跨列居中"的效果:
// 合并A1到E1单元格 ws['!merges'] = [ { s: { r: 0, c: 0 }, e: { r: 0, c: 4 } } ] // 设置合并后单元格的样式 ws['A1'].s = { font: { bold: true, sz: 16 }, alignment: { horizontal: "center" } }6.2 条件格式
虽然SheetJS不支持直接设置Excel的条件格式,但我们可以通过编程方式实现类似效果:
data.forEach((item, rowIndex) => { Object.keys(item).forEach((key, colIndex) => { const cellRef = this.$xlsx.utils.encode_cell({ r: rowIndex + 1, // +1因为第一行是表头 c: colIndex }) // 根据状态设置不同颜色 if(key === 'status') { ws[cellRef].s = { fill: { fgColor: { rgb: item.status === '在职' ? 'C6EFCE' : 'FFC7CE' } }, font: { color: { rgb: item.status === '在职' ? '006100' : '9C0006' } } } } }) })6.3 多Sheet导出
一个Excel文件可以包含多个工作表:
// 第一个工作表 const ws1 = this.$xlsx.utils.json_to_sheet(data1) this.$xlsx.utils.book_append_sheet(wb, ws1, "用户数据") // 第二个工作表 const ws2 = this.$xlsx.utils.json_to_sheet(data2) this.$xlsx.utils.book_append_sheet(wb, ws2, "部门统计") // 第三个工作表(空表带样式) const ws3 = this.$xlsx.utils.aoa_to_sheet([]) ws3['A1'] = { v: "统计报表", t: 's', s: headerStyle } this.$xlsx.utils.book_append_sheet(wb, ws3, "报表")7. 实际项目中的经验总结
在多个项目中实现Excel导出功能后,我总结了以下几点经验:
- 样式统一管理:将常用的样式提取为常量,便于维护
// styles.js export const HEADER_STYLE = { font: { bold: true, color: { rgb: "FFFFFF" }, sz: 12 }, fill: { fgColor: { rgb: "4472C4" } }, alignment: { horizontal: "center", vertical: "center" } } export const WARNING_STYLE = { font: { color: { rgb: "9C0006" } }, fill: { fgColor: { rgb: "FFC7CE" } } }- 性能监控:添加导出性能日志
console.time('Excel导出') // ...导出操作 console.timeEnd('Excel导出') // 打印耗时- 用户反馈:对于大数据量导出,提供进度提示
<template> <div v-if="exporting"> <progress :value="exportProgress" max="100"></progress> <span>{{ exportProgress }}%</span> </div> </template>- 错误边界:捕获并处理可能的错误
try { this.$xlsxStyle.writeFile(wb, fileName) } catch (error) { console.error('导出失败:', error) this.$message.error(`导出失败: ${error.message}`) // 回退到无样式导出 try { this.$xlsx.writeFile(wb, fileName) this.$message.warning('已导出无样式版本') } catch (fallbackError) { console.error('回退导出失败:', fallbackError) } }- 浏览器兼容性:针对不同浏览器做适配处理
// IE11兼容方案 if (window.Blob && window.navigator.msSaveOrOpenBlob) { const blob = new Blob( [this.$xlsxStyle.write(wb, { bookType: 'xlsx', type: 'binary' })], { type: 'application/octet-stream' } ) window.navigator.msSaveOrOpenBlob(blob, fileName) } else { this.$xlsxStyle.writeFile(wb, fileName) }