写这篇帖子之前我特意翻了翻之前的聊天记录发现“前端怎么导出Excel”这个问题几乎每隔几周就会被同事或者群友翻出来问一次。别小看这个功能它不像写页面样式、调接口那么“有存在感”但它实实在在地是后台管理系统里的高频刚需——报表导出、数据备份、离线分析但凡涉及表格展示的页面几乎躲不过这一关。而“Vue Element Plus 实现前端导出 Excel 功能”这套组合恰恰是目前前端方案里成本最低、落地最快的一种做法不需要后端介入不需要临时加接口纯浏览器端就能把页面上渲染出来的数据变成货真价实的 Excel 文件。这篇内容我会用 Vue 3 Element Plus 的组合来完整拆解整个实现链路。从最基础的“把表格数据导出来”到处理日期格式、自定义列宽、追加表头再到解决大数据量导出时页面卡顿的问题全部用可以直接抄的代码和步骤来讲。适合正在做后台管理系统、刚接手导出需求、或者想系统了解前端生成 Excel 原理的开发者参考。你不需要已经很熟 xlsx 这个库跟着走一遍基本就能应付日常工作里 80% 的导出需求。1. 前端导出 Excel 的方案选型与思路拆解做前端导出第一件事不是写代码而是想清楚走哪条路。技术选型直接决定了后面你会遇到多少坑。1.1 前端导出与后端导出的边界选择很多团队一提到“导出Excel”第一反应是让后端写接口。这种思路没有错但它适用的是另一类场景数据量大到百万行级别、导出内容需要服务端权限校验、或者数据源根本不在当前前端页面上。但如果你面对的场景是“页面上表格里已经有一批数据用户想一键下载存个档”这时候再走后端起接口就属于重复造轮子了。前端直接拿到表格的列定义和数据源用 JavaScript 在浏览器里生成 .xlsx 文件再触发下载整个流程不需要网络请求响应速度是毫秒级的而且不会额外占用服务器资源。我个人的判断标准很简单数据量在几万行以内优先前端导出。需要跨系统汇总数据、或涉及复杂服务端业务逻辑才考虑后端导出。既想快又想要大能力可以前后端配合前端生成模板后端填充数据。这个判断在绝大多数管理后台项目里是成立的。因为这类项目里的表格页基本都是分页加载的一次能展示的数据量天然有限前端导出是最合身的选择。1.2 核心库选型xlsx、exceljs 还是 html 表格转 blob前端生成 Excel 的库主流无非三个流派。第一种是用 SheetJS 的 xlsx 库。它的核心能力是把二维数组、JSON 数组转换成 Excel 能识别的数据格式优点是轻量、上手快、文档丰富社区里随便一搜就是大把示例。但它的社区版对样式支持很弱合并单元格、单元格颜色、边框这些操作要么得手动操作底层 XML要么直接不支持。第二种是 exceljs。它的强项正好是样式和高级特性——合并单元格、条件格式、富文本都能搞定API 设计得也更接近“操作 Excel 文件”的心智模型。但缺点也很实在打包体积比 xlsx 大不少API 复杂度更高如果你只是导个列表用它有点杀鸡用牛刀。第三种思路比较取巧把 HTML 表格转成 Blob 下载文件后缀写成 .xls。这招老项目中很常见优点是代码量极少缺点是生成的文件本质上是一个 HTML 文件Excel 打开时会弹格式警告特殊字符也容易错乱。这属于旁门左道我不建议在新项目里用了。我的建议是日常业务表格导出直接上 xlsx 库的 community 版本。它的体积和性能是最适合浏览器端场景的我们对样式的需求用“拼接配置数据”就能覆盖大部分。等哪天真遇到需要花式样式的需求再在 exceljs 方向单独做技术方案。2. 基于 Element Plus 表格的快速导出实现方案定了就可以动手了。这一节我会从零开始写一个完整的导出流程用 Element Plus 的 el-table 作为数据展示层xlsx 作为文件生成层最终实现一个“点按钮表格变文件”的效果。2.1 环境准备与依赖安装首先项目必须基于 Vue 3 Element Plus。如果你还没安装 Element Plus标准做法是使用官方提供的组件库安装指令把组件库和图标库一起加进项目。已装过的跳过这块即可。接着安装核心依赖。这里我建议直接装完整版的 SheetJS 库因为它的 API 稳定兼容性也最好安装命令只需要一行。注意SheetJS 官方现在把新版本的稳定分发放到了他们自己的 CDN 上npm 上的 xlsx 包停留在较旧版本。对绝大多数后台项目来讲npm 上这个版本完全够用不是非要追新。安装完成之后在需要导出的页面里引入两个模块一个用于把 HTML 表格结构解析成工作簿另一个用于触发文件下载。import * as XLSX from xlsx如果你觉得打包体积敏感也可以只引入需要的子模块但按我的经验xlsx 整体引入的体积并不夸张开发阶段没必要过早优化它。2.2 最简导出步骤从 el-table 到 .xlsx 文件Element Plus 的 el-table 组件有一个很关键的设计它会在页面里渲染出一个真实的 table 结构。这意味着 xlsx 库可以直接把这个 table 抓取出来转成工作簿数据。这个方案最大的优势是“所见即所得”——页面上表格长什么样导出的 Excel 里基本就是什么样。具体实现分三步第一步给需要导出的表格加上 id。确保这个 id 是页面里唯一的这样选择器才能精确命中。el-table :datatableData idexportTable el-table-column propname label姓名 / el-table-column propage label年龄 / el-table-column propcity label城市 / /el-table第二步写导出函数。核心逻辑就是“找到表格 - 转成工作表 - 放进工作簿 - 写入文件”。const exportExcel () { // 1. 从页面里拿到这个表格的真实 DOM const tableDom document.getElementById(exportTable) // 2. 使用 XLSX 将 DOM 表格解析为工作表对象 const worksheet XLSX.utils.table_to_sheet(tableDom) // 3. 创建新工作簿把工作表挂载进去 const workbook XLSX.utils.book_new() XLSX.utils.book_append_sheet(workbook, worksheet, Sheet1) // 4. 生成并触发下载 XLSX.writeFile(workbook, 用户列表.xlsx) }第三步在工具栏放一个按钮绑定这个函数。el-button typeprimary clickexportExcel导出 Excel/el-button就这么简单一个可用的导出功能已经完成了。点击按钮浏览器就会下载一个包含当前表格全部数据的 Excel 文件。这套做法的底层逻辑我拆开说一下。table_to_sheet这一句做的事情是把 DOM 里的table元素逐行遍历读取每个单元格的文本内容然后映射成二维数据结构。book_new和book_append_sheet则是把二维数组包装成 Excel 的通用格式。最后writeFile会根据文件扩展名自动选择文件格式直接把二进制数据以 .xlsx 为后缀保存下来。整个过程完全没有经过任何服务器数据始终在浏览器本地流转既快也安全。3. 导出功能的核心细节与进阶优化基础版能跑通后实战里你就得面对各种“不止那么简单”的需求了。这节我会把导出这个需求拉高一个层次讲清楚表头、单元格内容处理、文件名、大数据量这几块的实际解法。3.1 自定义表头与指定数据列的导出方案直接用 table_to_sheet 有一个局限性它导出的是页面表格里展示的所有列。但实际需求往往是“表格有10列导出只要其中5列”或者“导出的列名要和页面上显示的不一样、要更正式”。这时候有两种调整思路。第一种如果列名需要修改直接把表格 DOM 里对应表头的文本改掉。但这种做法会污染页面状态属于临时方案不推荐。第二种更靠谱的做法是绕过 DOM直接从数据源拼接二维数组。这样既控制了导出的列也控制了导出的表头。const exportCustomExcel () { // 定义要导出的列配置 const columns [ { prop: name, label: 姓名 }, { prop: age, label: 年龄 }, { prop: city, label: 所在城市 } ] // 第一行放表头 const header columns.map(col col.label) // 数据行只保留配置中存在的字段 const rows tableData.value.map(item columns.map(col item[col.prop])) // 把头和数据合并成二维数组 const exportData [header, ...rows] const worksheet XLSX.utils.aoa_to_sheet(exportData) const workbook XLSX.utils.book_new() XLSX.utils.book_append_sheet(workbook, worksheet, 用户列表) XLSX.writeFile(workbook, 用户列表.xlsx) }这里的核心 API 从table_to_sheet换成了aoa_to_sheet意思是“array of arrays to sheet”接收一个二维数组作为输入。这是 xlsx 库最通用的数据入口几乎所有导出需求最后都能归结为“拼一个二维数组转成工作表”。这个方案我更喜欢因为它把导出逻辑和页面 DOM 解耦了。以后哪怕表格组件换成别的库导出功能依然是自洽的。3.2 日期格式化、金额保留小数等常见数据处理技巧Excel 不是一个“宽容”的格式。导入导出时最常翻车的场景就是日期、数字这类有格式语义的数据。先说日期。如果你的表格里存的是2025-12-01 10:30:00这种标准字符串导出后 Excel 大概率把它当作普通文本而不是日期单元格。如果你存的是时间戳那导出来就是一串数字用户根本看不懂。推荐的做法是在拼二维数组时就把时间戳格式化成人类可读的字符串。const formatDate (timestamp) { const date new Date(timestamp) const year date.getFullYear() const month String(date.getMonth() 1).padStart(2, 0) const day String(date.getDate()).padStart(2, 0) const hours String(date.getHours()).padStart(2, 0) const minutes String(date.getMinutes()).padStart(2, 0) return ${year}-${month}-${day} ${hours}:${minutes} }再说金额。浮点数在 JS 里天生有精度问题0.1 0.2 不等于 0.3 想必大家都有印象。导出时如果直接拿浮点数写进单元格Excel 里显示出来的可能是 100.30000000000001 这种让人头皮发麻的值。所以数值型字段在导出前最好用toFixed(2)统一处理一下。const formatMoney (value) { return Number(value || 0).toFixed(2) }处理这类问题的原则只有一条导出的数据必须在进入工作表之前就完成格式化。不要指望 Excel 自己去猜它猜出来的结果大概率不合你心意。3.3 设置列宽从一行一列手动调到整个工作簿统一配置直接用aoa_to_sheet生成的工作表列宽是 Excel 默认的。假如你的数据里有“这个月各组销售额汇总统计表”这种超长标题导出后列宽不够文字会被截断或挤成一团用户体验相当糟糕。在 xlsx 库里列宽是一个需要手动配置的字段。准备工作表的列宽配置然后把这份配置赋给工作表的cols属性就可以生效。const setColumnWidths (worksheet, widths) { worksheet[!cols] widths.map(width ({ wch: width })) }调用也很直接在生成工作表之后、写入文件之前执行即可。const worksheet XLSX.utils.aoa_to_sheet(exportData) setColumnWidths(worksheet, [15, 10, 20]) XLSX.utils.book_append_sheet(workbook, worksheet, 用户列表)这里的wch是 xlsx 库约定的列宽单位大体上可以理解成“能放下几个英文字符”。中文相比英文要宽一些所以如果列内容偏中文宽度最好在预估字符数基础上再加 30% 左右。如果你觉得一种列宽配置不够灵活也可以做成动态计算根据每列最长字符串长度自动生成宽度。但我的经验是固定宽度配置在绝大多数场景里够用动态计算反而容易因为中英文混排出现误差。3.4 带合计行、多级表头的复杂业务表格导出管理后台的表格很少是“纯数据”的。最常见的情况是表格底部有一个合计行或者表头分两级。这种结构下表格 DOM 方案反而有优势因为它在页面上渲染成什么样导出就能还原成什么样。但如果你走的是“二维数组拼接”这条路就得自己把这些结构拼出来。多级表头在二维数组里的实现方式是让上一层表头占据多列的位置。const header [ [员工信息, 员工信息, 业绩, 业绩], [姓名, 工号, 第一季度, 第二季度] ] const rows [ [张三, A001, 12000, 15000], [李四, A002, 9800, 13200] ] const worksheet XLSX.utils.aoa_to_sheet([...header, ...rows]) // 合并第一行的前两列以及后两列 worksheet[!merges] [ XLSX.utils.decode_range(A1:B1), XLSX.utils.decode_range(C1:D1) ]decode_range做的事情是把类似A1:B1的区间字符串解析成 xlsx 库内部使用的行列范围对象。有这张表导出后的 Excel 里第一行就会正确合并单元格形成两级表头效果。合计行的思路类似在数据数组末尾追加一行汇总数据。唯一需要注意的是合计行一般要带醒目的样式但社区版 xlsx 对样式的支持有限能做到的也就是“加粗”这种级别。如果你对样式要求很高建议直接把底部的合计行从表格数据源里独立出来单独设置而不是混在数据里。3.5 文件名动态生成与浏览器兼容性处理Excel 文件名看起来是个小事实际这里也有讲究。写死的文件名比如用户列表.xlsx在导出一周后再次点击时浏览器会直接覆盖之前的下载用户想找历史记录都找不到。动态文件名的做法是把导出时间拼进去。const getExportFileName (prefix) { const now new Date() const datePart ${now.getFullYear()}${String(now.getMonth() 1).padStart(2, 0)}${String(now.getDate()).padStart(2, 0)} const timePart ${String(now.getHours()).padStart(2, 0)}${String(now.getMinutes()).padStart(2, 0)} return ${prefix}_${datePart}_${timePart}.xlsx }浏览器兼容性这块XLSX.writeFile在主流现代浏览器里都工作正常。但需要注意一点如果用XLSX.write方法手动控制输出格式再自己创建 Blob 下载就需要留意URL.createObjectURL的可用性并且在下载完成后主动调用URL.revokeObjectURL释放内存。这个问题在老版本浏览器或者特殊 webview 环境里会暴露出来。const downloadBlob (blob, fileName) { const url URL.createObjectURL(blob) const link document.createElement(a) link.href url link.download fileName link.click() URL.revokeObjectURL(url) }这套手动流程在writeFile出现之前是主流写法现在虽然大多数场景用不到但理解了它你就能明白浏览器下载文件的底层原理。3.6 大数据量导出的优化策略分片处理与超时体验管理后台的表格页默认是分页的但导出需求往往是“不分页导全量”。一旦接口把几万、几十万条数据一次性返回页面就可能卡死。卡死的原因是 JS 主线程在同一时间做了太多计算。xlsx 库把二维数组转成工作表的过程本身就是 CPU 密集操作。数据量大了以后这层转换逻辑会阻塞 UI 渲染用户能直观感受到“点完导出页面动不了了”如果正好还开着 loading 动画那动画也会卡成幻灯片。我的做法是分片处理。把大数据量的数组切分成小块每一块放进宏任务里处理让浏览器有空隙去刷新 UI。const exportLargeData async (dataSource, columns, fileName) { const header columns.map(col col.label) const sheetData [header] const CHUNK_SIZE 5000 for (let i 0; i dataSource.length; i CHUNK_SIZE) { const chunk dataSource.slice(i, i CHUNK_SIZE) const rows chunk.map(item columns.map(col item[col.prop])) sheetData.push(...rows) // 让出主线程给渲染留一点时间 await new Promise(resolve setTimeout(resolve, 0)) } const worksheet XLSX.utils.aoa_to_sheet(sheetData) const workbook XLSX.utils.book_new() XLSX.utils.book_append_sheet(workbook, worksheet, Sheet1) XLSX.writeFile(workbook, fileName) }注意这里有两个细节分割块大小建议控制在 3000 到 5000 行之间太小了加频繁调度开销太大了失去分片意义。setTimeout的 0 延迟并不会真的“等待”它只是把回调节器放到下一次事件循环里作用是给浏览器一次执行绘制任务的机会。如果你的数据量大到超过十万行那就不建议前端导了这不是优化策略能解决的瓶颈。前端导出适合的规模是“几万行以内”再往上后端流式生成文件再返回下载链接才是正确赛道。4. 常见问题排查与避坑实录这个功能看似简单但真到工作里你可能会遇到一系列“为什么我导出之后打不开”“为什么导出内容一片乱码”“为什么列宽明明设置了却没效果”这类问题。下面这些是我在自己项目里真踩过的坑整理成速查表按图索骥排查效率很高。4.1 问题速查表现象可能原因解决方案点击导出无反应、浏览器无下载表格 id 不存在DOM 查找失败检查 id 是否写对、是否大小写敏感导出后 Excel 打开提示格式错误使用 HTML 转 .xls 的旁门左道改用 xlsx 库标准写法中文文件名下载后乱码某些浏览器对 UTF-8 文件名支持异常文件名穿插英文字母或使用 download 属性设置合计行、多级表头导出后错位二维数组里行列不完整用!merges声明合并区间数据显示为 #### 或科学计数法列宽不足配置!cols列宽长数字如身份证号变成科学计数法Excel 默认类型推断数据转字符串或在数字前加制表符大数据量导出卡顿主线程同步计算过载分片处理 异步让出主线程导出的日期变成一串数字时间戳未格式化导出前调用日期格式化函数加粗当前行首项显示异常可以排查时逐行列对照。特别提醒“长数字显示成科学计数法”这个坑身份证号、订单编号这类数字一旦超过 Excel 默认的 11 位数字上限就会被转成科学计数法用户根本看不清原值。解决办法是在拼二维数组时把这些字段统一转成字符串const formatIdCard (value) { return ${value} }注意这里不是真的引出双引号而是一个取巧的写法当 xlsx 库检测到某个单元格的值是字符串类型时Excel 就不会再做数字类型转换。所以只要你把原始值通过String()包一层问题就解决了。4.2 导出文件打不开隐藏的 DOM 表格污染问题有次我遇到一个诡异的问题某个表格页导出后的文件个别电脑能打开个别电脑直接报文件损坏。一开始怀疑是浏览器版本差异后来逐行排查数据才发现问题出在表格里某个单元格内容包含了特殊字符这些字符在解析时干扰了工作表的 XML 结构。这类问题在纯 DOM 方案里更容易出现因为表格里可能会有浮层、按钮组、标签这些非文本元素。table_to_sheet会把它们的嵌套结构也一并解析进去导出的数据里混进了多余的层级。我的建议是表格内部放按钮、链接、标签等组件时导出逻辑不要依赖table_to_sheet。改用从数据源直接拼接二维数组的方案从根源上控制导出内容。如果确实需要导出更多内部 DOM 内容先手动提取文本再交给aoa_to_sheet。4.3 导出过程中页面状态管理按钮防重复与 loading 时机导出的数据量一旦上千生成文件的过程需要一点时间。用户手一抖连点三次导出按钮浏览器就会排队下载三个文件既难看又浪费资源。更重要的时机是如果文件生成逻辑是同步的loading 动画是在导出结束后才关闭的用户看到的画面是点击后页面卡住、没有反馈、几分钟后突然下载。更好用的做法是整个过程用异步控制点击导出立即显示 loading。用 setTimeout 或 requestAnimationFrame 把生成文件的同步逻辑包一层让 loading 先渲染到页面。导出完成后关闭 loading。const handleExport async () { if (exporting.value) { ElMessage.warning(正在导出中请勿重复操作) return } exporting.value true try { // 先把提取数据的逻辑切成异步给 loading 渲染时间 await nextTick() exportExcel() } finally { exporting.value false } }这里的exporting是一个 boolean 类型的响应式变量用它做互斥锁比单独禁用按钮更可靠。因为按钮禁用状态在某些极端情况下会被重置而互斥锁的逻辑在函数入口出口都做校验双保险。4.4 关于依赖版本的一个提醒前面提到过npm 上 xlsx 的社区版版本停留在某个历史 commit。它工作正常但有两类情况你需要额外注意第一种如果项目里用了pnpm且开启了严格依赖隔离某些版本下XLSX.writeFile可能会报无法访问全局对象的错误。解决办法是在引入时显式指定默认导出import * as XLSX from xlsx第二种如果你的项目对包体积极其敏感可以按需引入 xlsx 的核心函数import { utils, writeFile } from xlsx这两种方式都能绕过模块解析的边界问题。但说实话遇到这类问题的人并不多如果你的项目没踩过不必提前做这些优化。”5. 从导出 Excel 到通用导出模块的封装思路导出一个表格不难难的是每个页面都要导出时如何不写大量重复代码。如果你负责的项目里有十几个列表页每个页面都抄一份exportExcel函数后续想改文件名规则、加统一 loading、换导出格式就得全局搜索替换维护成本立刻上来了。5.1 抽一个 useExport 组合式函数在 Vue 3 里组合式函数是抽公共逻辑的标准形态。我把导出逻辑封装成useExport导出函数接收列配置和数据源返回一个导出方法页面里只用传参数其他地方完全不用关心 xlsx 内部发生了什么。import { utils, writeFile } from xlsx export const useExport () { const exporting ref(false) const exportData async (columns, dataSource, fileName) { if (exporting.value) { return } exporting.value true try { const header columns.map(col col.label) const rows dataSource.map(item columns.map(col item[col.prop])) const worksheet utils.aoa_to_sheet([header, ...rows]) const workbook utils.book_new() utils.book_append_sheet(workbook, worksheet, Sheet1) writeFile(workbook, fileName) } finally { exporting.value false } } return { exporting, exportData } }页面里的调用方式简洁清晰const { exporting, exportData } useExport() const handleExport () { const columns [ { prop: name, label: 姓名 }, { prop: age, label: 年龄 } ] exportData(columns, tableData.value, 用户列表.xlsx) }这套封装的收益不只是省代码。列配置在页面里往往和 el-table 的 column 定义高度重合理想情况下能做到“一套配置两处使用”页面上展示什么导出就导什么。这个思路建议尽早落实别等到导出逻辑散落十几个页面之后再来重构。写在最后算是我个人摸索这个功能下来的一点体会前端导出 Excel 最迷人的地方在于它把“后端才能干的活”拉到了浏览器里。但正因为入口太简单很多人忽略了数据格式化、大数据量优化、用户体验这些细节。如果你在落地过程中也遇到奇怪的导出问题优先去检查数据源而不是导出函数本身——大部分坑其实都是数据的问题。
阅读完成 · 觉得有帮助?