☰
前端导出Excel实战:ExcelJS+file-saver完整方案与踩坑记录
2026/10/21 13:39:00 网站建设 项目流程

后台管理系统做到一定阶段,总有一个看起来不起眼、但躲不掉的功能:把表格数据导成 Excel。我最近就接了这么个活,需求描述大概只有一句话——“把筛选出来的数据下载成 Excel 表格”,听起来很简单,真做起来却不是调一个接口就能交差的。尤其是当数据来自后端接口、需要在前端动态拼装、还要求表头好看、文件名正常、几万行数据不能卡死页面的时候,方案选型和处理细节就变得很重要。

这篇文章完整复盘一下,我用 exceljs + file-saver 实现后端数据在前端转成 Excel 并下载的整套方案。内容包括技术选型对比、ExcelJS 核心概念、字段映射与清洗、完整通用导出函数、以及我在真实项目里踩过的各种坑。适合正在做中后台项目、被“导出 Excel”功能折腾过、又不想事事麻烦后端的前端同学,也适合刚接触电子表格前端生成的新手。

1. 需求诞生与技术选型

1.1 为什么这个需求更适合放在前端做

很多团队遇到导出功能的第一反应是让后端写一个导出接口,返回一个文件流,前端用 a 标签或者 window.location 直接下载。这个思路不能说错,但实际维护起来很痛苦。

后端生成 Excel 的方案通常是处理类库直接生成 xlsx,再把文件流通过接口返回。这样做有几个绕不开的麻烦:

  • 前端改动列名、样式、字段顺序,都要去求后端改接口,一来一回半天就过去了。
  • 接口直接把文件流吐出来,在大数据量场景下很容易超时,用户只看到几个菊花转圈,体验很差。
  • 接口暴露了文件生成逻辑,权限控制、下载日志、内容安全这些事都得后端单独处理,链路被拉得很长。
  • 如果产品说“导出时要有合计行,标题要加粗,列宽要自适应,还要冻结首行”,后端每次都要重新发布。

前端做导出,数据还是走原来的列表查询接口,只是在拿到 JSON 数据之后,用 exceljs 在浏览器内存里生成 xlsx 二进制,再用 file-saver 触发浏览器下载。列名、样式、排序、合并单元格全由前端控制,后端完全不用感知,一个通用导出函数能覆盖很多页面。从团队协作效率角度看,这个方案天然更有优势。

1.2 exceljs + file-saver 为什么是首选组合

社区里能生成 Excel 的前端库不少,我做了一次快速对比,核心结论如下:

方案优点缺点适合场景
ExcelJS样式能力最全,支持颜色、边框、合并、公式、超链接,浏览器和 Node 都能跑学习成本略高,大数据量时需要做优化中后台数据导出、带复杂表头的报表
SheetJS 社区版解析能力强,老牌项目社区版不支持样式,专业版收费只需简单读取或导出无样式 CSV
CSV 文本导出实现最简单,体积小中文编码易乱码,不支持单元格样式,日期和数字格式难控制临时工具、日志导出
浏览器原生 Blob + a 标签手写下载不依赖第三方下载库各浏览器命名行为不统一,兼容性坑多对兼容性要求不高的内部系统

所以我的选型是:exceljs 负责“生成”,file-saver 负责“下载”。exceljs 生成的 xlsx 本质是一个二进制 Buffer,在浏览器里要转成 Blob,再用 saveAs 触发下载。file-saver 的优势在于帮我们处理了不同浏览器的下载差异,比如 Safari 下的命名问题、部分 Webkit 内核的路径问题,省掉了自己手写那十几行兼容代码的麻烦。

提示:如果项目极简,确实可以用URL.createObjectURL(blob)配合<a download>实现下载。但一旦遇到中文文件名、iframe 场景或者旧版浏览器,file-saver 更稳。与其在别人踩过的坑里再踩一遍,不如直接依赖一个成熟小库。

2. ExcelJS 核心概念拆解

2.1 Workbook、Worksheet、Cell 三层结构

ExcelJS 的对象模型和 Excel 文件结构完全对应,理解三层结构,后面写代码基本不会迷路。

  • Workbook,就是整个 xlsx 文件,相当于日常说的“工作簿”。
  • Worksheet,是工作簿里的一个 Sheet 表格页,一个 Workbook 里可以创建很多个 Worksheet。
  • Cell,是表格里的单元格,通过行、列定位。

