js-xlsx指南:Excel读取导出、合并单元格与公式写入避坑实战
简介js-xlsx使用demo是一份面向广大前端开发者的Excel处理实战示例聚焦js-xlsx库在浏览器环境下的表格导出应用解决日常项目中HTML表格数据转为XLSX文件的常见需求。压缩包内共包含6个文件由5个JavaScript脚本和1个HTML页面组成JS脚本覆盖官方核心库、导出功能封装及配套工具HTML页面提供可视化演示包体仅903KB下载后即可本地运行调试。示例流程完整而清晰从安装、导入、读取表格到最终生成Excel文件均有代码呈现同时提示了自定义样式、处理合并单元格、异步导出及服务器端导出等扩展方向。资源结构精简核心代码可直接复用便于快速接入实际项目。已有1545人学习下载适合需要快速集成Excel导入导出功能的初中级前端开发者也适合作为技能回顾与项目起步参考。1. js-xlsx 到底怎么用从文件读取到导出的 20 行代码后台系统做久了迟早遇到把 Excel 导进来、导出去的需求。js-xlsx 这个老牌 JavaScript 库四五行代码就能把 xlsx 解析成 JSON反向导出不到二十行。我最早在某个合同导入的模拟项目里用它被二进制格式折腾到想放弃换它之后流程清晰多了。这套使用 demo 覆盖完整链路文件读取、工作表定位、数据转 JSON、导出 xlsx、合并单元格、公式写入以及日期乱码、大文件卡死这类高频坑。适合正在做 Excel 导入导出、想摸清 sheet_to_json 边界参数的新手和老手。下文代码都在浏览器里跑过Node 环境去掉文件读取部分同样成立。先读再写最后踩坑。2. 读取 Excel三步把 xlsx 变成 JavaScript 对象读取的链路其实很短拿文件、解析出工作簿对象、从工作表取值。真正让新手翻车的不是链路而是 XLSX.read 的 type 参数和 sheet_to_json 的输出形状。这一章把每一步的参数讲透代码可以直接抄进你的导入页面。2.1 安装引入npm 包与全局脚本两种姿势npm 上这个包的名字叫 xlsx早期仓库叫 js-xlsxAPI 一直沿用至今。很多老项目里看到的是 script 引入 xlsx.full.min.js 的方式加载完成后 window 上挂一个 XLSX 全局对象和 import 进来的是同一个东西。npm install xlsx// 现代打包工具里用 import import * as XLSX from xlsx; // 或者 CommonJS 环境 // const XLSX require(xlsx);我一般建议用 npm 引入版本可控也方便后续升级CDN 方式适合做演示页或者内网离线环境。引入之后第一步永远是确认 XLSX 对象存在打包工具里如果遇到 XLSX is not defined先检查是不是装成了别的同名包——npm 上出现过仿冒 xlsx 的包安装时务必看清维护者信息再装。2.2 文件读取ArrayBuffer 与 type 参数浏览器里读 Excel 的标准姿势是用 file input 拿 File 对象转成 ArrayBuffer 再交给 XLSX.read。下面这段是我每次写导入功能都会先粘过去的骨架input typefile idfileInput accept.xlsx,.xls,.csv /const input document.getElementById(fileInput); input.addEventListener(change, async (e) { const file e.target.files[0]; if (!file) return; const buf await file.arrayBuffer(); // File 转 ArrayBuffer const wb XLSX.read(buf, { type: array, // 输入类型必须是 array cellDates: true, // 日期列解析成 Date 对象 }); const ws wb.Sheets[wb.SheetNames[0]]; // 取第一个工作表 const rows XLSX.utils.sheet_to_json(ws, { header: 1, // 输出二维数组首行是表头 defval: , // 空单元格填空字符串 }); console.log(rows); });关键在 XLSX.read 的 type 参数。file.arrayBuffer() 返回 ArrayBuffer所以 type 写 array如果你从别处拿到的是二进制字符串或 base64type 要对应改成 binary 或 base64写错了解析结果会变成乱码甚至直接抛错。cellDates: true 是我每次必开的开关不开的话日期栏会得到一串数字序列号具体原因在第 5 章细说。解析出来的 wb 对象有两个常用字段SheetNames 是工作表名数组Sheets 是工作表名到工作表对象的映射。工作表对象 ws 里的数据用单元格坐标做 keyws[A1] 就是左上角那个格子而 ws[!ref] 保存了当前数据的有效范围。2.3 定位工作表与 sheet_to_json 输出形状多 sheet 文件里取第一个表往往不是业务想要的行为。更稳的做法是按名字找找不到就明确报错// 按名字找 sheet而不是死取第一个 const target wb.SheetNames.find(name name.includes(订单)); if (!target) throw new Error(没找到订单表); const ws wb.Sheets[target]; // header 不传时第一行作为 key输出对象数组 const json XLSX.utils.sheet_to_json(ws); // 只想读前 100 行、要格式化后的文本值 const limited XLSX.utils.sheet_to_json(ws, { range: A1:J100, raw: false, // 日期会变成 2024-01-01 defval: null, });sheet_to_json 的 header 参数决定输出形状是整个库最容易记混的地方header 取值输出形状适用场景不传对象数组第一行做 key数据规整、表头固定的文件1二维数组含表头行数据清洗、后续做列名映射A对象数组key 是 A/B/C需要按列号定位的场景range 参数支持 A1:J100 字符串写法也支持 { s: { r, c }, e: { r, c } } 对象写法。raw 传 true 拿原始值false 拿格式化文本。还有一个 blankrows 参数默认 false 时会跳过空行如果用户的表格里故意留了间隔行想原样保留就把它设成 true。提示除了 sheet_to_json同一批 utils 里还有 sheet_to_csv、sheet_to_html、sheet_to_formulae。做预览功能时用 sheet_to_html 直接塞进 iframe 就能看到表格省掉手写渲染。3. 导出 Excel从 JSON 到可下载文件的两条组装路径导出比导入简单方向反过来而已。但导出也有自己的坑对象数组和二维数组怎么选、列宽怎么控制、多 sheet 怎么拼、以及部分浏览器里中文文件名的问题。这一章按实际开发顺序把导出链路走一遍。3.1 json_to_sheet 与 aoa_to_sheet两种造表 API 怎么选造工作表有两个入口选错不会报错但会多写不少凑数据的代码。后端接口返回的是对象数组直接用 json_to_sheet前端已经组装好带表头的二维数组用 aoa_to_sheet 更直白// 后端接口来的对象数组 const orderList [ { orderNo: A001, customer: 某公司, amount: 12000 }, { orderNo: A002, customer: 另一家公司, amount: 8000 }, ]; const ws1 XLSX.utils.json_to_sheet(orderList); // 前端已有的二维数组 const aoa [ [订单号, 客户, 金额], [A001, 某公司, 12000], [A002, 另一家公司, 8000], ]; const ws2 XLSX.utils.aoa_to_sheet(aoa);json_to_sheet 把对象的 key 自动做成表头列的顺序按对象属性插入顺序排列。如果列顺序敏感先 map 一份只含目标 key 的新数组再传。这里有个细节对象属性顺序在现代引擎里按插入序但遇到数字开头的 key 会先按数字序排所以表头顺序要求严格时别赌这个行为直接用 aoa_to_sheet 显式给表头最稳。3.2 列宽与行高!cols 和 !rowsExcel 默认列宽对中文很不友好尤其是客户名称、备注这类字段导出后要么被截断要么挤成一团。解决办法是在 sheet 对象上手动挂 !cols 和 !rowsws[!cols] [ { wch: 12 }, // 订单号 { wch: 24 }, // 客户名称中文宽一点 { wch: 10 }, // 金额 ]; ws[!rows] [ { hpt: 24 }, // 表头行高单位是磅 ];wch 是字符宽度hpt 是磅值行高。注意 wch 是按 ASCII 字符算的一个中文字大概占两个 wch所以设置列宽时要往宽里留余量。不设置这两项文件也能打开只是格式化效果全靠用户自己调。3.3 多工作表与下载writeFile 与 Blob 回退业务报表很少有单 sheet 的场景通常一个工作簿里要放汇总、明细、对账单好几张表。组装多个 sheet 再下载的完整链路如下const wb XLSX.utils.book_new(); XLSX.utils.book_append_sheet(wb, XLSX.utils.json_to_sheet(customers), 客户); XLSX.utils.book_append_sheet(wb, XLSX.utils.json_to_sheet(orders), 订单); // 最省事的方式直接触发浏览器下载 XLSX.writeFile(wb, 业务数据.xlsx);book_new 创建空工作簿book_append_sheet 往里塞工作表第三个参数是工作表名。writeFile 会按文件扩展名推断格式.xlsx 输出 xlsx.csv 输出 csv。文件名带中文时多数现代浏览器没问题个别环境会乱码稳妥做法是先把文件名设成字母触发下载后再用 download 属性修正。如果你的运行环境不支持 writeFile或者想完全控制下载触发逻辑就用 write 配合 Blob 自己拼const out XLSX.write(wb, { bookType: xlsx, type: array }); const blob new Blob([out], { type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, }); const url URL.createObjectURL(blob); const a document.createElement(a); a.href url; a.download 业务数据.xlsx; a.click(); URL.revokeObjectURL(url);write 的 bookType 可选 xlsx、xlsb、csv、ods 等type: array 返回 ArrayBuffer正好喂给 Blob。这套代码在桌面浏览器和 WebView 里都跑得通是我做导出功能时的兜底写法。4. 进阶玩法合并单元格、公式与样式黑匣子读和写都通了接下来是导出时容易卡住的三类需求合并单元格、公式、样式。前两个有标准解法第三个要认清能力边界别在社区版上硬磕。4.1 合并单元格手工构造 !merges合并单元格没有专门的 API但有个隐藏字段 !merges往里塞坐标对象就能生效const ws XLSX.utils.aoa_to_sheet([ [季度销售汇总, , ], [月份, 销售额, 利润], [1月, 12000, 3000], [2月, 15000, 4000], ]); ws[!merges] [ { s: { r: 0, c: 0 }, e: { r: 0, c: 2 } }, // 第一行第1~3列合并 { s: { r: 2, c: 0 }, e: { r: 3, c: 0 } }, // 左侧月份两行合并 ];!merges 数组里每个元素是一个合并区间s 是起始单元格e 是结束单元格r 是行号c 是列号都从 0 开始。合并之后除了左上角那个单元格其余被合并的格子别填数据否则写入的文件打开时可能出现内容重叠的提示。合并本身不会保留边框线要边框得靠后面的模板方案。4.2 公式写入f 字段与结果缓存导出带公式的表格最常见的是小计、合计这类列。单元格对象里塞一个 f 字段存公式文本t 字段标类型const ws XLSX.utils.aoa_to_sheet([ [编号, 单价, 数量, 小计], [1, 10, 3, null], [2, 8, 5, null], ]); ws[D2] { t: n, f: B2*C2 }; ws[D3] { t: n, f: B3*C3 };f 是公式文本t 是单元格类型n 表示数字。这里没给 v缓存值Excel 打开时会自动重算对这个场景够用。但有些只读预览器不会重算公式列会显示空白这时候自己把结果算好塞进 v 字段两全其美。// 带缓存值的写法预览器不重算也能看到数字 ws[D2] { t: n, f: B2*C2, v: 30 };注意公式里引用的单元格范围写错导出文件在 Excel 里打开时会提示发现不可读的内容虽然文件不损坏但会很吓人。写完公式后用公式预览或者先导出一份验证一下再交付。4.3 样式黑匣子社区版为什么改不了字体颜色这是 js-xlsx 被吐槽最多的地方。读取时加 cellStyles: true 能拿到一部分样式信息但写入端没有公开的样式 API直接给单元格对象加 alignment、fill 之类的字段写出来的文件里这些设置基本不生效。我在某图像处理 Demo 的导出功能里试过各种姿势结论是别在社区版里折腾样式。业界通用的绕法是模板方案预先做一个带表头配色、边框、冻结窗格的 xlsx 模板读取模板后往指定单元格填数据再导出同一个工作簿模板里的样式会被原样保留const tplBuf await fetch(/templates/export-tpl.xlsx).then(r r.arrayBuffer()); const wb XLSX.read(tplBuf, { type: array, cellStyles: true }); const ws wb.Sheets[wb.SheetNames[0]]; // 按坐标填数据模板样式原地保留 ws[C5] { t: n, v: totalAmount }; ws[C6] { t: n, v: totalCount }; XLSX.writeFile(wb, 月度报表.xlsx);模板方案有几个注意点模板里别放有公式占位但没数据的单元格否则每次导出都会弹重算提示读取模板时开 cellDates: true避免日期被序列化填数据时保持目标单元格类型和模板一致纯文本列别塞数字反之亦然。5. 避坑记录日期、乱码、大文件与版本分裂的四个现场这四类问题我在生产环境里都碰到过每条按现象、原因、解决的顺序写排查时可以照着对号入座。5.1 日期列变成 44562 这样的数字现象导入的 Excel 里明明是日期解析出来却是一串四万多开头的整数。原因Excel 内部日期存的是从 1899-12-30 开始的天数序列号sheet_to_json 默认 raw: true 给的就是原始序列号不做任何转换。解决读取时开 cellDates: true 让日期变成 Date 对象或者提取数据时 raw: false 拿格式化字符串。我在项目里两个开关都开业务逻辑里优先处理 Date 类型展示层用 raw: false 的结果这样既保证计算精度又能直接展示。5.2 导出 CSV 中文乱码现象用 sheet_to_csv 导出的 CSV记事本打开正常Excel 打开中文全乱。原因CSV 文件没有 BOM 头Excel 会用本地编码去猜Windows 环境下经常猜成 ANSIUTF-8 的中文自然全乱。解决写内容前拼一个 \uFEFF这是 UTF-8 BOM 的标准写法const csv \uFEFF XLSX.utils.sheet_to_csv(ws); const blob new Blob([csv], { type: text/csv;charsetutf-8; });注意别用 writeFile 直接导 csvwriteFile 生成的 CSV 默认不带 BOM同样会踩这个坑。要导 CSV 就用 Blob 方案自己拼顺便还能控制文件名和点击逻辑。5.3 大文件解析卡死页面现象一个几十 MB 的多 sheet 文件导入时页面白屏点哪里都没反应过几分钟才开始转。原因XLSX.read 是同步解析直接在主线程里跑文件越大占的时间越长内存峰值也越高。几十 MB 的 xlsx 解析出几万行数据很正常主线程根本扛不住。解决把解析过程丢进 Web Worker主线程只负责收结果和渲染// 主线程 const worker new Worker(./excel-worker.js); worker.postMessage({ file }, [file]); // File 转移给 worker不复制 worker.onmessage (e) renderTable(e.data.rows); // excel-worker.js self.onmessage async (e) { const { file } e.data; const buf await file.arrayBuffer(); const wb XLSX.read(buf, { type: array, cellDates: true }); const rows XLSX.utils.sheet_to_json(wb.Sheets[wb.SheetNames[0]], { header: 1 }); self.postMessage({ rows }); };worker 里要单独引入 xlsx 库可以用 importScripts 加载全局版或者用打包工具把 worker 也打一份。postMessage 的第三个参数传数组是转移所有权省掉一次结构化克隆的开销大文件场景下这点差异很明显。解析过程在后台跑UI 保持流畅用户至少能看到一个 loading 而不是白屏。5.4 版本分裂js-xlsx、xlsx 与同名包的坑现象照着老教程写的代码有的 API 存在有的不存在最离谱的是 npm install 之后 import 进来直接报错。原因这个库的历史有点曲折。早期仓库叫 js-xlsxnpm 包名是 xlsx后来官方更新节奏变化npm 上能装到的版本和老教程的写法对不上npm 上还出现过仿冒的同名包。解决项目里锁版本装包时确认维护者信息。核心的 read、sheet_to_json、writeFile 这套 API 从老版本到现在一直是稳的真正让你翻车的往往不是版本而是 type、raw、cellDates 这几个参数的默认值。遇到行为和教程不一致先检查自己传的参数再怀疑版本。6. 收尾技巧Excel 表头到后端字段的映射表6.1 场景Excel 导入最常见的形态是用户给的表头是中文后端接口要的字段是英文。直接在业务代码里写一堆 if-else 映射加一个字段要改两处代码。我习惯在导入入口维护一张映射表一个 key-value 对象搞定。6.2 代码const FIELD_MAP { 订单编号: orderNo, 客户名称: customerName, 下单时间: orderTime, 金额: amount, }; function mapRows(aoa) { const [header, ...rows] aoa; return rows.map(row { const obj {}; header.forEach((label, i) { const key FIELD_MAP[label.trim()]; if (key) obj[key] row[i]; }); return obj; }); }表头 trim 掉空格是必须的用户手动填的表头经常带全角空格映射匹配会失败。没匹配到的列直接忽略不会让整行报错需要保留原始数据的话可以给 obj 加一个 raw 字段兜底。6.3 验证有空顺便校验必填字段是否为空、金额能不能 parseFloat错误信息带上行号用户才知道改哪一行。从那以后我每次接 Excel 导入需求都强制先找对接方要一份真实模板把表头抄进 FIELD_MAP 再动手宁可多花十分钟也不在联调时翻车。希望帮到你。本文还有配套的精品资源点击获取