ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

xlsx库实操指南:前端Excel导入导出与避坑总结

xlsx库实操指南:前端Excel导入导出与避坑总结 1. 为什么要单独沉淀一篇库的操作我又为什么死死盯住 xlsx如果你和我一样平时在维护自己的技术笔记肯定会理解沉淀中这三个字的价值。它不是一句免责声明而是说明这块内容还在持续积累、还会继续补全。今天这篇就是我笔记系列里的第 02 篇库的操作。这里的库不是数据库的库而是前端日常开发里要频繁接触的第三方工具库。最近我在整理自己写过的 Excel 导入导出需求时发现一个很明显的检索趋势很多人都在搜操作 excel 的 js 工具库而搜索结果里出现频率最高的就是 xlsx。这个库的全名其实是 SheetJS但大家都习惯了直接用包名 xlsx 来称呼它。我决定把它的常见用法、坑点和选型思路全部沉淀下来一方面是为了自己下次不要再翻官网另一方面也是给同样被 Excel 处理折磨过的同事和网友一条相对完整的上手路径。1.1 为什么先说选型而不是直接甩 API很多人一上来就直接npm install xlsx然后对着文档抄代码等写到我要给单元格加个颜色的时候才发现这个库的社区版本根本不提供样式能力当场傻眼。所以我建议先花两分钟搞清楚你做 Excel 到底是要数据交换还是报表美化。如果你只是需要把后端返回的 JSON 转成 Excel 下载或者把用户上传的 Excel 表格内容读进页面里做展示、再做数据清洗那么 xlsx 绝对是最省事的方案。它的核心优势是轻、快、API 简洁、格式转换能力强一个库就能在数组、JSON、CSV、HTML 表格和 xlsx 文件之间来回倒腾。但如果你要做的是那种带标题行合并、单元格背景色、边框线、页眉页脚的复杂报表xlsx 社区版就无能为力了。这时候我更推荐用 ExcelJS 这类偏样式的库。它支持设置字体、颜色、边框、合并单元格做出来的文件更像给人看的报表而不是给系统读的数据。我给自己定了一个很简单的判断标准数据落地用 xlsx报表设计用 ExcelJS。这个原则帮我避免了很多不必要的返工。1.2 一个最小示例先让 Excel 在手里走一遍不扯太多理论先上一个最简例子让你知道这个库用起来到底是什么手感。这一步跑通了后面所有概念都能顺藤摸瓜。import * as XLSX from xlsx; const data [ { name: 张三, age: 30, city: 上海 }, { name: 李四, age: 25, city: 杭州 }, { name: 王五, age: 28, city: 北京 } ]; // 对象数组转 worksheet const worksheet XLSX.utils.json_to_sheet(data); // 新建工作簿把工作表塞进去 const workbook XLSX.utils.book_new(); XLSX.utils.book_append_sheet(workbook, worksheet, 人员名单); // 直接生成并下载/保存 xlsx 文件 XLSX.writeFile(workbook, 人员名单.xlsx);这段代码其实已经覆盖了 xlsx 最核心的三个能力utils负责各种格式转换book_new和book_append_sheet负责组装工作簿writeFile负责把内存中的数据变成真实文件。浏览器里运行这段代码会自动触发文件下载在 Node 环境里运行会直接在当前目录写入文件。提示在浏览器里使用writeFile它本质上也是创建一个 Blob 然后触发下载所以文件名里的中文大多数浏览器都能正常处理。如果你遇到极端情况下的文件名乱码再考虑手动创建 Blob 并接管下载逻辑。2. 读取 Excel从 HTML 文件到后端文件的三条常规路径读取和写入相比读取稍微麻烦一点因为你要先把外部文件变成 xlsx 能识别的数据流再交给它解析。这里最绕的其实是用户到底给了你什么形式的内容。2.1 浏览器里上传文件后怎么解析前端最常见的场景是用户拖拽或选择了一个.xlsx文件我们需要拿到里面的数据。核心代码写出来并不长但每一步都有讲究。const input document.getElementById(upload); input.addEventListener(change, async (e) { const file e.target.files[0]; const buffer await file.arrayBuffer(); const workbook XLSX.read(buffer, { type: array }); const firstSheetName workbook.SheetNames[0]; const worksheet workbook.Sheets[firstSheetName]; const rows XLSX.utils.sheet_to_json(worksheet); console.log(rows); });这里的type: array很关键它告诉 xlsx 你传进去的是一个 ArrayBuffer。我已经见过太多次有人传了 ArrayBuffer 却忘了写type或者写了type: buffer导致解析失败。记住对应关系就行文件转成了 ArrayBuffer就配type: array如果是 Node 里的 Buffer就配type: buffer如果是 Base64 字符串就配type: base64。workbook.SheetNames返回的是一个数组因为一个 xlsx 文件可以包含多个工作表。workbook.Sheets则是一个对象键是工作表名称值是对应的工作表对象。大多数业务逻辑都只关心第一个工作表所以上面的写法足够用了。2.2 远程 URL、Base64 和 Node 本地路径接口直接返回一个 Excel 文件流的情况也很常见。前端用 fetch 拿文件时优先考虑拿 ArrayBuffer再走和上传一样的解析流程。const res await fetch(/api/download-template); const data await res.arrayBuffer(); const workbook XLSX.read(data, { type: array });有时候后端为了绕过文件下载的一些限制会把文件内容转成 Base64 字符串塞在 JSON 接口里返回。这种就简单了const res await fetch(/api/excel-base64); const json await res.json(); const workbook XLSX.read(json.base64, { type: base64 });Node 后端自己处理 Excel 时可以用readFile直接读路径这是最省事的方式const workbook XLSX.readFile(/tmp/report.xlsx); const sheet workbook.Sheets[workbook.SheetNames[0]]; const rows XLSX.utils.sheet_to_json(sheet);2.3 sheet_to_json 之后的脏数据清洗sheet_to_json看起来是一个一步到位的转换函数它默认会把第一行当作表头然后返回一个对象数组。比如 Excel 里第一行是姓名、年龄、城市后面每行数据就会变成{ 姓名: 张三, 年龄: 30, 城市: 上海 }。但真实世界的 Excel 远没有这么规整。我统计过自己经手的项目至少有一半的上传表格存在以下几种问题表头有空值、数据中间夹着空行、某些列整列是 Excel 公式但没算好、明明是数字却被 Excel 存成了文本、或者是字符串里带着肉眼看不到的换行和空格。所以我在实际项目中从来不会直接拿sheet_to_json的结果去存库而是会先做一层清洗const rows XLSX.utils.sheet_to_json(worksheet, { defval: }); const cleanRows rows .filter((row) row[姓名] || row[年龄] || row[城市]) .map((row) ({ name: String(row[姓名] || ).trim(), age: Number(row[年龄]) || 0, city: String(row[城市] || ).trim() }));这里用了defval: 意思是如果某个单元格是空的就补一个空字符串而不是让它在对象里消失。这样后面处理时取值就不会出现undefined导致的一连串诡异报错。注意如果 Excel 内容里没有表头或者你想拿到的就是一个二维数组可以用XLSX.utils.sheet_to_json(worksheet, { header: 1 })它会返回[[姓名,年龄],[张三,30]]这样的数组结构。这个模式在做批量导入解析时非常有用。3. 写入 Excel从对象数组到指定单元格的三种粒度写入其实比读取更常用因为导出报表几乎是每个管理后台的标配功能。xlsx 给出的工具函数非常丰富我习惯按数据形态分成三种写法。3.1 对象数组直接用 json_to_sheet大多数后端接口返回的都是对象数组所以json_to_sheet是使用率最高的函数const orders [ { id: A1001, customer: 张三, amount: 199 }, { id: A1002, customer: 李四, amount: 299 } ]; const worksheet XLSX.utils.json_to_sheet(orders); const workbook XLSX.utils.book_new(); XLSX.utils.book_append_sheet(workbook, worksheet, 订单明细); XLSX.writeFile(workbook, 订单.xlsx);这种写法足够应付 90% 的导出需求。需要注意的一点是json_to_sheet生成的列顺序取决于对象键的枚举顺序。如果你希望 Excel 里的列顺序明确可以在接口返回前就排好或者在写入前先重新构造一个按业务列顺序排列的对象数组。多个工作表也可以一次导出比如一个文件里同时放用户表和订单表const userSheet XLSX.utils.json_to_sheet(users); const orderSheet XLSX.utils.json_to_sheet(orders); const workbook XLSX.utils.book_new(); XLSX.utils.book_append_sheet(workbook, userSheet, 用户); XLSX.utils.book_append_sheet(workbook, orderSheet, 订单); XLSX.writeFile(workbook, 报表汇总.xlsx);3.2 精确控制单元格位置时用 aoa_to_sheet如果表格结构比较复杂比如第一行是标题第二行是表头第三行以下才是数据用对象数组就不太方便。这时我喜欢用aoa_to_sheet它接收的是数组的数组每一行一个数组位置完全可控。const worksheet XLSX.utils.aoa_to_sheet([ [供应商对账单], [订单号, 客户名称, 金额, 备注], [A1001, 张三, 199, 已结清], [A1002, 李四, 299, 待支付] ]); const workbook XLSX.utils.book_new(); XLSX.utils.book_append_sheet(workbook, worksheet, 对账单); XLSX.writeFile(workbook, 对账单.xlsx);还有更底层的方式直接给某个单元格赋值。xlsx 的工作表对象把单元格当作键值对处理键是A1、B2这种坐标值是一个描述单元格内容的对象比如{ t: s, v: 字符串内容 }。其中t是类型s代表字符串n代表数字b代表布尔值d代表日期。const worksheet XLSX.utils.aoa_to_sheet([ [名称, 数量] ]); worksheet[A3] { t: s, v: 手动补充项 }; worksheet[B3] { t: n, v: 10 };如果你要做的是动态表格我建议优先用aoa_to_sheet除非你自己在拼坐标越拼越晕。像上面这样手动维护单元格坐标只适合少量定位写入或补丁式修改。3.3 浏览器端下载和 Node 端落盘的正确打开方式writeFile很方便但它是走默认输出方式。浏览器里它会触发下载Node 里它会写到当前工作目录。如果你需要自己控制文件输出的格式可以用write方法拿到内存里的数据再决定怎么处理。浏览器端比较完整的下载逻辑是这样的function downloadWorkbook(workbook, fileName) { const out XLSX.write(workbook, { bookType: xlsx, type: array }); const blob new Blob([out], { type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet }); const link document.createElement(a); link.href URL.createObjectURL(blob); link.download fileName; link.click(); URL.revokeObjectURL(link.href); }Node 端拿到{ type: buffer }之后可以直接写文件const out XLSX.write(workbook, { bookType: xlsx, type: buffer }); const fs require(fs); fs.writeFileSync(/tmp/output.xlsx, out);bookType除了xlsx还可以填xls、csv、ods等格式。需要兼容老版本 Office 的时候改成bookType: xls就能输出老格式。4. 真实项目里最容易翻车的几个隐蔽问题工具函数来回就那么几个真正让人头疼的从来不是 API 会不会用而是数据在 Excel 和代码之间来回转换时产生的各种想当然。4.1 身份证、手机号数字列的精度和科学计数法这是我在导出功能里踩过最多次的坑身份证号明明是 18 位用 xlsx 导出来Excel 一打开就变成了1.23456E17用单元格再看发现末尾几位全成了 0。原因很简单JavaScript 处理超过Number.MAX_SAFE_INTEGER的数字时会丢失精度而 Excel 本身对大数字列也倾向于按科学计数法显示。解决思路只有一个凡是身份证、手机号、银行卡号这类字段在写入 Excel 之前必须转成字符串并且写入单元格的类型必须是字符串类型。json_to_sheet在处理对象数组时如果某个字段值是数字它就会按数字写入。所以我在导出前会专门做一轮字段转换const exportData data.map((item) ({ ...item, idCard: String(item.idCard), phone: ${item.phone}, bankCard: String(item.bankCard) }));给数字前面加单引号是让 Excel 把它当文本处理的偏方。如果你用XLSX.writeFile导出后打开文件看到数字左侧有个绿色小三角那通常就是 Excel 在提示这个数字是以文本形式存储的这是正常现象业务上可接受。4.2 日期被读成数字以及时区导致的偏移Excel 内部存储日期用的是序列数比如 2024 年 1 月 1 日可能对应数字45292。用 xlsx 读取时默认会拿到这些序列数而不是你预期的日期字符串。要拿到 Date 对象需要在读取时开启cellDates: trueconst workbook XLSX.read(buffer, { type: array, cellDates: true });但cellDates只是把单元格解析成 JS 的 Date 对象后续你把它转成字符串时还是得自己控制格式。用原生 Date 转换时还要注意时区问题因为new Date(2024-01-01)在部分浏览器里会被当成 UTC 零点导致本地时间变成前一天或后一天。我在实际项目里习惯引入 dayjs 这类库统一处理而不是直接用toLocaleDateStringconst rawDate row[下单时间]; // 可能已经用 cellDates 转成了 Date const dateStr dayjs(rawDate).format(YYYY-MM-DD HH:mm:ss);如果是写入 Excel我更倾向于先把日期全部格式化成字符串再写入。虽然 xlsx 支持日期类型但在真实业务里后端更愿意看到明确的2024-01-01 10:30:00而不是一个需要前端再次解析的时间对象。4.3 大批量数据导出时的内存和耗时xlsx 社区版的所有转换都是内存操作。当你导出几万行甚至几十万行数据时浏览器页面可能会卡到完全没法操作Node 端也可能消耗大量内存。这个库在纯数据交换场景下性能算不错但毕竟不是流式处理方案。我压测过的一个典型场景前端拿到 10 万行、每行 20 列的数据通过json_to_sheet生成 Excel整个过程大约会花费一两秒内存峰值可能在几百 MB 级别。如果是本地桌面工具这个量级还能接受但如果是网页端给普通用户使用最好在数据量超过 5 万行时就走后端导出并且给后端加导出任务队列和文件留存的机制。如果必须在前端处理大量数据我有个小建议一次性构建好完整数组再一次性调用json_to_sheet不要在循环里频繁修改 worksheet 对象。这个库的单元格操作虽然在内存里进行但频繁写对象属性会产生大量开销分批拼接数组反而更快。4.4 样式、合并单元格、CSV 的 BOM 问题先说样式。SheetJS 的社区版不处理cellStyles你就算在源码里写了颜色、边框导出后也不会生效。如果项目需求明确要求单元格要红色、表头要加粗、要有边框线尽快换用 ExcelJS不要试图用 xlsx 硬凑。我见过有人为了在 xlsx 上实现样式给 source code 打补丁后来升级版本又全线崩掉这种事得不偿失。合并单元格倒是可以通过工作表的!merges属性手动写入但同样只在导出时生效读取时 xlsx 也不会帮你还原成有意义的逻辑结构const worksheet XLSX.utils.aoa_to_sheet([ [对账单, , , ], [订单号, 客户, 金额, 日期] ]); worksheet[!merges] [ { s: { r: 0, c: 0 }, e: { r: 0, c: 3 } } ];最后说一个冷门但容易让用户直接投诉的问题导出 CSV 文件时如果里面包含中文用 Excel 打开经常会乱码。原因是 UTF-8 编码的 CSV 缺少 BOM 标记Excel 默认会用本地编码读取。解决办法是在 CSV 文本前面加一个\uFEFF字符const csv \uFEFF XLSX.utils.sheet_to_csv(worksheet); const blob new Blob([csv], { type: text/csv;charsetutf-8 });这只是很小的一个改动但它能省掉大量为什么我导出的 CSV 标题乱码的问题。5. 周边延伸JSON 转下载文件的小工具和后续沉淀方向把 xlsx 的基本用法整理完之后我顺手把几个高频场景封装成了可以直接复用的函数。这里分享一个最常用的前台页面里一个函数就能把后端返回的 JSON 直接变成可下载的 Excel。5.1 一个现成的函数接口 JSON 直接变下载 Excelfunction exportJsonToExcel(data, fileName export.xlsx, sheetName Sheet1) { if (!Array.isArray(data) || data.length 0) { console.warn(没有可导出的数据); return; } const worksheet XLSX.utils.json_to_sheet(data); const workbook XLSX.utils.book_new(); XLSX.utils.book_append_sheet(workbook, worksheet, sheetName); const out XLSX.write(workbook, { bookType: xlsx, type: array }); const blob new Blob([out], { type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet }); const link document.createElement(a); link.href URL.createObjectURL(blob); link.download fileName; link.click(); URL.revokeObjectURL(link.href); }这个函数里我做了一个空数据保护因为空数组生成 Excel 时虽然不会报错但用户下载下来打开是一个空文件显得很怪。更好的交互是提前 toast 提示当前没有符合条件的记录而不是默默生成一个空文件。5.2 CSV、HTML table 与 xlsx 互相转换的常见姿势xlsx 的价值不止在 Excel 文件本身它还内置了一组转换器能处理 CSV 和 HTML 表格。前端经常会遇到页面上已经渲染好了 table用户想要一键下载的需求。以前我可能会直接抓innerHTML然后写一坨 DOM 解析代码用了 xlsx 之后一行搞定const tableDom document.getElementById(report-table); const worksheet XLSX.utils.table_to_sheet(tableDom); const workbook XLSX.utils.book_new(); XLSX.utils.book_append_sheet(workbook, worksheet, 页面表格); XLSX.writeFile(workbook, report.xlsx);CSV 转 JSON 同样方便const csvText name,age\n张三,30\n李四,25; const workbook XLSX.read(csvText, { type: string }); const rows XLSX.utils.sheet_to_json(workbook.Sheets[workbook.SheetNames[0]]);其实这些能力在官方文档里都叫 utility functions只是平时大家只盯着json_to_sheet和sheet_to_json两个函数忽略了其他同样好用的入口。5.3 这份笔记后续我打算再补什么库的操作这系列笔记我已经列了几个后续方向一个是 Node 端配合接口做批量导入特别是大文件的流式读取再一个是 xlsx 结合 ExcelJS 做数据校验 样式模板的混合方案比如先读模板拿样式再填数据生成正式报表还有一个是用 Web Worker 处理大文件导出避免页面卡死。我在实际项目里的体会是Excel 相关需求永远不会只有一种形态工具库本身不复杂复杂的是业务数据在边界条件下出问题的那些细节。把这些细节一点点补进笔记里比反复查文档更能提升效率。后面等我继续踩出新的坑再回来更新这篇沉淀中的记录。
返回列表