我用一个生活化类比来理解:Workbook 像一本多页记事本,每一页是一个 Worksheet,每页上的格子是 Cell。操作 Excel 文件的第一步永远是new ExcelJS.Workbook(),然后 addWorksheet,再往 worksheet 里填充内容。

实际编码中最常用的几句是:

const workbook = new ExcelJS.Workbook(); const worksheet = workbook.addWorksheet('数据报表'); // 通过字母定位单元格 worksheet.getCell('A1').value = '标题'; // 通过行列号定位单元格,行列号从 1 开始 worksheet.getCell(2, 1).value = 100; // 获取整行或整列再操作 const row2 = worksheet.getRow(2); const colA = worksheet.getColumn('A');

这里我多说一句,用行列号比用字母定位更省心。写循环遍历的时候,字母坐标还要转数字,行列号直接就是数字,配合 for 循环非常顺手。

2.2 columns 列定义:表头和字段的映射入口

这是 exceljs 里最容易忽略却最核心的概念。columns 是一个数组,每个元素定义一列:

worksheet.columns = [ { header: '姓名', key: 'name', width: 18 }, { header: '部门', key: 'dept', width: 24 }, { header: '月薪', key: 'salary', width: 14 }, ];

header 是表头要显示的文字,key 是后续 addRow 时对象属性名,width 是列宽。设置好 columns 之后,第一行会自动变成表头行,紧接着 addRow,数据会从第二行开始排列。

这个机制的好处是:你只需要在 columns 里声明一次字段与表头的映射关系,之后加数据时不用关心列顺序,exceljs 会按 key 自动把数据放到对应的列里。如果后端返回的是一个按 key 排好的数组对象,直接worksheet.addRows(data)就能一次性写入大量数据。

有个细节很容易踩到:columns 必须在 addRow 之前定义。如果你先 addRow 再设置 columns,表头虽然会出现,但原有的数据行可能不会自动和 columns 对齐,甚至某些环境下表头会覆盖掉第一行真实数据。这个顺序问题,后面排查章节会再展开。

2.3 单元格样式模型与常用属性

ExcelJS 的样式体系很完整,主要分为字体、填充、边框、对齐、数字格式五大类。每一类都是一个配置对象,直接赋值给 cell 或者行对象。

字体用在表头加粗、标题变色:

cell.font = { name: '微软雅黑', size: 11, bold: true, color: { argb: 'FFFFFFFF' } };

填充是背景色,注意一定要写完整结构:

cell.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FF4472C4' } };

边框需要四边分别声明:

cell.border = { top: { style: 'thin', color: { argb: 'FFD9D9D9' } }, left: { style: 'thin', color: { argb: 'FFD9D9D9' } }, bottom: { style: 'thin', color: { argb: 'FFD9D9D9' } }, right: { style: 'thin', color: { argb: 'FFD9D9D9' } } };

对齐方式支持水平、垂直和自动换行:

cell.alignment = { horizontal: 'center', vertical: 'middle', wrapText: true };

数字格式也值得一提,比如保留两位小数、千分位:

cell.numFmt = '#,##0.00';

样式不光可以单独设置一个单元格,也可以遍历一行、一列甚至一个矩形区域批量设置。实际项目里最典型的做法是:先写完所有数据,再对表头行做一次统一美化,最后对正文区域设置边框。

3. 数据获取与字段映射

3.1 后端数据形态与前端处理逻辑

前端导出 Excel 的第一步,是拿到后端返回的“干净”数据。真实项目里后端接口返回的结构五花八门,最常见的是下面两种:

// 形式一:直接返回数组 { code: 0, data: [ { id: 1, name: '张三' }, { id: 2, name: '李四' } ] } // 形式二:分页对象 { code: 0, data: { list: [ ... ], total: 100, page: 1, pageSize: 20 } }

不管哪种,我们最终要做的是把 data 里的数组抽出来,转成导出函数需要的格式。这个步骤看起来简单,但我建议一定不要跳过,直接在 addRow 时现想办法处理字段,很容易写出又丑又难维护的代码。

我习惯的做法是,先请求接口,然后在调用导出函数之前把数据做一次标准化:

const res = await fetch('/api/user/list?page=1&pageSize=9999'); const json = await res.json(); const rawData = json.data.list || json.data || []; const exportData = rawData.map(item => mapExportRow(item));

这样导出函数只接收一个标准数组,内部不用关心字段到底是从哪里来的,后续复用起来特别舒服。

3.2 字段映射、类型清洗与文本型数字保留

