Vue项目实战:xlsx+FileSaver实现Excel导入导出全流程与性能优化
在企业管理后台和数据分析系统中,Excel文件的导入导出功能几乎是标配需求。本文将深入探讨如何在Vue项目中高效集成xlsx和FileSaver库,实现完整的Excel交互解决方案,同时分享实际开发中的性能优化技巧和常见问题应对策略。
1. 环境准备与库选型
1.1 核心库安装与版本控制
首先需要安装两个核心依赖:
npm install xlsx file-saver --save
版本选择至关重要,以下是经过生产验证的稳定版本组合:
| 库名称 | 推荐版本 | 主要特性 |
|---|---|---|
| xlsx | 0.18.5 | 稳定的API接口,良好的中文支持 |
| file-saver | 2.0.5 | 跨浏览器文件保存方案 |
常见版本问题解决方案:
- 中文乱码:确保使用xlsx 0.17.0及以上版本
- 类型声明缺失:对于TypeScript项目,需额外安装
@types/xlsx
1.2 基础工具函数封装
建议在项目中创建excelUtils.js作为工具模块:
import * as XLSX from 'xlsx'
import { saveAs } from 'file-saver'
// 导出Excel的通用方法
export const exportAsExcel = (data, fileName = 'export') => {
const worksheet = XLSX.utils.json_to_sheet(data)
const workbook = XLSX.utils.book_new()
XLSX.utils.book_append_sheet(workbook, worksheet, 'Sheet1')
XLSX.writeFile(workbook, `${fileName}.xlsx`)
}
// 读取Excel文件的Promise封装
export const readExcelFile = (file) => {
return new Promise((resolve) => {
const reader = new FileReader()
reader.onload = (e) => {
const data = new Uint8Array(e.target.result)
const workbook = XLSX.read(data, { type: 'array' })
resolve(workbook)
}
reader.readAsArrayBuffer(file)
})
}
2. 高效Excel导出实现
2.1 基础数据导出
最简单的导出实现只需要几行代码:
const exportSimpleData = () => {
const data = [
{ id: 1, name: '产品A', price: 299 },
{ id: 2, name: '产品B', price: 399 }
]
exportAsExcel(data, '产品列表')
}
2.2 复杂表格样式控制
对于需要自定义表头、合并单元格等高级需求,可以使用以下方案:
const exportComplexExcel = () => {
// 1. 创建工作簿
const workbook = XLSX.utils.book_new()
// 2. 准备数据(包含多级表头)
const data = [
['主分类', '', '商品信息', '', ''],
['ID', '分类', '名称', '规格', '单价'],
[1, '电子产品', '手机', '128G', 2999],
[2, '家居用品', '台灯', 'LED', 199]
]
// 3. 创建带有合并配置的工作表
const worksheet = XLSX.utils.aoa_to_sheet(data)
// 4. 设置合并单元格
if(!worksheet['!merges']) worksheet['!merges'] = []
worksheet['!merges'].push(
{ s: { r: 0, c: 0 }, e: { r: 0, c: 1 } }, // 合并主分类
{ s: { r: 0, c: 2 }, e: { r: 0, c: 4 } } // 合并商品信息
)
// 5. 添加到工作簿并导出
XLSX.utils.book_append_sheet(workbook, worksheet, '商品报表')
XLSX.writeFile(workbook, '复杂表格.xlsx')
}
2.3 大数据量导出优化
当数据量超过万行时,需要考虑性能优化:
- 分块处理:将大数据分成多个Sheet
const exportLargeData = (data) => {
const chunkSize = 50000 // 每个Sheet最多5万行
const workbook = XLSX.utils.book_new()
for(let i = 0; i < Math.ceil(data.length / chunkSize); i++) {
const chunk = data.slice(i * chunkSize, (i + 1) * chunkSize)
const worksheet = XLSX.utils.json_to_sheet(chunk)
XLSX.utils.book_append_sheet(workbook, worksheet, `数据_${i+1}`)
}
XLSX.writeFile(workbook, '大数据量导出.xlsx')
}
- Web Worker方案:将计算密集型任务放到后台线程
// worker.js
self.importScripts('https://cdn.jsdelivr.net/npm/xlsx@0.18.5/dist/xlsx.full.min.js')
self.onmessage = function(e) {
const { data, fileName } = e.data
const workbook = XLSX.utils.book_new()
const worksheet = XLSX.utils.json_to_sheet(data)
XLSX.utils.book_append_sheet(workbook, worksheet, 'Sheet1')
const excelOutput = XLSX.write(workbook, { bookType: 'xlsx', type: 'array' })
self.postMessage({ excelOutput, fileName })
}
// 主线程调用
const worker = new Worker('worker.js')
worker.postMessage({ data: largeData, fileName: '大数据导出' })
worker.onmessage = (e) => {
const blob = new Blob([e.data.excelOutput], {type: 'application/octet-stream'})
saveAs(blob, e.data.fileName)
}
3. Excel导入的高级处理
3.1 基础文件导入
const handleFileImport = async (file) => {
try {
const workbook = await readExcelFile(file)
const firstSheetName = workbook.SheetNames[0]
const worksheet = workbook.Sheets[firstSheetName]
return XLSX.utils.sheet_to_json(worksheet)
} catch (error) {
console.error('文件导入失败:', error)
return null
}
}
3.2 复杂表头处理
实际业务中常遇到带有多级表头的Excel文件:
const parseComplexExcel = (workbook) => {
const worksheet = workbook.Sheets[workbook.SheetNames[0]]
const rawData = XLSX.utils.sheet_to_json(worksheet, { header: 1 })
// 假设第1-2行为表头
const headers = processHeaders(rawData.slice(0, 2))
const content = rawData.slice(2)
return content.map(row => {
const item = {}
headers.forEach((header, index) => {
if(header) item[header.key] = row[index]
})
return item
})
}
// 处理多级表头合并
function processHeaders(headerRows) {
const [mainHeaders, subHeaders] = headerRows
return subHeaders.map((sub, i) => {
return {
title: `${mainHeaders[i] || ''}${sub ? '/' + sub : ''}`,
key: `col_${i}`
}
})
}
3.3 大数据量导入优化
对于超过10MB的Excel文件,建议采用流式处理:
const streamImportExcel = (file, onProgress) => {
return new Promise((resolve) => {
const CHUNK_SIZE = 1024 * 1024 // 1MB
const fileSize = file.size
let offset = 0
let workbook = null
let processedRows = 0
const processChunk = (chunk) => {
const buffer = new Uint8Array(chunk)
workbook = workbook
? XLSX.read(buffer, { type: 'array', bookFiles: true })
: XLSX.read(buffer, { type: 'array' })
if(workbook.SheetNames.length) {
const worksheet = workbook.Sheets[workbook.SheetNames[0]]
const jsonData = XLSX.utils.sheet_to_json(worksheet)
processedRows += jsonData.length
onProgress && onProgress({
loaded: offset,
total: fileSize,
processedRows
})
}
}
const reader = new FileReader()
reader.onload = (e) => {
processChunk(e.target.result)
offset += CHUNK_SIZE
if(offset < fileSize) {
readNextChunk()
} else {
resolve(workbook)
}
}
const readNextChunk = () => {
const slice = file.slice(offset, offset + CHUNK_SIZE)
reader.readAsArrayBuffer(slice)
}
readNextChunk()
})
}
4. 实战中的疑难问题解决方案
4.1 中文乱码问题
现象:导出的Excel打开后中文显示为乱码
解决方案:
- 确保使用xlsx 0.17.0+版本
- 添加BOM头:
const exportWithBOM = (data, fileName) => {
const worksheet = XLSX.utils.json_to_sheet(data)
const workbook = XLSX.utils.book_new()
XLSX.utils.book_append_sheet(workbook, worksheet, 'Sheet1')
// 添加UTF-8 BOM头
const excelBuffer = XLSX.write(workbook, { bookType: 'xlsx', type: 'array' })
const bom = new Uint8Array([0xEF, 0xBB, 0xBF])
const blob = new Blob([bom, excelBuffer], {type: 'application/octet-stream'})
saveAs(blob, `${fileName}.xlsx`)
}
4.2 日期格式处理
Excel中的日期存储为数字,需要特殊处理:
// 导入时转换Excel日期
const excelDateToJSDate = (serial) => {
const utc_days = Math.floor(serial - 25569)
const utc_value = utc_days * 86400
return new Date(utc_value * 1000)
}
// 导出时转换JS日期为Excel日期
const jsDateToExcelDate = (date) => {
return (date.getTime() - new Date(Date.UTC(1899, 11, 30)).getTime()) / (86400 * 1000)
}
4.3 内存溢出问题
处理超大型Excel文件时可能遇到内存不足:
- 使用流式API:
const stream = fs.createReadStream('huge.xlsx')
const workbook = XLSX.stream.to_json(stream, {
sheetRows: 1000 // 限制每Sheet读取行数
})
- 分片处理:
const processInChunks = async (file, chunkSize = 5000) => {
const workbook = await readExcelFile(file)
const worksheet = workbook.Sheets[workbook.SheetNames[0]]
const jsonData = XLSX.utils.sheet_to_json(worksheet)
for(let i = 0; i < jsonData.length; i += chunkSize) {
const chunk = jsonData.slice(i, i + chunkSize)
await processDataChunk(chunk) // 处理每个数据块
}
}
5. 企业级应用的最佳实践
5.1 前端性能监控
添加性能埋点监控关键指标:
const exportWithMetrics = async (data, fileName) => {
const startTime = performance.now()
try {
// 导出操作
const workbook = XLSX.utils.book_new()
const worksheet = XLSX.utils.json_to_sheet(data)
XLSX.utils.book_append_sheet(workbook, worksheet, 'Sheet1')
const exportStart = performance.now()
XLSX.writeFile(workbook, `${fileName}.xlsx`)
const exportTime = performance.now() - exportStart
// 上报性能数据
trackPerformance({
event: 'excel_export',
dataSize: data.length,
duration: performance.now() - startTime,
exportTime
})
return true
} catch (error) {
trackError({
event: 'excel_export_error',
error: error.message
})
return false
}
}
5.2 安全防护措施
- 文件类型校验:
const isValidExcel = (file) => {
const validTypes = [
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'application/vnd.ms-excel'
]
return validTypes.includes(file.type)
}
- 文件大小限制:
const MAX_SIZE = 10 * 1024 * 1024 // 10MB
if(file.size > MAX_SIZE) {
alert('文件大小超过10MB限制')
return
}
- 内容消毒处理:
const sanitizeExcelData = (data) => {
return data.map(row => {
const cleanRow = {}
Object.keys(row).forEach(key => {
cleanRow[key] = typeof row[key] === 'string'
? row[key].replace(/<script.*?>.*?<\/script>/gi, '')
: row[key]
})
return cleanRow
})
}
5.3 用户体验优化
- 进度显示:
<template>
<div>
<progress :value="progress" max="100"></progress>
<span>{{ progress }}%</span>
</div>
</template>
<script>
export default {
data() {
return { progress: 0 }
},
methods: {
async exportLargeFile() {
const chunkCount = Math.ceil(this.bigData.length / 10000)
for(let i = 0; i < chunkCount; i++) {
await processChunk(i)
this.progress = Math.round((i + 1) / chunkCount * 100)
}
}
}
}
</script>
- 断点续传:
const resumeExport = (data, processed = 0) => {
const CHUNK = 1000
const chunk = data.slice(processed, processed + CHUNK)
return exportChunk(chunk).then(() => {
if(processed + CHUNK < data.length) {
return resumeExport(data, processed + CHUNK)
}
})
}
// 保存进度到localStorage
window.addEventListener('beforeunload', () => {
localStorage.setItem('exportProgress', JSON.stringify({
data: remainingData,
processed: processedCount
}))
})
6. 扩展应用场景
6.1 与Element UI集成
<template>
<el-upload
action=""
:auto-upload="false"
:on-change="handleImport"
accept=".xlsx, .xls"
>
<el-button type="primary">导入Excel</el-button>
</el-upload>
</template>
<script>
export default {
methods: {
async handleImport(file) {
const data = await readExcelFile(file.raw)
this.$emit('import', data)
}
}
}
</script>
6.2 生成带样式的模板
const generateTemplate = () => {
const workbook = XLSX.utils.book_new()
const worksheet = XLSX.utils.aoa_to_sheet([
['姓名', '年龄', '部门'],
['示例:张三', '示例:30', '示例:技术部']
])
// 设置列宽
worksheet['!cols'] = [{ width: 15 }, { width: 10 }, { width: 20 }]
// 设置单元格样式
const style = { font: { bold: true }, fill: { fgColor: { rgb: "FFFF00" } } }
worksheet['A1'].s = worksheet['B1'].s = worksheet['C1'].s = style
XLSX.utils.book_append_sheet(workbook, worksheet, '导入模板')
XLSX.writeFile(workbook, '员工信息模板.xlsx')
}
6.3 多Sheet数据组装
const exportMultiSheetReport = (datasets) => {
const workbook = XLSX.utils.book_new()
datasets.forEach(({ name, data }, index) => {
const worksheet = XLSX.utils.json_to_sheet(data)
XLSX.utils.book_append_sheet(workbook, worksheet, name || `Sheet${index+1}`)
})
// 添加目录
const tocWorksheet = XLSX.utils.aoa_to_sheet([
['报表目录'],
...workbook.SheetNames.map((name, i) => [`${i+1}`, name])
])
XLSX.utils.book_append_sheet(workbook, tocWorksheet, '目录', 0)
XLSX.writeFile(workbook, '综合报表.xlsx')
}
在实际项目开发中,Excel导入导出功能的实现需要根据具体业务需求进行调整优化。本文介绍的技术方案已在多个生产环境中验证,能够满足大多数业务场景的需求。对于更复杂的场景,建议结合Web Worker和服务端渲染技术进一步提升性能和用户体验。
&spm=1001.2101.3001.5002&articleId=154326047&d=1&t=3&u=85d8f9ae41b04815902483ee5f94627c)
7362

被折叠的 条评论
为什么被折叠?



