1. 项目概述:Vue集成xlsx实现前端表格导入导出
在前后端分离的开发模式下,前端处理Excel表格数据的需求越来越普遍。最近接手了一个后台管理系统项目,需要实现用户数据的批量导入和报表导出功能。传统方案是让后端处理Excel文件,但这会导致服务器压力增大、响应延迟等问题。经过技术调研,最终选择在前端直接集成xlsx.js库实现表格处理,实测效果非常理想。
这个方案的核心优势在于:
- 完全在前端完成Excel解析和生成,减轻服务器负担
- 用户上传文件后即时反馈数据格式问题
- 导出报表时无需等待后端处理,响应速度提升3-5倍
- 支持主流浏览器且兼容xlsx/xls/csv多种格式
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 技术选型与核心原理
2.1 为什么选择xlsx.js
在比较了SheetJS、xlsx-style、exceljs等多个库后,最终选用xlsx.js的原因包括:
- 轻量高效:压缩后仅30KB左右,不增加项目体积负担
- 功能全面:支持读写xlsx/xls/csv格式,兼容Excel 2007+
- 零依赖:纯JS实现,不依赖其他库
- 活跃维护:GitHub上star数超过1.5万,issue响应及时
注意:xlsx-style虽然支持样式设置,但维护不及时且存在兼容性问题,生产环境慎用
2.2 核心工作原理
xlsx.js通过以下流程处理Excel文件:
- 文件解析:将二进制文件转换为workbook对象
- 工作表操作:通过sheet_to_json/json_to_sheet方法转换数据
- 文件生成:将workbook对象转为二进制数据
javascript复制// 典型处理流程示例
import XLSX from 'xlsx'
// 读取Excel
function readExcel(file) {
const reader = new FileReader()
reader.onload = (e) => {
const data = new Uint8Array(e.target.result)
const workbook = XLSX.read(data, { type: 'array' })
const firstSheet = workbook.Sheets[workbook.SheetNames[0]]
const jsonData = XLSX.utils.sheet_to_json(firstSheet)
return jsonData
}
reader.readAsArrayBuffer(file)
}
3. 完整实现方案
3.1 环境准备
首先安装必要依赖:
bash复制npm install xlsx file-saver --save
# 或
yarn add xlsx file-saver
- xlsx:核心表格处理库
- file-saver:用于文件下载(可选,可用原生API替代)
3.2 导入功能实现
3.2.1 基础导入组件
创建ExcelImport.vue组件:
vue复制<template>
<div>
<input
type="file"
accept=".xlsx, .xls, .csv"
@change="handleFile"
/>
<table v-if="tableData.length">
<thead>
<tr>
<th v-for="header in headers" :key="header">{{ header }}</th>
</tr>
</thead>
<tbody>
<tr v-for="(row, index) in tableData" :key="index">
<td v-for="header in headers" :key="header">{{ row[header] }}</td>
</tr>
</tbody>
</table>
</div>
</template>
<script>
import XLSX from 'xlsx'
export default {
data() {
return {
tableData: [],
headers: []
}
},
methods: {
handleFile(e) {
const files = e.target.files
if (!files.length) return
const file = files[0]
const reader = new FileReader()
reader.onload = (e) => {
try {
const data = new Uint8Array(e.target.result)
const workbook = XLSX.read(data, { type: 'array' })
const firstSheet = workbook.Sheets[workbook.SheetNames[0]]
// 获取表头
this.headers = this.getHeaders(firstSheet)
// 转换JSON数据
this.tableData = XLSX.utils.sheet_to_json(firstSheet)
this.$emit('import-success', this.tableData)
} catch (error) {
this.$emit('import-error', error)
}
}
reader.readAsArrayBuffer(file)
},
getHeaders(sheet) {
const headers = []
const range = XLSX.utils.decode_range(sheet['!ref'])
for (let C = range.s.c; C <= range.e.c; ++C) {
const cell = sheet[XLSX.utils.encode_cell({ c: C, r: range.s.r })]
headers.push(cell ? cell.v : '')
}
return headers
}
}
}
</script>
3.2.2 高级功能扩展
- 数据校验:在转换JSON数据前验证格式
javascript复制validateData(jsonData) {
const requiredFields = ['name', 'age', 'email']
const errors = []
jsonData.forEach((row, index) => {
requiredFields.forEach(field => {
if (!row[field]) {
errors.push(`第${index + 2}行缺少${field}字段`)
}
})
if (row.email && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(row.email)) {
errors.push(`第${index + 2}行邮箱格式不正确`)
}
})
if (errors.length) {
throw new Error(errors.join('\n'))
}
}
- 大数据量处理:使用Web Worker避免UI阻塞
javascript复制// worker.js
self.importScripts('https://cdn.jsdelivr.net/npm/xlsx@0.18.5/dist/xlsx.full.min.js')
self.onmessage = function(e) {
const { data, type } = e.data
const workbook = XLSX.read(data, { type: 'array' })
const jsonData = XLSX.utils.sheet_to_json(workbook.Sheets[workbook.SheetNames[0]])
postMessage(jsonData)
}
// 组件中使用
const worker = new Worker('./worker.js')
worker.onmessage = (e) => {
this.tableData = e.data
}
worker.postMessage({
data: new Uint8Array(e.target.result),
type: 'parse'
})
3.3 导出功能实现
3.3.1 基础导出方法
javascript复制export function exportExcel(data, fileName = 'export.xlsx') {
// 创建工作簿
const workbook = XLSX.utils.book_new()
// 创建工作表
const worksheet = XLSX.utils.json_to_sheet(data)
// 添加工作表到工作簿
XLSX.utils.book_append_sheet(workbook, worksheet, 'Sheet1')
// 生成Excel文件
XLSX.writeFile(workbook, fileName)
}
3.3.2 复杂表格导出
- 带样式的导出(使用xlsx-style)
javascript复制import XLSX from 'xlsx'
import XLSXStyle from 'xlsx-style'
function exportWithStyle() {
const workbook = XLSX.utils.book_new()
const worksheet = XLSX.utils.json_to_sheet(data)
// 设置标题行样式
for (let i = 0; i < headers.length; i++) {
const cell = worksheet[XLSX.utils.encode_cell({ r: 0, c: i })]
cell.s = {
font: { bold: true, color: { rgb: "FFFFFF" } },
fill: { fgColor: { rgb: "4472C4" } },
alignment: { horizontal: "center" }
}
}
XLSX.utils.book_append_sheet(workbook, worksheet, 'Sheet1')
XLSXStyle.writeFile(workbook, 'styled-export.xlsx')
}
- 多sheet导出
javascript复制function exportMultiSheet(data1, data2) {
const workbook = XLSX.utils.book_new()
// 第一个sheet
const ws1 = XLSX.utils.json_to_sheet(data1)
XLSX.utils.book_append_sheet(workbook, ws1, '数据1')
// 第二个sheet
const ws2 = XLSX.utils.json_to_sheet(data2)
XLSX.utils.book_append_sheet(workbook, ws2, '数据2')
XLSX.writeFile(workbook, 'multi-sheet.xlsx')
}
- 大数据量分片导出
javascript复制async function exportLargeData(data, fileName, chunkSize = 10000) {
const workbook = XLSX.utils.book_new()
let currentChunk = 1
for (let i = 0; i < data.length; i += chunkSize) {
const chunk = data.slice(i, i + chunkSize)
const worksheet = XLSX.utils.json_to_sheet(chunk)
XLSX.utils.book_append_sheet(workbook, worksheet, `数据_${currentChunk}`)
currentChunk++
// 每处理完一个chunk让出主线程
await new Promise(resolve => setTimeout(resolve, 0))
}
XLSX.writeFile(workbook, fileName)
}
4. 性能优化与实战技巧
4.1 性能优化方案
- 使用Web Worker处理大文件
javascript复制// export.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 excelData = XLSX.write(workbook, { bookType: 'xlsx', type: 'array' })
self.postMessage({ excelData, fileName })
}
// 组件中使用
const exportWorker = new Worker('./export.worker.js')
exportWorker.onmessage = (e) => {
const { excelData, fileName } = e.data
const blob = new Blob([excelData], { type: 'application/octet-stream' })
saveAs(blob, fileName)
}
exportWorker.postMessage({ data: largeData, fileName: 'large-export.xlsx' })
- 虚拟滚动优化表格渲染
vue复制<template>
<div class="excel-preview" @scroll="handleScroll">
<div class="table-header">
<table>
<thead>
<tr>
<th v-for="header in headers" :key="header" :style="{ width: colWidths[header] }">
{{ header }}
</th>
</tr>
</thead>
</table>
</div>
<div class="table-body" :style="{ height: totalHeight }">
<div class="table-content" :style="{ transform: `translateY(${offsetY}px)` }">
<table>
<tbody>
<tr v-for="row in visibleData" :key="row.id" :style="{ height: rowHeight }">
<td v-for="header in headers" :key="header" :style="{ width: colWidths[header] }">
{{ row[header] }}
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</template>
<script>
export default {
props: ['data', 'headers'],
data() {
return {
rowHeight: 40,
visibleCount: 20,
offsetY: 0
}
},
computed: {
totalHeight() {
return this.data.length * this.rowHeight + 'px'
},
visibleData() {
const start = Math.floor(this.offsetY / this.rowHeight)
return this.data.slice(start, start + this.visibleCount)
},
colWidths() {
const widths = {}
this.headers.forEach(header => {
widths[header] = '150px' // 可根据内容动态计算
})
return widths
}
},
methods: {
handleScroll(e) {
this.offsetY = e.target.scrollTop
}
}
}
</script>
4.2 实战经验与避坑指南
- 中文乱码问题解决方案
javascript复制// 读取CSV文件时指定编码
function readCSV(file) {
return new Promise((resolve) => {
const reader = new FileReader()
reader.onload = (e) => {
const text = new TextDecoder('gbk').decode(new Uint8Array(e.target.result))
const workbook = XLSX.read(text, { type: 'string', codepage: 936 })
resolve(workbook)
}
reader.readAsArrayBuffer(file)
})
}
// 导出时设置编码
function exportWithChinese() {
const workbook = XLSX.utils.book_new()
const worksheet = XLSX.utils.json_to_sheet(data)
XLSX.utils.book_append_sheet(workbook, worksheet, 'Sheet1')
// 设置中文编码
const excelData = XLSX.write(workbook, {
bookType: 'xlsx',
type: 'buffer',
bookSST: true,
ignoreEC: false
})
const blob = new Blob([excelData], { type: 'application/octet-stream' })
saveAs(blob, '中文导出.xlsx')
}
- 日期格式处理技巧
javascript复制// 读取时转换日期
function readDates(workbook) {
const opts = {
raw: false,
dateNF: 'yyyy-mm-dd',
cellDates: true
}
return XLSX.utils.sheet_to_json(workbook.Sheets[workbook.SheetNames[0]], opts)
}
// 导出时格式化日期
function formatDates(data) {
return data.map(row => {
if (row.date instanceof Date) {
row.date = XLSX.SSF.format('yyyy-mm-dd', row.date)
}
return row
})
}
- 大文件分片处理方案
javascript复制async function processLargeFile(file, chunkSize = 5 * 1024 * 1024) {
const fileSize = file.size
let offset = 0
const chunks = []
while (offset < fileSize) {
const chunk = file.slice(offset, offset + chunkSize)
const chunkData = await readChunk(chunk)
chunks.push(chunkData)
offset += chunkSize
// 更新进度
this.progress = Math.min(100, (offset / fileSize) * 100)
await new Promise(resolve => setTimeout(resolve, 0)) // 让出主线程
}
return mergeChunks(chunks)
}
function readChunk(chunk) {
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(XLSX.utils.sheet_to_json(workbook.Sheets[workbook.SheetNames[0]]))
}
reader.readAsArrayBuffer(chunk)
})
}
5. 企业级应用方案
5.1 与Element UI/Ant Design Vue集成
5.1.1 Element UI上传组件集成
vue复制<template>
<el-upload
action=""
:auto-upload="false"
:show-file-list="false"
:on-change="handleImport"
accept=".xlsx, .xls, .csv"
>
<el-button type="primary">导入Excel</el-button>
</el-upload>
</template>
<script>
import XLSX from 'xlsx'
export default {
methods: {
async handleImport(file) {
try {
this.loading = true
const data = await this.readExcel(file.raw)
await this.validateData(data)
await this.$store.dispatch('submitData', data)
this.$message.success('导入成功')
} catch (error) {
this.$message.error(`导入失败: ${error.message}`)
} finally {
this.loading = false
}
},
readExcel(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader()
reader.onload = (e) => {
try {
const data = new Uint8Array(e.target.result)
const workbook = XLSX.read(data, { type: 'array' })
const jsonData = XLSX.utils.sheet_to_json(
workbook.Sheets[workbook.SheetNames[0]],
{ defval: '' }
)
resolve(jsonData)
} catch (error) {
reject(error)
}
}
reader.onerror = () => reject(new Error('文件读取失败'))
reader.readAsArrayBuffer(file)
})
}
}
}
</script>
5.1.2 Ant Design Vue表格导出
vue复制<template>
<a-button @click="exportExcel">导出Excel</a-button>
<a-table
:columns="columns"
:data-source="data"
rowKey="id"
:pagination="false"
ref="table"
/>
</template>
<script>
import XLSX from 'xlsx'
import { saveAs } from 'file-saver'
export default {
methods: {
exportExcel() {
// 获取表格DOM
const table = this.$refs.table.$el.querySelector('.ant-table-content table')
// 克隆表格避免修改原样式
const cloneTable = table.cloneNode(true)
// 移除不需要的元素
const actions = cloneTable.querySelectorAll('.ant-table-row-expand-icon')
actions.forEach(el => el.remove())
// 转换为工作表
const worksheet = XLSX.utils.table_to_sheet(cloneTable)
// 创建工作簿
const workbook = XLSX.utils.book_new()
XLSX.utils.book_append_sheet(workbook, worksheet, 'Sheet1')
// 生成文件
const excelData = XLSX.write(workbook, { bookType: 'xlsx', type: 'array' })
const blob = new Blob([excelData], { type: 'application/octet-stream' })
saveAs(blob, '表格导出.xlsx')
}
}
}
</script>
5.2 服务端协同方案
5.2.1 前后端数据校验方案
javascript复制// 前端校验规则配置
const VALIDATION_RULES = {
name: {
required: true,
validator: (value) => /^[\u4e00-\u9fa5]{2,10}$/.test(value),
message: '姓名必须为2-10个汉字'
},
idCard: {
required: true,
validator: (value) => /^[1-9]\d{5}(18|19|20)\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])\d{3}[\dXx]$/.test(value),
message: '身份证号格式不正确'
},
phone: {
required: false,
validator: (value) => !value || /^1[3-9]\d{9}$/.test(value),
message: '手机号格式不正确'
}
}
// 校验方法
function validateRow(row, index) {
const errors = []
Object.keys(VALIDATION_RULES).forEach(key => {
const rule = VALIDATION_RULES[key]
const value = row[key]
if (rule.required && (value === undefined || value === null || value === '')) {
errors.push(`第${index + 2}行【${key}】不能为空`)
} else if (value && !rule.validator(value)) {
errors.push(`第${index + 2}行【${key}】${rule.message}`)
}
})
return errors
}
// 服务端二次校验
async function submitToServer(data) {
try {
const res = await axios.post('/api/validate-excel', { data })
if (res.data.errors && res.data.errors.length) {
throw new Error(res.data.errors.join('\n'))
}
return res.data.validData
} catch (error) {
throw error
}
}
5.2.2 断点续传与批量处理
javascript复制// 前端分片上传
async function chunkUpload(file, chunkSize = 2 * 1024 * 1024) {
const fileSize = file.size
const chunks = Math.ceil(fileSize / chunkSize)
const fileMd5 = await calculateMD5(file)
// 检查已上传分片
const { data } = await axios.get('/api/upload-status', {
params: { fileMd5, fileName: file.name }
})
let uploaded = data.uploadedChunks || 0
while (uploaded < chunks) {
const start = uploaded * chunkSize
const end = Math.min(fileSize, start + chunkSize)
const chunk = file.slice(start, end)
const formData = new FormData()
formData.append('file', chunk)
formData.append('chunkIndex', uploaded)
formData.append('chunks', chunks)
formData.append('fileMd5', fileMd5)
formData.append('fileName', file.name)
try {
await axios.post('/api/upload-chunk', formData, {
headers: { 'Content-Type': 'multipart/form-data' }
})
uploaded++
// 更新进度
this.progress = Math.floor((uploaded / chunks) * 100)
} catch (error) {
console.error('上传失败:', error)
break
}
}
if (uploaded === chunks) {
// 通知服务端合并文件
await axios.post('/api/merge-chunks', {
fileMd5,
fileName: file.name,
chunks
})
// 开始处理Excel
await this.processExcelOnServer(fileMd5)
}
}
6. 扩展功能实现
6.1 复杂表格布局导出
javascript复制function exportComplexLayout() {
const workbook = XLSX.utils.book_new()
// 创建合并单元格的工作表
const wsData = [
['主要信息', null, null, '详细信息'],
['姓名', '年龄', '性别', '地址'],
['张三', 25, '男', '北京市朝阳区'],
['李四', 30, '女', '上海市浦东新区']
]
const worksheet = XLSX.utils.aoa_to_sheet(wsData)
// 设置合并单元格
worksheet['!merges'] = [
{ s: { r: 0, c: 0 }, e: { r: 0, c: 2 } }, // 合并A1:C1
{ s: { r: 0, c: 3 }, e: { r: 0, c: 3 } } // D1单独
]
// 设置列宽
worksheet['!cols'] = [
{ wch: 15 }, // A列宽度
{ wch: 10 }, // B列
{ wch: 10 }, // C列
{ wch: 30 } // D列
]
// 设置行高
worksheet['!rows'] = [
{ hpx: 30 }, // 第一行高度
{ hpx: 25 } // 第二行
]
XLSX.utils.book_append_sheet(workbook, worksheet, '复杂表格')
XLSX.writeFile(workbook, 'complex-layout.xlsx')
}
6.2 模板下载与数据填充
javascript复制// 模板下载
function downloadTemplate() {
const template = [
['姓名', '年龄', '性别', '邮箱', '手机号'],
['示例数据', 25, '男', 'example@test.com', '13800138000'],
['', '', '', '', ''] // 空行用于填充
]
const workbook = XLSX.utils.book_new()
const worksheet = XLSX.utils.aoa_to_sheet(template)
// 设置数据验证
const dvRange = {
s: { r: 1, c: 1, }, // 从B2开始
e: { r: 100, c: 1 } // 到B101结束
}
worksheet['!dataValidations'] = [
{
ref: XLSX.utils.encode_range(dvRange),
type: 'whole',
operator: 'between',
formulae: [18, 60], // 年龄范围18-60
allowBlank: true
}
]
XLSX.utils.book_append_sheet(workbook, worksheet, '模板')
XLSX.writeFile(workbook, '用户数据模板.xlsx')
}
// 数据填充
function fillTemplateData(data) {
const template = [
['姓名', '年龄', '性别', '邮箱', '手机号']
]
data.forEach(item => {
template.push([
item.name,
item.age,
item.gender,
item.email,
item.phone
])
})
const workbook = XLSX.utils.book_new()
const worksheet = XLSX.utils.aoa_to_sheet(template)
XLSX.utils.book_append_sheet(workbook, worksheet, '填充数据')
XLSX.writeFile(workbook, '填充后的数据.xlsx')
}
6.3 与图表结合导出
javascript复制function exportWithChart() {
// 创建包含数据的工作表
const data = [
['月份', '销售额'],
['1月', 5000],
['2月', 8000],
['3月', 12000],
['4月', 9500],
['5月', 11000],
['6月', 15000]
]
const workbook = XLSX.utils.book_new()
const worksheet = XLSX.utils.aoa_to_sheet(data)
XLSX.utils.book_append_sheet(workbook, worksheet, '销售数据')
// 创建图表工作表
const chartSheet = XLSX.utils.aoa_to_sheet([])
chartSheet['!charts'] = [
{
type: 'bar',
series: [
{
name: '销售额',
categories: '销售数据!$A$2:$A$7',
values: '销售数据!$B$2:$B$7'
}
],
title: '上半年销售额趋势',
style: {
width: 800,
height: 400
},
position: {
from: { r: 1, c: 1 },
to: { r: 20, c: 10 }
}
}
]
XLSX.utils.book_append_sheet(workbook, chartSheet, '销售图表')
XLSX.writeFile(workbook, '带图表的报表.xlsx')
}
7. 安全与异常处理
7.1 文件安全校验
javascript复制function validateExcelFile(file) {
// 校验文件类型
const validTypes = [
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'application/vnd.ms-excel',
'text/csv'
]
if (!validTypes.includes(file.type)) {
throw new Error('不支持的文件类型')
}
// 校验文件大小(限制10MB)
const maxSize = 10 * 1024 * 1024
if (file.size > maxSize) {
throw new Error('文件大小不能超过10MB')
}
// 校验文件扩展名
const validExtensions = ['.xlsx', '.xls', '.csv']
const extension = file.name.substring(file.name.lastIndexOf('.')).toLowerCase()
if (!validExtensions.includes(extension)) {
throw new Error('不支持的文件扩展名')
}
return true
}
7.2 异常处理机制
javascript复制async function safeImport(file) {
try {
// 基础校验
this.validateExcelFile(file)
// 读取数据
const workbook = await this.readExcel(file)
const jsonData = XLSX.utils.sheet_to_json(
workbook.Sheets[workbook.SheetNames[0]],
{ defval: '', raw: false }
)
// 数据校验
const validationResult = this.validateData(jsonData)
if (!validationResult.valid) {
throw new Error(validationResult.errors.join('\n'))
}
// 处理数据
const processedData = this.processData(validationResult.cleanData)
// 提交到后端
const response = await this.submitToBackend(processedData)
return {
success: true,
data: response.data,
stats: {
total: jsonData.length,
success: processedData.length,
failed: jsonData.length - processedData.length
}
}
} catch (error) {
console.error('导入失败:', error)
return {
success: false,
error: error.message,
stack: process.env.NODE_ENV === 'development' ? error.stack : undefined
}
}
}
7.3 性能监控与日志
javascript复制// 性能监控装饰器
function measurePerformance(target, name, descriptor) {
const originalMethod = descriptor.value
descriptor.value = async function(...args) {
const start = performance.now()
let result, error
try {
result = await originalMethod.apply(this, args)
} catch (e) {
error = e
throw e
} finally {
const duration = performance.now() - start
const logData = {
operation: name,
duration: duration.toFixed(2),
timestamp: new Date().toISOString(),
success: !error,
error: error?.message,
fileSize: args[0]?.size,
rowsProcessed: result?.data?.length
}
// 发送监控数据
this.sendAnalytics(logData)
// 本地存储日志
if (process.env.NODE_ENV === 'development') {
console.table([logData])
}
}
return result
}
return descriptor
}
// 在方法上使用
class ExcelService {
@measurePerformance
async importExcel(file) {
// 导入逻辑...
}
}
8. 测试与调试方案
8.1 单元测试示例
javascript复制import { readExcel, exportExcel } from '@/utils/excel'
import XLSX from 'xlsx'
describe('Excel工具', () => {
const mockData = [
{ name: '张三', age: 25, gender: '男' },
{ name: '李四', age: 30, gender: '女' }
]
it('正确导出Excel文件', async () => {
const fileName = 'test-export.xlsx'
await exportExcel(mockData, fileName)
// 读取导出的文件验证内容
const workbook = XLSX.readFile(fileName)
const worksheet = workbook.Sheets[workbook.SheetNames[0]]
const jsonData = XLSX.utils.sheet_to_json(worksheet)
expect(jsonData).toEqual(mockData)
})
it('正确解析Excel文件', async () => {
// 创建测试文件
const workbook = XLSX.utils.book_new()
const worksheet = XLSX.utils.json_to_sheet(mockData)
XLSX.utils.book_append_sheet(workbook, worksheet, 'Sheet1')
XLSX.writeFile(workbook, 'test-import.xlsx')
// 测试解析
const file = new File(
[XLSX.write(workbook, { bookType: 'xlsx', type: 'array' })],
'test-import.xlsx',
{ type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' }
)
const result = await readExcel(file)
expect(result).toEqual(mockData)
})
})
8.2 E2E测试方案
javascript复制describe('Excel导入导出功能', () => {
it('成功导入Excel并显示数据', () => {
cy.visit('/')
// 上传测试文件
cy.fixture('test-data.xlsx', 'binary')
.then((file) => {
const blob = Cypress.Blob.binaryStringToBlob(file, 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')
const testFile = new File([blob], 'test-data.xlsx', { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' })
cy.get('input[type="file"]').attachFile({
fileContent: testFile,
fileName: 'test-data.xlsx',
mimeType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
})
})
// 验证表格显示
cy.get('table tr').should('have.length', 3) // 表头+2行数据
cy.contains('td', '张三').should('exist')
cy.contains('td', '25').should('exist')
})
it('成功导出Excel文件', () => {
cy.visit('/')
cy.get('button').contains('导出Excel').click()
// 验证下载
const downloadsFolder = Cypress.config('downloadsFolder')
const fileName = path.join(downloadsFolder, 'export.xlsx')
cy.readFile(fileName, 'binary', { timeout: 15000 })
.should((buffer) => {
// 验证文件内容
const workbook = XLSX.read(buffer, { type: 'buffer' })
const worksheet = workbook.Sheets[workbook.SheetNames[0]]
const jsonData = XLSX.utils.sheet_to_json(worksheet)
expect(jsonData).to.have.length.gt(0)
})
})
})
8.3 调试技巧
- 查看Workbook结构
javascript复制function debugWorkbook(workbook) {
console.log('工作表列表:', workbook.SheetNames)
workbook.SheetNames.forEach(sheetName => {
const worksheet = workbook.Sheets[sheetName]
console.log(`工作表 "${sheetName}" 范围:`, worksheet['!ref'])
// 查看前5行数据
const jsonData = XLSX.utils.sheet_to_json(worksheet, { header: 1 })
console.table(jsonData.slice(0, 5))
// 查看合并单元格
if (worksheet['!merges']) {
console.log('合并单元格:', worksheet['!merges'])
}
})
}
- 生成测试文件
javascript复制function generateTestFile(rows = 1000)