后端字段大多是 snake_case,比如 hire_date、department_name,而前端表格显示时要变成“入职日期”“部门名称”。字段映射本质是给导出函数传入一个 columns 配置,同时把数据对象做一次换名。

我比较推荐这种显式映射:

function mapExportRow(row) { return { name: row.user_name || '--', dept: row.department_name || '--', hireDate: formatDate(row.hire_date), salary: Number(row.salary || 0), }; }

这里有几个经验想分享。

日期字段千万不要拿到什么就写什么。后端可能是 "2024-01-01 10:00:00" 字符串,也可能是一个时间戳数字。写进 Excel 之前统一格式化,比如转成 "YYYY-MM-DD" 或者直接转成 Date 对象,Excel 才能正确识别为日期。

数字字段尽量保持 Number 类型。有些人图省事把所有值都转成字符串,结果导出后数字列无法求和,也无法排序。更麻烦的是金额列会有尾差,显示出来的东西别人不敢信。

文本型数字要特别小心,比如身份证号、手机号、订单编号。如果你在代码里写cell.value = 12345678901234567890,Excel 打开后大概率变成科学计数法,看着很吓人。稳妥做法是强制把单元格类型设置为文本,或者用 exceljs 的对象语法:

cell.value = { text: '8232922222222222' };

这样既保留了原始字符串内容,又不会触发 Excel 的科学计数法自动转换。这个细节我推荐在写通用导出函数时,直接对 String 类型的字段做统一处理。

另外空值也要处理。后端返回 null、undefined、空字符串的情况很常见,直接在单元格里写 null 可能变成空白,部分旧版本 Excel 打开时会提示文件异常。我习惯统一转成 '--' 或者空字符串,保证单元格内容始终是有效值。

4. 完整实操:从零构建导出功能

4.1 安装依赖与引入方式

先安两个依赖:

npm install exceljs file-saver

引入的姿势按项目环境来。现代前端工程用 ESM 或 CommonJS:

import ExcelJS from 'exceljs'; import { saveAs } from 'file-saver';

如果项目没有模块打包工具,比如纯 HTML 页面,也可以使用 script 标签引入:

<script src="https://unpkg.com/exceljs/dist/exceljs.min.js"></script> <script src="https://unpkg.com/file-saver/dist/FileSaver.min.js"></script>

这时全局会挂载 ExcelJS 和 saveAs 两个变量,后面代码基本一样。唯一要注意的是 CDN 引入时,ExcelJS 的默认导出方式有时不太一致,建议用window.ExcelJS显式访问。

4.2 构建 Workbook 与写入数据

下面这段代码是最小可用版本,先跑通再谈美化:

