当你需要把后端返回的 JSON 数据变成用户可以下载的 Excel 文件时,事情远比 JSON.stringify 复杂得多。据统计,超过 70% 的 B 端管理系统都涉及"数据导出"功能,而 Excel 导出几乎是最常见的需求。然而很多开发者在实现时只用了最简单的方案,导致大数据量页面卡死、中文乱码、格式丢失等问题频发。本文将从工程实践角度,深度对比三种主流前端 Excel 导出方案,帮你选对技术路线并避开生产环境中的各种坑。
📊 一、三大方案全景对比
前端 JSON 转 Excel 的主流方案有三种:SheetJS(xlsx.js)、ExcelJS 和纯 CSV 导出。它们在功能、体积和适用场景上有显著差异。
🔍 1.1 方案特性对比
| 特性 | SheetJS (xlsx.js) | ExcelJS | 纯 CSV 导出 |
|---|---|---|---|
| 包体积 | ~450KB (gzip ~160KB) | ~300KB (gzip ~100KB) | 0(原生实现) |
| 读写能力 | ✅ 读 + 写 | ✅ 读 + 写 | ❌ 只能写 |
| 样式支持 | ❌ 社区版不支持 | ✅ 完整支持 | ❌ 不支持 |
| 多 Sheet | ✅ | ✅ | ❌ |
| 公式支持 | ✅ | ✅ | ❌ |
| 大数据量 | ⚠️ 内存占用高 | ⚠️ 内存占用高 | ✅ 流式写入 |
| 浏览器兼容 | IE6+ | 现代浏览器 | 所有浏览器 |
| 开源协议 | Apache 2.0 | MIT | N/A |
💡 **提示:**如果你只需要简单的表格数据导出且不关心样式,CSV 是最佳选择——零依赖、零体积、兼容性最好。但用户打开 CSV 时可能遇到中文乱码(编码问题)和前导零丢失(如手机号
0138变成138)等经典坑。
🎯 1.2 方案选型决策树
选型不能只看功能列表,还要考虑业务场景:
- ✅ 选 CSV:数据量大(10万+行)、不需要样式、用户接受 CSV 格式
- ✅ 选 SheetJS:需要读取 Excel 文件、对样式要求不高、需要兼容老浏览器
- ✅ 选 ExcelJS:需要丰富的样式定制(颜色、边框、合并单元格)、现代浏览器环境
⚠️ **警告:**SheetJS 的开源版本(Community Edition)不支持单元格样式设置!如果你需要设置字体颜色、背景色、边框等,必须使用商业版($$$$)或选择 ExcelJS。
🚀 二、生产级实现方案
📦 2.1 SheetJS 方案:快速导出
SheetJS 是历史最悠久的 Excel 处理库,API 简洁,适合快速实现。以下是完整的导出实现:
// SheetJS 完整导出实现:JSON → Excel 下载
import * as XLSX from 'xlsx'
function exportToExcel(jsonData, fileName = 'export.xlsx') {
// 1. 将 JSON 数据转为工作表
const worksheet = XLSX.utils.json_to_sheet(jsonData)
// 2. 设置列宽(根据数据内容自动计算)
const colWidths = Object.keys(jsonData[0] || {}).map(key => ({
wch: Math.max(
key.length * 2,
...jsonData.map(row => String(row[key] || '').length)
) + 2
}))
worksheet['!cols'] = colWidths
// 3. 创建工作簿并添加工作表
const workbook = XLSX.utils.book_new()
XLSX.utils.book_append_sheet(workbook, worksheet, 'Sheet1')
// 4. 触发下载
XLSX.writeFile(workbook, fileName)
}
// 使用示例
const data = [
{ 姓名: '张三', 部门: '技术部', 薪资: 25000, 入职日期: '2024-03-15' },
{ 姓名: '李四', 部门: '产品部', 薪资: 22000, 入职日期: '2024-06-01' },
{ 姓名: '王五', 部门: '技术部', 薗资: 28000, 入职日期: '2023-11-20' }
]
exportToExcel(data, '员工信息表.xlsx')
SheetJS 的优点是 API 极简,三行代码就能完成导出。但它有一个致命缺陷:开源版不支持样式。如果你的产品经理说"表头要加粗、薪资列要红色",SheetJS 社区版就无能为力了。
🎨 2.2 ExcelJS 方案:样式与格式化
ExcelJS 是目前前端 Excel 导出的最佳选择——开源、MIT 协议、完整样式支持。以下是带完整样式的实现:
// ExcelJS 完整导出实现:带样式、合并单元格和条件格式
import ExcelJS from 'exceljs'
async function exportStyledExcel(jsonData, fileName = 'styled-export.xlsx') {
const workbook = new ExcelJS.Workbook()
workbook.creator = 'jsjson.com'
workbook.created = new Date()
const sheet = workbook.addWorksheet('员工信息', {
properties: { defaultColWidth: 18 }
})
// 1. 定义列和表头
sheet.columns = [
{ header: '姓名', key: 'name', width: 15 },
{ header: '部门', key: 'department', width: 15 },
{ header: '薪资(元)', key: 'salary', width: 18 },
{ header: '入职日期', key: 'hireDate', width: 18 },
{ header: '绩效评分', key: 'score', width: 12 }
]
// 2. 设置表头样式
const headerRow = sheet.getRow(1)
headerRow.font = { bold: true, size: 12, color: { argb: 'FFFFFFFF' } }
headerRow.fill = {
type: 'pattern',
pattern: 'solid',
fgColor: { argb: 'FF2563EB' } // 蓝色背景
}
headerRow.alignment = { horizontal: 'center', vertical: 'middle' }
headerRow.height = 28
// 3. 填充数据
jsonData.forEach(row => {
const addedRow = sheet.addRow({
name: row.name,
department: row.department,
salary: row.salary,
hireDate: row.hireDate,
score: row.score
})
// 薪资列格式化为货币
addedRow.getCell('salary').numFmt = '¥#,##0.00'
// 绩效评分条件格式:>= 90 绿色,< 60 红色
const scoreCell = addedRow.getCell('score')
if (row.score >= 90) {
scoreCell.font = { color: { argb: 'FF16A34A' }, bold: true }
} else if (row.score < 60) {
scoreCell.font = { color: { argb: 'FFDC2626' }, bold: true }
}
})
// 4. 添加数据边框
sheet.eachRow((row, rowNumber) => {
row.eachCell(cell => {
cell.border = {
top: { style: 'thin', color: { argb: 'FFE5E7EB' } },
bottom: { style: 'thin', color: { argb: 'FFE5E7EB' } },
left: { style: 'thin', color: { argb: 'FFE5E7EB' } },
right: { style: 'thin', color: { argb: 'FFE5E7EB' } }
}
})
})
// 5. 写入文件并下载
const buffer = await workbook.xlsx.writeBuffer()
const blob = new Blob([buffer], {
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
})
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = fileName
a.click()
URL.revokeObjectURL(url)
}
// 使用示例
const employees = [
{ name: '张三', department: '技术部', salary: 25000, hireDate: '2024-03-15', score: 92 },
{ name: '李四', department: '产品部', salary: 22000, hireDate: '2024-06-01', score: 78 },
{ name: '王五', department: '技术部', salary: 28000, hireDate: '2023-11-20', score: 45 },
{ name: '赵六', department: '设计部', salary: 20000, hireDate: '2025-01-10', score: 95 }
]
exportStyledExcel(employees, '员工信息表.xlsx')
📌 **记住:**ExcelJS 的
writeBuffer()返回的是 ArrayBuffer,在内存中构建整个 Excel 文件。对于 10 万行以下的数据完全没问题,但如果数据量更大,需要用分片策略(见下文)。
🌊 2.3 流式写入方案:大数据量导出
当数据量达到 10 万行甚至 100 万行时,一次性构建整个工作簿会导致页面卡死甚至崩溃。解决方案是使用 Web Worker + 分片写入:
// === export-worker.js ===
// 在 Web Worker 中执行 Excel 导出,避免阻塞主线程
importScripts('https://cdn.sheetjs.com/xlsx-0.20.3/package/dist/xlsx.full.min.js')
self.onmessage = function (e) {
const { data, chunkSize = 5000, fileName } = e.data
try {
// 1. 分片处理:每次处理 chunkSize 行
const totalRows = data.length
const worksheet = XLSX.utils.aoa_to_sheet([])
for (let i = 0; i < totalRows; i += chunkSize) {
const chunk = data.slice(i, i + chunkSize)
// 第一个分片包含表头
if (i === 0) {
const headers = Object.keys(chunk[0])
XLSX.utils.sheet_add_aoa(worksheet, [headers], { origin: 0 })
}
// 将分片数据追加到工作表
const rows = chunk.map(row => Object.values(row))
XLSX.utils.sheet_add_aoa(worksheet, rows, { origin: i + 1 })
// 报告进度
self.postMessage({
type: 'progress',
percent: Math.round(((i + chunk.length) / totalRows) * 100)
})
}
// 2. 生成工作簿
const workbook = XLSX.utils.book_new()
XLSX.utils.book_append_sheet(workbook, worksheet, 'Sheet1')
// 3. 生成二进制数据
const wbOut = XLSX.write(workbook, { bookType: 'xlsx', type: 'array' })
self.postMessage({ type: 'complete', buffer: wbOut }, [wbOut.buffer])
} catch (err) {
self.postMessage({ type: 'error', message: err.message })
}
}
// === 主线程:启动 Worker 并处理下载 ===
function exportLargeData(jsonData, fileName = 'large-export.xlsx') {
return new Promise((resolve, reject) => {
const worker = new Worker('/js/export-worker.js')
const startTime = performance.now()
worker.postMessage({ data: jsonData, chunkSize: 5000, fileName })
worker.onmessage = (e) => {
switch (e.data.type) {
case 'progress':
console.log(`导出进度: ${e.data.percent}%`)
// 可以更新 UI 进度条
break
case 'complete': {
const blob = new Blob([e.data.buffer], {
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
})
downloadBlob(blob, fileName)
const elapsed = ((performance.now() - startTime) / 1000).toFixed(2)
console.log(`✅ 导出完成,${jsonData.length} 行,耗时 ${elapsed}s`)
worker.terminate()
resolve()
break
}
case 'error':
worker.terminate()
reject(new Error(e.data.message))
}
}
})
}
function downloadBlob(blob, fileName) {
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = fileName
document.body.appendChild(a)
a.click()
document.body.removeChild(a)
URL.revokeObjectURL(url)
}
⚠️ **警告:**即使使用 Web Worker,对于超过 50 万行的数据,浏览器端生成 Excel 仍然可能耗时较长。建议在数据量超过 20 万行时,改为服务端生成(Node.js 环境使用 ExcelJS 的流式 API),前端只负责下载。
💡 三、工程化最佳实践与避坑指南
🐛 3.1 经典踩坑点
在生产环境中,Excel 导出有大量"看起来简单但实际会出问题"的场景:
❌ 问题 1:科学计数法吞噬数字
手机号 13812345678 在 Excel 中显示为 1.38123E+10,身份证号后三位变成 000。这是 Excel 的"智能"格式检测导致的。
// ❌ 错误写法:直接写入数字字符串
sheet.addRow({ phone: 13812345678 })
// Excel 显示: 1.38123E+10 ❌
// ✅ 正确写法:强制以文本格式写入
const row = sheet.addRow({ phone: '13812345678' })
row.getCell('phone').numFmt = '@' // @ 表示文本格式
// Excel 显示: 13812345678 ✅
❌ 问题 2:CSV 中文乱码
用 Blob 直接生成的 CSV 文件在 WPS 和部分 Excel 版本中打开会乱码,因为缺少 BOM(Byte Order Mark)头。
// ❌ 错误写法:缺少 BOM,中文乱码
const csvContent = '姓名,部门\n张三,技术部'
const blob = new Blob([csvContent], { type: 'text/csv' })
// ✅ 正确写法:添加 UTF-8 BOM
const BOM = '\uFEFF'
const csvContent = BOM + '姓名,部门\n张三,技术部'
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8' })
❌ 问题 3:日期格式不一致
JSON 中的日期字符串 '2026-06-10' 直接写入 Excel 会变成纯文本,而不是 Excel 日期类型,导致用户无法排序或做日期计算。
// ✅ 正确做法:写入 Excel 日期对象
const cell = row.getCell('hireDate')
cell.value = new Date('2026-06-10')
cell.numFmt = 'yyyy-mm-dd'
🔒 3.2 性能优化策略
对于大数据量导出,以下是经过生产验证的优化策略:
- ✅ 延迟创建 Object URL:不要在循环中创建 URL,只在最终下载时创建一次
- ✅ 使用
writeBuffer()而非writeFile():writeBuffer()返回 ArrayBuffer,避免触发多次文件系统操作 - ✅ 分片处理 + 进度反馈:每处理 N 行向用户报告一次进度,避免"页面卡死"的假象
- ✅ 及时释放内存:下载完成后调用
URL.revokeObjectURL()释放 Blob URL - ✅ 服务端兜底:数据量超过阈值时自动切换为服务端生成
- ❌ 避免在主线程直接处理大数据:超过 5 万行的数据必须放到 Worker 中处理
- ❌ 避免使用
JSON.parse(JSON.stringify(data))做深拷贝:大数据量下极慢,改用structuredClone() - ⚠️ 注意 Blob URL 的生命周期:某些浏览器在页面跳转后会自动释放 Blob URL,导致下载失败
🏗️ 3.3 生产级封装建议
// 生产级 Excel 导出工具类封装
class ExcelExporter {
constructor(options = {}) {
this.chunkSize = options.chunkSize || 5000
this.maxClientRows = options.maxClientRows || 200000
this.onProgress = options.onProgress || (() => {})
}
async export(jsonData, config = {}) {
const {
fileName = `export_${Date.now()}.xlsx`,
sheetName = 'Sheet1',
columns = null,
styleFn = null
} = config
// 数据量超过阈值时提示
if (jsonData.length > this.maxClientRows) {
const confirmed = confirm(
`数据量较大(${jsonData.length.toLocaleString()} 行),` +
`导出可能需要较长时间。是否继续?`
)
if (!confirmed) return
}
if (jsonData.length > this.chunkSize) {
return this._exportLarge(jsonData, fileName, sheetName, columns, styleFn)
}
return this._exportSmall(jsonData, fileName, sheetName, columns, styleFn)
}
async _exportSmall(data, fileName, sheetName, columns, styleFn) {
const ExcelJS = await import('exceljs')
const workbook = new ExcelJS.default.Workbook()
const sheet = workbook.addWorksheet(sheetName)
if (columns) {
sheet.columns = columns
data.forEach(row => sheet.addRow(row))
} else {
data.forEach(row => sheet.addRow(row))
}
if (styleFn) styleFn(sheet)
await this._download(workbook, fileName)
}
async _download(workbook, fileName) {
const buffer = await workbook.xlsx.writeBuffer()
const blob = new Blob([buffer], {
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
})
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = fileName
document.body.appendChild(a)
a.click()
document.body.removeChild(a)
URL.revokeObjectURL(url)
}
}
// 使用
const exporter = new ExcelExporter({
maxClientRows: 100000,
onProgress: (pct) => progressBar.style.width = `${pct}%`
})
await exporter.export(data, {
fileName: '销售报表.xlsx',
sheetName: '2026年Q2',
columns: [
{ header: '产品', key: 'product', width: 20 },
{ header: '销售额', key: 'amount', width: 15 },
{ header: '日期', key: 'date', width: 15 }
],
styleFn: (sheet) => {
sheet.getRow(1).font = { bold: true, size: 12 }
}
})
💡 **提示:**上述封装使用了动态
import('exceljs')实现按需加载,只有用户点击"导出"按钮时才下载 ExcelJS 库,避免影响首屏加载性能。
✅ 总结与建议
经过实际测试和生产验证,以下是三种方案的最终推荐:
| 场景 | 推荐方案 | 理由 |
|---|---|---|
| ✅ 简单表格导出、数据量大 | CSV + BOM | 零依赖、零体积、性能最好 |
| ✅ 需要样式和格式化 | ExcelJS | MIT 开源、样式完整、API 友好 |
| ✅ 需要读取 Excel 文件 | SheetJS | 读写能力最强、兼容性最好 |
| ✅ 数据量 10 万+ 且需要样式 | ExcelJS + Web Worker | 不阻塞主线程,体验流畅 |
| ⚠️ 数据量 50 万+ | 服务端生成 | 浏览器端已不现实 |
💰 成本与许可证注意事项
在选择方案时,许可证问题经常被忽略,但可能带来法律风险:
- ✅ ExcelJS:MIT 协议,可自由用于商业项目,无需担心合规问题
- ✅ 纯 CSV 导出:原生实现,无第三方依赖,零合规风险
- ⚠️ SheetJS 社区版:Apache 2.0 协议,可商用但样式功能受限
- ❌ SheetJS Pro 版:需要购买商业授权,价格不菲,适合大型企业
💡 **提示:**如果你的项目对许可证有严格要求(如政府项目或金融行业),优先选择 ExcelJS 或纯 CSV 方案。SheetJS 社区版虽然免费,但功能受限;Pro 版功能完整但需要付费。
⚡ **关键结论:**对于大多数 B 端管理系统的"导出报表"场景,ExcelJS + Web Worker 是最佳方案——开源免费、样式完整、大数据量不卡页面。如果只是简单的数据导出且用户能接受 CSV,那就用 CSV + BOM 方案,省去几百 KB 的库体积。
📌 **记住:**无论选择哪种方案,都要处理好三个"必踩之坑":科学计数法问题(长数字字段强制文本格式)、CSV 编码问题(添加 BOM 头)、日期格式问题(写入 Date 对象而非字符串)。这三个问题在生产环境中出现的频率极高,提前处理能省去大量用户投诉。