js-xlsx实战指南:从读取解析到批量导出的完整实现
简介一份面向前端开发及相关后台管理项目开发者的JS-XLSX库实战示例重点演示如何将HTML表格数据一键导出为Excel文件。资源包采用zip压缩共6个文件包含1个可直接在浏览器打开运行的HTML演示页面和5个JavaScript脚本覆盖核心解析库、表格样式扩展、二进制数据处理与压缩等方面整体体积约903KB便于本地调试与二次修改。示例以真实常见的数据表格导出场景为切入点完整展示从定位DOM表格、调用表转工作簿方法到生成Base64下载链接并触发下载的完整链路并封装了可直接复用的工具函数。在此基础上还延伸介绍了单元格样式设置、复杂数据结构处理、异步导出及服务器端导出等多种优化思路帮助使用者根据项目需要定制符合业务的功能。目前已有1545人参与学习下载无论入门Excel前端处理还是快速集成导入导出能力这份资源都具备较好的参考价值。1. js-xlsx 不是只能读 Excel一个 demo 背后能省下大半天的活如果你搜「js-xlsx」大概率是想在网页或 Node 服务端里跟 Excel 文件打交道——前端导出报表、后端解析用户上传的表格、批处理一大摞 xlsx。这个库最容易被低估的地方在于它不是一个只能「读」的解析器读写转换三件事都能干而且不依赖浏览器环境就能跑。很多从业者以为要处理 Excel 就得引入重量级方案其实一个 js-xlsx 的 demo 就能覆盖绝大多数常见需求生成报表、导出下载、解析上传、批量清洗数据。它适合三类人不想为一个小需求引入整套报表框架的前端、需要定时处理 Excel 的 Node 脚本开发者以及被 Excel 文件格式折腾过、想快速拿到可控结果的工具党。2. 快速吃透官方 APIread / write / utils 三件套与最小可用 demo2.1 先搞清楚三个核心对象workbook、worksheet、celljs-xlsx 的数据模型是三层结构。最外层是 workbook工作簿对应一个 .xlsx 文件里面是 worksheet工作表对应 Excel 底部的一个个 sheet 标签最底层是 cell单元格用类似A1这样的地址定位。任何操作——不管是读还是写——最终都逃不开这三层。const XLSX require(xlsx); // 创建一个空工作簿 const wb XLSX.utils.book_new(); // 创建一个工作表二维数组 - sheet const ws XLSX.utils.aoa_to_sheet([ [姓名, 部门, 薪资], [张三, 研发, 15000], [李四, 测试, 12000] ]); // 把 sheet 挂到工作簿上 XLSX.utils.book_append_sheet(wb, ws, 薪资表); // 触发下载或落盘 XLSX.writeFile(wb, salary.xlsx);book_new()是初始化工作簿的唯一入口没有它后面什么都挂不上。aoa_to_sheet()接收二维数组第一行当表头后面的行当数据返回一个 worksheet 对象。book_append_sheet()决定这个 sheet 在 Excel 底部显示的名字这里命名为「薪资表」。writeFile()是浏览器和 Node 通用的落盘方法Node 里直接写文件浏览器里触发下载。参数上要注意aoa_to_sheet支持第三个可选参数origin默认从A1开始写入如果你想把数据写到指定起点比如从B2开始填可以传{ origin: B2 }这在做模板填充时很实用。2.2 解析一份现有 Excel从文件到 JSON 的完整链路读取是另一个高频场景。用户上传一个 xlsx后端拿到后要转成 JSON 落库或做校验。链路是拿到文件 →read()解析成 workbook → 取 sheet →sheet_to_json()转成对象数组。const XLSX require(xlsx); // 从文件路径读取Node 环境 const wb XLSX.readFile(upload.xlsx); // 取第一个 sheet 的数据 const firstSheetName wb.SheetNames[0]; const ws wb.Sheets[firstSheetName]; // 转成 JSON默认把第一行当作字段名 const jsonData XLSX.utils.sheet_to_json(ws); console.log(jsonData); // [{ 姓名: 张三, 部门: 研发, 薪资: 15000 }, ...]readFile()是同步读取小型文件没问题超 10MB 建议放到子进程或 worker 里否则主进程会卡住。SheetNames是一个数组保存所有 sheet 的名字按文件中出现的顺序排列。sheet_to_json()是整个库最常用的转换函数默认行为是把第一行当字段名每一行数据变成一个对象对象的 key 就是表头文字。这里有一个很关键的参数header: 1。加上它之后返回的不再是对象数组而是二维数组每一行都是纯值列表。当你不在乎表头叫什么、只关心单元格内容时比如做数据清洗用header: 1更直接。还有个defval参数也值得记空单元格默认会被跳过导致每行对象缺 key传defval: 可以让空值以空字符串出现JSON 结构更整齐。2.3 浏览器与 Node 的差异type 参数决定数据怎么喂进去read()和readFile()的区别在于数据来源。readFile()接收路径read()接收内存中的二进制数据。在浏览器里文件来自input typefile拿到的是 File 对象需要先转成 ArrayBuffer 再喂给read()同时必须指定type告诉库这是什么格式。// 浏览器端解析用户上传的 Excel const fileInput document.getElementById(excelFile); fileInput.addEventListener(change, (e) { const file e.target.files[0]; const reader new FileReader(); reader.onload (ev) { const data new Uint8Array(ev.target.result); const wb XLSX.read(data, { type: array }); const ws wb.Sheets[wb.SheetNames[0]]; const rows XLSX.utils.sheet_to_json(ws, { header: 1 }); console.log(rows); }; reader.readAsArrayBuffer(file); });type: array表示输入是字节数组这是浏览器端最稳妥的喂法。read()还支持buffer、string、base64各自的适用场景不同——base64适合从接口拿到的 base64 字符串string适合已经读成文本的 CSV。选错 type 是最常见的报错来源Data 解析出来全是乱码或直接抛异常优先检查这里。2.4 导出文件名的细节中文名与扩展名都要处理writeFile(wb, 薪资表.xlsx)在 Node 和现代浏览器里都能正常工作但中文文件名在部分浏览器尤其是老 Edge 和部分移动端 WebView下会变成乱码。常见做法是手动触发下载并把文件名 encode 一下// 浏览器端手动触发带中文文件名的下载 const XLSX require(xlsx); function exportExcel(wb, fileName) { const wbout XLSX.write(wb, { bookType: xlsx, type: array }); const blob new Blob([wbout], { 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); }XLSX.write()返回的是二进制内容不触达文件系统bookType控制输出的文件格式xlsx、csv、ods 都支持type: array返回字节数组方便塞进 Blob。URL.revokeObjectURL()记得调用否则大文件导出时浏览器的内存会持续被占用导出几次之后标签页就可能卡死。3. 把 demo 拆成生产级模块合并单元格、日期处理与文件解析流程3.1 表头、样式与合并单元格哪些能做哪些做不了js-xlsx 社区版对样式的支持几乎为零——字体颜色、背景色、边框、行高列宽统统不认。这是一个让很多人踩坑的点用这个库写好数据打开 Excel 发现是素颜的表头不居中、关键行没标红。社区版确实做不了这些解决路径有两个要么导出后在 Excel 里手动调格式要么换支持样式的 fork 版本比如xlsx-js-styleAPI 和原版一致额外支持设置单元格样式。合并单元格是另一个高频需求好消息是这个能做通过 worksheet 的!merges属性控制const XLSX require(xlsx); const ws XLSX.utils.aoa_to_sheet([ [2024 年第一季度各部门薪资汇总], [部门, 一月, 二月, 三月], [研发, 5000, 5200, 5300], [测试, 4000, 4100, 4150] ]); // 合并第一行 A1:D1让大标题跨四列居中 ws[!merges] [ { s: { r: 0, c: 0 }, e: { r: 0, c: 3 } } ]; const wb XLSX.utils.book_new(); XLSX.utils.book_append_sheet(wb, ws, 季度汇总); XLSX.writeFile(wb, quarter.xlsx);!merges是一个数组每个元素用sstart和eend描述合并范围r是行号从 0 开始c是列号从 0 开始。{ s: { r: 0, c: 0 }, e: { r: 0, c: 3 } }表示从第 0 行第 0 列合并到第 0 行第 3 列也就是 A1:D1。注意 r/c 和 Excel 的 A1 表示法不是一回事A1 的行是 1 起始r 是 0 起始这个偏移在手工构造 merges 时非常容易弄反写完之后用 Excel 打开检查一下最稳妥。3.2 日期和时间读出来是数字不是日期这是 js-xlsx 另外一个容易让人摸不着头脑的地方。Excel 内部把日期存成数字——1900 年 1 月 1 日对应 1之后的每一天加 1。所以当你用sheet_to_json()读一个日期单元格时拿到的可能是一个类似 45000 的数字而不是2023-03-15。这个问题有两个解法const XLSX require(xlsx); // 方案一解析时直接让日期保持为 Date 对象 const wb XLSX.readFile(with-dates.xlsx, { cellDates: true }); const ws wb.Sheets[wb.SheetNames[0]]; const rows XLSX.utils.sheet_to_json(ws, { raw: false }); // 方案二不解析手动把 Excel 序列号换算成日期 function excelSerialToDate(serial) { const utcDays Math.floor(serial - 25569); return new Date(utcDays * 86400 * 1000); } console.log(excelSerialToDate(45000));cellDates: true读出来的日期直接是 JavaScript 的 Date 对象省心但要注意时区——Excel 默认不带时区js 的 Date 会按本地时区解释在 UTC8 地区可能出现日期偏移一天的情况。raw: false是让sheet_to_json使用单元格的格式化文本而不是原始值代价是拿到的不是 Date 对象而是字符串格式由 Excel 文件里设置的显示格式决定。生产环境我的习惯是数据量小用cellDates: true拿到 Date 对象后自己toISOString()统一格式化数据量大且只要日期部分直接手动算序列号速度快且不依赖单元格的显示格式。3.3 封装一个通用的 Excel 工具类把 read、write、json 互转这些操作包成一个工具类业务代码里就不用在各个文件里重复写配置了。一个合理的工具类长这样const XLSX require(xlsx); class ExcelUtil { // 对象数组 - Excel 文件 static exportJSON(data, sheetName Sheet1, fileName export.xlsx) { const ws XLSX.utils.json_to_sheet(data); const wb XLSX.utils.book_new(); XLSX.utils.book_append_sheet(wb, ws, sheetName); XLSX.writeFile(wb, fileName); } // 二维数组 - Excel 文件数据不带表头 static exportAOA(data, sheetName Sheet1, fileName export.xlsx) { const ws XLSX.utils.aoa_to_sheet(data); const wb XLSX.utils.book_new(); XLSX.utils.book_append_sheet(wb, ws, sheetName); XLSX.writeFile(wb, fileName); } // 读取 Excel 文件 - 对象数组 static importJSON(path, sheetIndex 0) { const wb XLSX.readFile(path, { cellDates: true }); const ws wb.Sheets[wb.SheetNames[sheetIndex]]; return XLSX.utils.sheet_to_json(ws, { defval: }); } // 读取 Excel 文件 - 二维数组 static importAOA(path, sheetIndex 0) { const wb XLSX.readFile(path, { cellDates: true }); const ws wb.Sheets[wb.SheetNames[sheetIndex]]; return XLSX.utils.sheet_to_json(ws, { header: 1, defval: }); } } module.exports ExcelUtil;json_to_sheet是aoa_to_sheet的对象数组版本key 自动变成表头value 变成单元格内容。exportJSON和exportAOA的唯一区别就是传入的数据结构不同。sheetIndex参数控制读哪个 sheet默认取第一个。defval: 保证每一行的字段数量一致。这个工具类覆盖了 80% 的业务需求真遇到合并单元格或多 sheet 导出直接在具体业务代码里基于这些方法扩展不需要改工具类本身。4. 真实业务场景落地Node 端批量生成报表与浏览器端导入回显4.1 Node 端批量生成多 sheet 月度报表服务端定时任务的典型需求每个月把数据库里各部门数据抽出来生成一个多 sheet 的 Excel 报表——总览一个 sheet每个部门一个 sheet。用 js-xlsx 做这个事的代码并不复杂const XLSX require(xlsx); const db require(./db); // 假设这里能拿到数据库数据 async function generateMonthlyReport(month) { const wb XLSX.utils.book_new(); // 1. 总览 sheet const overview await db.query( SELECT dept, SUM(salary) AS total FROM employees WHERE month ? GROUP BY dept, [month] ); const overviewRows overview.map(row [ row.dept, row.total, month ]); overviewRows.unshift([部门, 薪资合计, 月份]); const overviewWs XLSX.utils.aoa_to_sheet(overviewRows); XLSX.utils.book_append_sheet(wb, overviewWs, 总览); // 2. 每个部门一个独立 sheet const depts await db.query(SELECT DISTINCT dept FROM employees); for (const dept of depts) { const emps await db.query( SELECT name, salary, position FROM employees WHERE dept ? AND month ?, [dept.dept, month] ); const empRows emps.map(emp [emp.name, emp.salary, emp.position]); empRows.unshift([姓名, 薪资, 岗位]); const deptWs XLSX.utils.aoa_to_sheet(empRows); XLSX.utils.book_append_sheet(wb, deptWs, dept.dept); } const fileName report-${month}.xlsx; XLSX.writeFile(wb, fileName); return fileName; }这个场景要注意两个点第一sheet 名不能超过 31 个字符不能包含: \ / ? * [ ]这些字符部门名称如果包含这些符号book_append_sheet会直接抛异常第二每个 sheet 的结构一致——先unshift一行表头再填入数据保证打开文件的人第一行就能看出字段含义。循环查库的方式在部门数量多的时候会产生 N1 查询数据量大时改成一次查出所有数据再在内存里分组会更高效。4.2 浏览器端Excel 导入后回显成页面表格导入回显是后台管理系统最常见的功能——用户上传 Excel页面立刻展示预览确认无误后再提交到后端。整个流程的关键在于把文件读成 ArrayBuffer 后交给 js-xlsx然后渲染成 HTML 表格// 浏览器端读取 Excel 并渲染预览表格 const XLSX require(xlsx); function handleFile(file) { const reader new FileReader(); reader.onload (e) { const data new Uint8Array(e.target.result); const wb XLSX.read(data, { type: array }); const ws wb.Sheets[wb.SheetNames[0]]; // 转成 HTML 表格 const html XLSX.utils.sheet_to_html(ws); document.getElementById(preview).innerHTML html; // 同时转成 JSON 存起来等用户点确认再提交 const json XLSX.utils.sheet_to_json(ws, { defval: }); window.__pendingData json; }; reader.readAsArrayBuffer(file); } // 用户点击确认后提交 function submitData() { const data window.__pendingData; fetch(/api/import, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify(data) }); }sheet_to_html是 js-xlsx 自带的方法直接返回一个完整的table字符串开发预览页时几乎零成本。它的格式比较朴素没有额外的 class 或样式但胜在真实——Excel 里的合并单元格、空行都会被保留。用户看到的预览和实际数据是一致的不会出现「预览好好的导进去就乱了」的尴尬。window.__pendingData存的是解析好的 JSON用户点确认再提交避免用户误传。4.3 后端接收上传multer 与 xlsx 的配合如果走的是传统表单上传而不是前端解析那后端用 multer 接收文件再用 xlsx 解析。注意 multer 默认只处理 multipart/form-data上传的 Excel 文件要经过内存存储或磁盘存储const multer require(multer); const XLSX require(xlsx); const upload multer({ storage: multer.memoryStorage() }); app.post(/api/upload, upload.single(file), (req, res) { if (!req.file) { return res.status(400).json({ error: 没有收到文件 }); } try { const wb XLSX.read(req.file.buffer, { type: buffer }); const ws wb.Sheets[wb.SheetNames[0]]; const data XLSX.utils.sheet_to_json(ws, { defval: }); res.json({ rows: data.length, data }); } catch (err) { res.status(500).json({ error: 文件解析失败请检查格式 }); } });multer.memoryStorage()让文件直接保存在内存里不需要经过磁盘 IO解析完就释放。type: buffer对应 req.file.buffer这两个是配套的。一个大坑如果用户上传的不是真正的 xlsx 而是把 csv 改了扩展名XLSX.read依然能解析出来——js-xlsx 支持自动识别格式不严格依赖扩展名——但解析出来的数据类型可能有差异csv 里全是字符串xlsx 里数值还是数值。在后端做类型校验时别假设扩展名和内容一致。4.4 大文件处理write 的 type 参数与内存控制当数据量超过几万行时writeFile直接落盘没问题但如果同时要把文件内容返回给前端或者上传到云存储就需要用XLSX.write()拿到内存里的二进制。type: buffer返回 Node Buffertype: base64返回 base64 字符串type: array返回字节数组。三者占用的内存依次递增对大文件优先用 buffer。// 大文件导出直接用 buffer 类型返回 async function exportLargeFile(req, res) { const rows []; for (let i 0; i 100000; i) { rows.push([i, name-${i}, Math.random() * 10000]); } const ws XLSX.utils.aoa_to_sheet(rows); const wb XLSX.utils.book_new(); XLSX.utils.book_append_sheet(wb, ws, big); const buffer XLSX.write(wb, { type: buffer, bookType: xlsx }); res.setHeader(Content-Type, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet); res.setHeader(Content-Disposition, attachment; filenamelarge.xlsx); res.send(buffer); }5. 避坑指南五个最容易让 Excel 处理翻车的问题5.1 现象读出来数值变字符串字符串变数值有次解析用户上传的员工信息表发现手机号全部变成了科学计数法打开 Excel 一看是1.38E10这种形式再转 JSON 直接丢精度。原因Excel 内部把超过 11 位的数字按数值处理显示不了就自动转科学计数法js-xlsx 忠实保留了底层值。解决手机号、身份证这类字段要么在源文件里把单元格格式设为文本再重新上传大多数业务方不愿意要么在解析时把这种列手动转字符串可以先用读取到的数值做一次正则校验超过 11 位就.toString()注意要先判断 typeof因为单元格读出来可能是 number 也可能是 string。5.2 现象解析出来的日期比 Excel 里看到的少一天一次做考勤导入用户 Excel 里写的是 2024-01-02读出来变成 2024-01-01。原因Excel 的日期序列号以 1900-01-01 为基准但这个基准没有考虑时区js-xlsx 用new Date()转换时按本地时区解释UTC8 地区某些边界值会往前偏一天。解决用cellDates: true配合手动setHours(0,0,0,0)或者给时间加 8 小时再取 UTC 日期部分。这个问题的表现跟时区强相关同一份文件在 UTC 时区的服务器上解析结果可能不同。5.3 现象中文文件名下载后乱码在部分浏览器下导出「薪资表.xlsx」下载后的文件名变成一串下划线或乱码。原因浏览器对Content-Disposition里的中文文件名支持不一致尤其老版本浏览器只认 ASCII。解决用encodeURIComponent编码文件名或者用 a 标签的 download 属性直接指定这个属性天然支持 Unicode前面代码里的a.download fileName就是最可靠的做法。5.4 现象sheet 名设置成功却导出失败业务里有个部门叫「销售/运营」book_append_sheet的时候没报错writeFile时抛异常文件生成不出来。原因微软对 sheet 名的规范是禁止含: \ / ? * [ ]字符js-xlsx 在 append 阶段不会校验到写文件时才炸。解决在拼接 sheet 名之前做一次清洗把这些字符替换成全角或下划线。这不是 js-xlsx 的 bug是 Excel 文件格式本身的约束哪个库都会遇到。5.5 现象npm install xlsx 装了个老版本项目里npm install xlsx装完发现 API 跟官方文档对不上book_new不存在。原因npm 官方仓库里的xlsx包停在 0.18.5 之后不再更新SheetJS 官方把新版本放到了自建 CDN 上。解决指定版本安装或从官方 CDN 获取最新版——常见做法是npm install https://cdn.sheetjs.com/xlsx-0.20.x/xlsx-0.20.x.tgz这种方式安装最新构建。老版本 0.18.5 对大多数功能是够用的但如果你需要新格式支持或者官方修复的 bug就别用 npm 默认源。6. 验证与收尾用一份自检清单证明你的 demo 真的能跑开发完导出和解析功能别急着交付先做一轮「读回验证」。所谓读回验证就是用 js-xlsx 生成文件 → 再用 js-xlsx 把刚生成的文件读进来 → 对比关键单元格的内容是否和写入时一致。这一步能一次揪出日期偏移、类型丢失、合并单元格错位三类问题比让业务方拿真实文件试错要快得多。const XLSX require(xlsx); // 1. 生成一份测试文件 const wb XLSX.utils.book_new(); const ws XLSX.utils.aoa_to_sheet([ [姓名, 日期, 金额], [王五, new Date(2024-03-15), 12345.67] ]); // 给日期列设置数字格式 ws[B2].z yyyy-mm-dd; XLSX.utils.book_append_sheet(wb, ws, 测试); XLSX.writeFile(wb, roundtrip-test.xlsx); // 2. 重新读取并校验 const wb2 XLSX.readFile(roundtrip-test.xlsx, { cellDates: true }); const ws2 wb2.Sheets[wb2.SheetNames[0]]; const row XLSX.utils.sheet_to_json(ws2, { header: 1 }); console.log(row[1][1]); // 应该是 2024-03-15 的 Date 对象 if (row[1][1].toISOString().slice(0, 10) ! 2024-03-15) { throw new Error(日期回读不一致); }这段代码里ws[B2].z yyyy-mm-dd是关键——它给单元格设置了显示格式否则 Excel 打开时日期会显示成一串数字。只做解析不做生成的话可能注意不到这个点但导出报表的场景一定会用到。单元格地址从A1开始第二行第二列就是B2这个映射关系在手工操作单元格时最容易混乱。除日期外还要验证三样东西合并单元格读回来之后合并区域里的值只在左上角出现一次空单元格没有凭空多出 key长数字没有变科学计数法。这三条过了基本可以放心交给业务方。我自己的习惯是每写一个 Excel 相关功能就单独跑一遍这个 round-trip 脚本再提交代码。有一次就是靠它抓到了时区偏移——本地测试是好的放到东八区服务器上日期少了一天排查了半天才发现是new Date()解释序列号时的隐性时区转换。从那以后我每次处理完 Excel 数据都强制走一遍读回验证再复杂的业务逻辑也先用 10 行脚本确认数据无损。这几个辅助函数和测试用例都已经整理进了一份完整的 demo 工具包里面有可运行的代码、注释和测试数据需要的直接下载拿到后先跑通再改业务场景。希望帮到你。本文还有配套的精品资源点击获取