async function exportSimple(list) { const workbook = new ExcelJS.Workbook(); const worksheet = workbook.addWorksheet('数据报表', { views: [{ state: 'frozen', ySplit: 1 }] }); worksheet.columns = [ { header: '姓名', key: 'name', width: 18 }, { header: '部门', key: 'dept', width: 24 }, { header: '入职日期', key: 'hireDate', width: 16 }, { header: '月薪', key: 'salary', width: 14 }, ]; list.forEach(item => { worksheet.addRow({ name: item.name, dept: item.dept, hireDate: item.hireDate, salary: item.salary, }); }); const buffer = await workbook.xlsx.writeBuffer(); const blob = new Blob([buffer], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', }); saveAs(blob, '数据报表.xlsx'); }

这里有两个容易错的地方。

worksheet 创建时传入的 views 数组是冻结首行用的。ySplit: 1表示冻结前 1 行,之后用户拖动滚动条时,表头永远固定在顶部。导出报表特别需要这个设置。

writeBuffer()在浏览器里返回的是一个 ArrayBuffer,在 Node 环境返回 Buffer,两者都可以直接作为 Blob 的输入。Blob 的 type 必须是 xlsx 的官方 MIME 类型,少了这个类型,部分浏览器会把文件当作文本下载,双击打不开。

4.3 表头样式与整体美化

数据写入之后,接下来是给表头做统一美化。我最常做的是四方格:加粗、白字、深蓝底、居中对齐、加边框。

const headerRow = worksheet.getRow(1); headerRow.height = 24; headerRow.eachCell(cell => { cell.font = { name: '微软雅黑', size: 11, bold: true, color: { argb: 'FFFFFFFF' } }; cell.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FF4472C4' } }; cell.alignment = { horizontal: 'center', vertical: 'middle' }; cell.border = { top: { style: 'thin', color: { argb: 'FFD9D9D9' } }, left: { style: 'thin', color: { argb: 'FFD9D9D9' } }, bottom: { style: 'thin', color: { argb: 'FFD9D9D9' } }, right: { style: 'thin', color: { argb: 'FFD9D9D9' } }, }; });

正文区域还可以加一个自动过滤按钮,方便用户拿到文件后直接排序筛选:

worksheet.autoFilter = { from: 'A1', to: `D${list.length + 1}`, };

注意 autoFilter 的下标要用${list.length + 1},因为第一行是表头。这个功能在数据量多的报表里非常实用,用户不用自己手动加筛选。

正文数据如果也想加边框,可以遍历数据区域:

const endRow = list.length + 1; for (let rowIndex = 2; rowIndex <= endRow; rowIndex++) { const row = worksheet.getRow(rowIndex); row.eachCell(cell => { cell.border = { top: { style: 'thin', color: { argb: 'FFD9D9D9' } }, left: { style: 'thin', color: { argb: 'FFD9D9D9' } }, bottom: { style: 'thin', color: { argb: 'FFD9D9D9' } }, right: { style: 'thin', color: { argb: 'FFD9D9D9' } }, }; cell.alignment = { horizontal: 'left', vertical: 'middle' }; }); }

这里每行都遍历一次 eachCell,如果数据量很大可能有点慢,后面我会讲优化策略。

4.4 导出 Blob 并触发下载

生成二进制、转 Blob、saveAs 下载,这三步是核心闭环。写成一个函数方便复用:

async function writeAndDownload(workbook, fileName) { const buffer = await workbook.xlsx.writeBuffer(); const blob = new Blob([buffer], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', }); saveAs(blob, fileName); }

saveAs 与 Blob 之间其实也可以简化。file-saver 内部自己会按 needsFallback 逻辑做兼容处理,但在现代 Chrome、Edge、Firefox 上,它都是优先创建临时 a 标签并触发 click,体验是一致的。

下载后的文件名后缀我建议固定.xlsx。如果你自己拼文件名,注意用文件名的核心部分,不要带上接口里传回来那种带奇怪前缀的字符串,否则用户导出的文件叫“xxx_1_2345678.xlsx”,体验真不太行。

4.5 封装成通用导出函数

把前面所有代码收拢成一个通用函数,业务侧调用就非常清爽了。我平时封装的长这样:

import ExcelJS from 'exceljs'; import { saveAs } from 'file-saver'; export async function exportExcel({ columns, data, sheetName = 'Sheet1', fileName = '导出数据' }) { const workbook = new ExcelJS.Workbook(); const worksheet = workbook.addWorksheet(sheetName, { views: [{ state: 'frozen', ySplit: 1 }], }); worksheet.columns = columns.map(col => ({ header: col.header, key: col.key, width: col.width || 16, })); worksheet.addRows(data); const headerRow = worksheet.getRow(1); headerRow.height = 24; headerRow.eachCell(cell => { cell.font = { name: '微软雅黑', size: 11, bold: true, color: { argb: 'FFFFFFFF' } }; cell.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FF4472C4' } }; cell.alignment = { horizontal: 'center', vertical: 'middle' }; cell.border = { top: { style: 'thin', color: { argb: 'FFDCE6F1' } }, left: { style: 'thin', color: { argb: 'FFDCE6F1' } }, bottom: { style: 'thin', color: { argb: 'FFDCE6F1' } }, right: { style: 'thin', color: { argb: 'FFDCE6F1' } }, }; }); const buffer = await workbook.xlsx.writeBuffer(); const blob = new Blob([buffer], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', }); saveAs(blob, `${fileName}.xlsx`); }

调用侧只要维护一份 columns 配置,再把数据塞进来即可:

const columns = [ { header: '姓名', key: 'name', width: 18 }, { header: '部门', key: 'dept', width: 24 }, { header: '入职日期', key: 'hireDate', width: 16 }, { header: '月薪', key: 'salary', width: 14 }, ]; await exportExcel({ columns, data: exportData, sheetName: '人员情况', fileName: '人员花名册', });

这个函数我一般会放在 utils 目录下,所有页面共用一份,后续要改全局样式只改一个地方。第一次写的时候可以简单点,后续加需求在函数内部扩展就行。

4.6 进阶:公式、超链接与合并单元格

导出功能不总是那么朴素。我接过一个需求,要在导出表里放“部门汇总”,数据里没有现成结果,这时候直接写公式最合适。

exceljs 支持给单元格赋公式:

const sheet = workbook.addWorksheet('统计'); // 在 C10 单元格写入 SUM 公式 sheet.getCell('C10').value = { formula: 'SUM(C2:C9)' };

当用户打开 Excel 时,公式会自动计算结果,不需要我们提前算好。这在报表汇总场景里特别好用,但要小心公式引用范围别写错。

超链接也是表格里常见的。如果单元格内容是一个官网地址,我们希望用户点击能跳转,同时单元格里显示一个友好的文字:

cell.value = { text: '点击访问', hyperlink: 'https://example.com', }; cell.style = { font: { color: { argb: 'FF0563C1' }, underline: true }, };

合并单元格更好理解:

worksheet.mergeCells('A1:C1'); worksheet.getCell('A1').value = '这是一个合并标题';

不要以为合并了就完事,合并区域里的边框和样式经常要单独补,后面排错章节我会给一个完整范本。

5. 常见问题与排查技巧实录

5.1 中文文件名与内容乱码

我遇到最多的一个情况是:Excel 导出后,文件名在浏览器里看是正常的,下载到本地变成一串乱码。这主要出现在部分 Webkit 内核浏览器中,file-saver 在某些版本里对中文文件名支持得不是特别好。

面对这个问题,我的经验是:

  • 优先确保调用saveAs(blob, '中文名.xlsx')时文件名是纯字符串,不要在文件名里拼接时间戳或接口返回的乱码字段。
  • 如果还是乱码,可以试试对文件名做 encodeURIComponent 转换。不过这个属于 workaround,不同浏览器表现不一样。
  • 内容乱码的情况反而很少,因为 xlsx 内部本质是 UTF-8 编码的 XML 打包文件。如果单元格里中文乱码,根源基本是后端接口返回的数据本身已经乱码,先检查请求返回的 JSON 是否正确。

简单说:文件名乱码优先确认 file-saver 版本和浏览器,内容乱码优先排查接口数据。

5.2 日期数据少 8 小时的时区坑

日期是最容易翻车的地方之一。exceljs 在把 Date 对象写入单元格时,会按 UTC 时间序列化。直接用new Date('2024-01-01')生成日期,在部分时区环境下写入 Excel,打开后可能会显示成 2023-12-31 16:00,看起来像是少了一天。

我常用的解决办法是手动构造一个不含时区偏移的 UTC 日期:

function toExcelDate(date) { return new Date( Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()) ); }

然后在写入数据时统一走这个函数。更简单粗暴的做法是直接把日期格式化成 "YYYY-MM-DD" 字符串写入单元格,同时给单元格设一个文本格式。这样 Excel 打开后不会随意做日期转换,只是单元格类型变成文本了。如果你希望单元格保留日期类型,推荐用第一种方式。

5.3 样式“失灵”的三种真相

我在周期性的疑难问题里总结过,所谓“样式不生效”,绝大多数是以下几个原因之一。

第一,columns 设置顺序问题。前面提过,如果你先 addRow 再设置 columns,表头可能覆盖到原有数据,或者已经写入的数据样式全部丢失。正确的顺序永远是:定义 columns → 写入数据 → 设置样式。

第二,fill 结构不完整。给单元格设置背景色时漏掉了type: 'pattern'或者pattern: 'solid',Excel 拿到一个不完整的填充对象,可能直接忽略。最简单的方式是永远复制一份完整的 fill 对象。

第三,边框设置不完整。Excel 的每个单元格有四条边,你只设置 top 和 bottom,视觉上左右两边的线就是断开的。特别是合并单元格场景,合并后只能看到左上角单元格的样式,其他区域全是白的,必须对合并区域内的每个单元格单独设置边框。

排查顺序建议是:先确认单元格能不能取到,其次确认设置对象结构完整,最后看浏览器控制台有没有对象属性拼写错误。这三点查完,九成样式问题都能定位。

5.4 大数据量导出卡顿的优化手段

几万行数据在前端生成 xlsx,内存和 CPU 压力都不小。我的经验是以下几招。

尽量用addRows批量写入,而不是循环里逐个addRow。exceljs 内部批量处理比单条写入节省大量开销。

样式设置不要逐格套用。如果几百行数据每行都做 eachCell,遍历次数会非常恐怖。改成分区域统一处理,或者只给表头加完整样式,正文行只加文字对齐,效率会高很多。

冻结首行和 autoFilter 会额外消耗一些计算资源,大表导出时可以只保留冻结首行,关掉自动筛选。

真的遇到十万行以上,前端方案就比较吃力了。这时候我的建议是分多个 Sheet,每 3 万行放一个 Sheet,并且明确告诉产品“大表建议定时由后端批量生成”。前端不该硬扛的场景,没必要为了技术实现自我感动。

5.5 合并单元格边框补全与自动换行

合并单元格的样式补全是经典坑。worksheet.mergeCells('A1:C1')只是把区域合并成一个逻辑单元格,但 A2、B2、C2 这些原有单元格的边框并不会自动统一。我一般这样处理:

ws.mergeCells('A1:C1'); ws.getCell('A1').value = '合并标题'; for (let col = 1; col <= 3; col++) { const cell = ws.getCell(1, col); cell.border = { top: { style: 'thin', color: { argb: 'FF000000' } }, left: { style: 'thin', color: { argb: 'FF000000' } }, bottom: { style: 'thin', color: { argb: 'FF000000' } }, right: { style: 'thin', color: { argb: 'FF000000' } }, }; cell.alignment = { horizontal: 'center', vertical: 'middle' }; }

这个循环把合并区域里每个单元格的边框都补上,视觉上才能看到完整表格框线。

自动换行是另一个容易忽略的点。当单元格内容太长,如果只设置wrapText: true,行高不会自动按内容高度增加,展示出来还是截断的。要么手动给这一行设置一个足够大的 height,要么在写完数据后统一按内容估算行高。日常场景我倾向于先估算最大文本长度,手动给行高一个合适值,省得用户打开文件后还得自己拖行高。

6. 导出体验优化与整体回顾

6.1 全量数据获取与分页接口的循环拉取

列表页接口通常是分页的,而导出需要全量数据。最稳妥的做法是:先请求一次不带条件的统计接口拿 total,然后按 pageSize 循环拉取,拼出全量数组。

我这里给一个简化示例:

async function fetchAllData(url) { const all = []; const pageSize = 500; const firstRes = await fetch(`${url}&page=1&pageSize=${pageSize}`); const firstJson = await firstRes.json(); const total = firstJson.data.total || firstJson.data.list.length; all.push(...(firstJson.data.list || [])); const totalPages = Math.ceil(total / pageSize); for (let page = 2; page <= totalPages; page++) { const res = await fetch(`${url}&page=${page}&pageSize=${pageSize}`); const json = await res.json(); all.push(...(json.data.list || [])); } return all; }

注意循环里每一页都要做一下错误判断,避免某一页失败导致整体中断,给出“第几页数据拉取失败”的提示比静默吞掉要好得多。

6.2 按钮状态、错误兜底与进度提示

导出按钮点击后,用户看到的第一反馈很重要。我在开发时专门加了一个状态管理:

  • 点击按钮先 disable,按钮文字变成“正在导出...”,防止用户连续点击。
  • 数据拉取和 writeBuffer 都是异步操作,期间页面不能卡死。
  • 如果接口报错,showError 提示“数据拉取失败,请稍后重试”。
  • 如果导出成功,再恢复按钮。

代码片段看起来是这样:

btn.disabled = true; btn.textContent = '正在导出...'; try { await handleExport(); } catch (err) { console.error(err); showToast('导出失败,请稍后重试'); } finally { btn.disabled = false; btn.textContent = '导出 Excel'; }

这个小改动对用户体感提升非常明显,很多人会连续点导出按钮,导致同一时间发起多个相同请求,按钮状态能有效避免这种浪费。

6.3 我对这套方案的整体感受

exceljs + file-saver 这套组合在中后台导出场景里已经是我心里的第一顺位方案。它核心胜在样式可控、前后端解耦、代码复用度高。真要说缺点,可能就是大表格性能需要额外优化,以及 API 文档有些细节比较绕,得靠实际踩坑积累经验。

我个人现在做每个新项目,都会在最开始就把通用导出函数、字段映射工具、日期格式化函数三个工具准备好。所有列表页导出都走同一套流程,产品提出新列、新样式,我只需要改 columns 数组,不用每个页面各写一套专用代码。这种“一次封装、到处复用”的做事方式,确实能让以后的维护轻松不少。

最后再分享一个小技巧:如果后端数据里嵌套了对象,比如 user: { name, avatar },不要直接在 addRow 时传 user 对象,exceljs 只能读取一层属性。提前拍平成一维结构,导出函数才不容易在某个字段上悄悄丢掉内容。希望这篇踩坑记录能帮你的导出功能少掉几次发际线。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询