尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
js-xlsx实战:Excel导入导出与日期精度避坑指南
简介在前端处理Excel文件时解析与生成的底层逻辑都围绕工作簿workbook和工作表worksheet展开。SheetJS的js-xlsx库提供了read/write两条核心链路能够将表格数据与JSON互相转换。实际工程中日期序列号如45123、长数字科学计数法、公式单元格结构等问题往往比API本身更棘手。理解这些数据类型的存储原理配合FileReader、json_to_sheet等工具可以有效规避导入导出时的精度丢失与数据错乱。本文从概念到工程实践梳理Excel导入导出的完整流程与高频踩坑点帮助开发者构建可靠的表格数据处理方案。1. js-xlsx 是什么一个 demo 能覆盖的导入导出场景接到「把这张 Excel 里的几百行数据导进系统」的需求时前端最容易想到的就是 js-xlsx。它是 SheetJS 社区版对 XLSX 解析/生成能力的封装浏览器里用 import 或 script 引一下导入、导出、修改单元格都能做。做 demo 最容易走偏的一点是上来就找「读 Excel 的函数」其实 js-xlsx 的核心 API 只有 read 和 write 两条线其余都是围绕 workbook、worksheet 结构转来转去。本文用一个可复现的本地 demo 把这两条线讲透覆盖导入解析、导出文件、日期精度、长数字精度这些最容易翻车的点。适合刚接触 SheetJS 的读者也适合已经写了 demo、但一上线就踩数据格式坑的开发者。2. 从 Excel 导入数据FileReader 读取、表头映射和多 Sheet 处理导入的完整链路是拿到 File 对象 → FileReader 读成 ArrayBuffer → XLSX.read 解析出 workbook → 取出 worksheet → sheet_to_json 转成前端好用的 JSON。demo 里最容易忽略的是type参数不传的话 read 会按默认字符串处理二进制内容直接乱掉。2.1 用 FileReader 异步读文件demo 里的标准姿势用 input[typefile] 选文件这是浏览器端最可靠的方式。不要用 ajax 去拉本地路径浏览器出于限制拿不到本地磁盘路径。直接看代码input typefile idexcelInput accept.xlsx,.xls / script srchttps://cdn.sheetjs.com/xlsx-0.20.3/package/dist/xlsx.full.min.js/script script document.getElementById(excelInput).addEventListener(change, async (e) { const file e.target.files[0]; if (!file) return; const buffer await file.arrayBuffer(); const workbook XLSX.read(buffer, { type: array }); const firstSheet workbook.Sheets[workbook.SheetNames[0]]; const rows XLSX.utils.sheet_to_json(firstSheet, { defval: }); console.log(rows); }); /script这里用file.arrayBuffer()替代了老版 demo 里的 FileReader.onload 写法代码更短且不会丢this上下文。type: array告诉解析器输入的是 ArrayBuffer这是浏览器端最常见的输入类型如果是 Node 端读文件应改用type: buffer。defval: 让空白单元格返回空字符串而不是 undefined后续做非空校验和渲染表格时少写很多判空逻辑。2.2 sheet_to_json 的表头映射默认模式与二维数组模式sheet_to_json 第一参数是 worksheet第二个参数支持不少选项。最常用的是默认模式把第一行当成表头后面的行按「表头字段 → 单元格值」映射成对象数组。这有个隐含约定表头必须唯一。如果表格里有两列都叫「金额」后面的会覆盖前面的数据直接丢列。下表是几个高频选项选项作用常见误用header: 1返回二维数组不解析表头以为返回的是对象数组下标取错defval空单元格的默认值不设置时返回 undefinedraw: false尝试把富文本转成可读字符串影响日期/数字的格式化行为range只读取指定范围范围写错会漏行多列// demo读取所有 sheet第二个 sheet 拿原始二维数组 const allData {}; workbook.SheetNames.forEach(name { const ws workbook.Sheets[name]; // 默认模式第一行表头映射为对象 allData[name] XLSX.utils.sheet_to_json(ws, { defval: }); }); // demoheader:1 拿二维数组适合没有表头或需要手拼数据的场景 const matrix XLSX.utils.sheet_to_json(ws, { header: 1, defval: });header: 1模式返回的是[[第一行], [第二行], ...]第一行是否表头由你自己决定适合表头不在 A1、标题占了前两行这类脏表格。默认模式下如果第一行是「序号、名称、数量」这样的表头字段名会直接是中文想换英文键名要么改 Excel 表头要么拿到对象后自己map一遍做重命名。demo 阶段我一般不做复杂映射先把数据原样打印出来核对列数再决定重命名的逻辑。2.3 多 Sheet 导入与列顺序校验真实业务里的 Excel 很少只有一个 sheet。常见的有「汇总表」「明细表」「说明」三个 sheet或者一个模板文件里按月份分 sheet。上面代码里用workbook.SheetNames遍历就是标准的全量读取姿势。还有两点容易被 demo 漏掉第一sheet 顺序不可靠。用户重命名、拖动 sheet 标签后SheetNames数组的顺序会变化。不要用「第几个 sheet 一定是汇总表」这种假设而是按 sheet 名匹配。找不到目标 sheet 时直接给用户提示。function pickSheet(workbook, wantedName) { const idx workbook.SheetNames.indexOf(wantedName); if (idx 0) { throw new Error(未找到 Sheet: ${wantedName}实际包含: ${workbook.SheetNames.join(, )}); } return workbook.Sheets[workbook.SheetNames[idx]]; }第二列顺序和 demo 里不一致。用户可能把「备注」插到「数量」前面也可能删掉某一列。导入前先取出表头行做一次校验比导入完再发现数据对不上要省事得多。校验逻辑见后面「常见问题与避坑」章节。3. 从数据导出 Exceljson_to_sheet、book_new 与 writeFile 的最小闭环导出方向正好反过来前端 JSON 数据 →XLSX.utils.json_to_sheet生成 worksheet →XLSX.utils.book_new建 workbook →XLSX.writeFile触发浏览器下载。这个闭环 5 行代码能跑通但实际项目里总会追加「要合并单元格」「要设置列宽」「要导出多个 sheet」的需求。3.1 json_to_sheet 一键导出最小可运行代码function exportDemo() { const rows [ { 姓名: 张三, 部门: 研发部, 工龄: 3 }, { 姓名: 李四, 部门: 市场部, 工龄: 5 }, ]; const ws XLSX.utils.json_to_sheet(rows); const wb XLSX.utils.book_new(); XLSX.utils.book_append_sheet(wb, ws, 人员名单); XLSX.writeFile(wb, 人员名单.xlsx); }json_to_sheet会按对象的 key 顺序生成列说白了就是Object.keys的顺序。中文 key 没问题但如果你要求列顺序固定例如「姓名、部门、工龄」而不是「部门、姓名、工龄」建议先对数据做一层字段重排。常见做法是传入一个有序数组再配合aoa_to_sheetconst header [姓名, 部门, 工龄]; const body rows.map(r [r.姓名, r.部门, r.工龄]); const ws XLSX.utils.aoa_to_sheet([header, ...body]);aoa_to_sheet接受二维数组第一行当表头完全由你控制顺序。项目里导出带格式的表格我基本都会切到这种写法因为json_to_sheet的字段顺序不够直观。3.2 列宽、合并单元格与多 Sheet导出 demo 的进阶配置用户拿到导出的文件后第一反馈永远是「列宽太窄文字挤在一起」「标题跑到第二页」「怎么只有第一个 sheet」。这些都能在 workbook 生成阶段做掉。设置列宽需要在 worksheet 的!cols属性上做文章ws[!cols] [ { wch: 10 }, // 姓名列宽 10 个字符 { wch: 12 }, // 部门列宽 12 { wch: 8 }, ];wch是「字符宽度」的单位中文按全角字符算英文按半角。不是像素也不是磅值调的时候按「最多能容纳几个字」估。合并单元格用!mergesws[!merges] [ { s: { r: 0, c: 0 }, e: { r: 0, c: 2 } }, // 第 1 行第 1 列到第 1 行第 3 列合并 ];s是起始单元格e是结束单元格r是行号0 开始c是列号0 开始。这里特别容易踩坑行列都是 0 索引而 Excel 界面里显示的行号从 1 开始、列号是字母。写错一格合并区域就偏移了。写 demo 时可以用一个辅助函数function merge(ws, r1, c1, r2, c2) { if (!ws[!merges]) ws[!merges] []; ws[!merges].push({ s: { r: r1, c: c1 }, e: { r: r2, c: c2 } }); }多 sheet 导出的逻辑和导入对称book_new创建空 workbook每调用一次book_append_sheet就往后追加一个 sheet。注意第二个参数 sheet 名不能重复也不能包含: \ / ? * [ ]这些非法字符。demo 里建议对 sheet 名做一次清理const safeName rawName.replace(/[:\\/?*\[\]]/g, _).slice(0, 31);Excel 的 sheet 名上限是 31 个字符超长会写入失败或弹修复提示这是我自己实际遇到过的问题。4. 常见问题与避坑日期精度、科学计数法和单元格玄学demo 跑通不意味着能用真正的问题都在「看着成功、打开一看不对」的场景里。下面 4 条是 js-xlsx 使用中最常见的血泪经验每一条都按「现象 → 原因 → 解决」梳理。4.1 日期列变成了 45123现象Excel 里明明是「2023-08-15」导出的 JSON 里变成了 45123 或 45123.0。原因Excel 的日期本质是序列号从 1900-01-01 开始按天计数js-xlsx 本身不会主动把它转成可读日期只有当你读取时声明需要日期解析才行。解决读入时加cellDates: trueconst workbook XLSX.read(buffer, { type: array, cellDates: true });加上之后日期类型单元格会直接解析成 JS 的Date对象。但如果你的表格里既有真日期又有「2023/8/15」这种文本型日期文本不会被转换输出就混着 Date 对象和字符串处理逻辑要分开写。稳妥做法是拿到行数据后统一格式化const rows XLSX.utils.sheet_to_json(ws, { cellDates: true, defval: }); rows.forEach(row { if (row[入职日期] instanceof Date) { row[入职日期] formatDate(row[入职日期]); } });formatDate自己写getFullYear()/getMonth()1/getDate()拼字符串不要直接toLocaleDateString()不同浏览器的本地化输出格式不同线上会翻车。另外导出方向也有坑如果你用JSON.stringify把 Date 对象转成 ISO 字符串再json_to_sheetExcel 打开会显示英文日期格式需要在生成的单元格里指定t: d或把日期先转成new Date()再交给json_to_sheet——直接传字符串是不会被识别成日期的。4.2 19 位订单号变成科学计数法现象Excel 里的订单号是 6222020200012345678 这种纯数字导入后变成 6.222020200012345e18精度全丢。原因js-xlsx 按数字解析单元格JS 的 Number 对超过 2^53 的整数无法精确表示。解决思路有两个方向导入时让 Excel 把文本当文本读。造成「改类型」这个事常见做法是在源数据侧把长数字写成字符串。比如导出时就把订单号拼成带前缀的文本或者在aoa_to_sheet之前把每一项都String()一下const body rows.map(r [String(r.orderId), r.name]);更细一点js-xlsx 判断一个单元格是不是文本看的是数据的类型和单元格的t字段。json_to_sheet默认把字符串写为t: s所以导出的文件里订单号是文本型Excel 打开不会变科学计数法。但如果你从 Excel 读入的是「数字型」长尾再导出时它仍然是数字型精度已经丢了不可逆。所以长数字的底线是源头能做成文本就做文本导入后立即转成字符串缓存不要中途做加减乘除。真实项目里遇到过一个场景订单号导出到 CSV 直接被 Excel 吞掉尾数后来改成 xlsx 导出加文本格式才解决。4.3 公式单元格只拿到计算值现象Excel 里某个单元格是SUM(B2:B10)导入后sheet_to_json直接给了求和结果不是公式字符串。原因默认情况下 js-xlsx 会把公式解析为值类型。这其实符合大部分人的预期——你要的是「结果」不是公式。但有些场景比如审计、模板回填确实需要原样保留公式。解法是读取时加cellFormula: trueconst workbook XLSX.read(buffer, { type: array, cellFormula: true });此时单元格对象多一个f字段即公式文本。sheet_to_json默认输出仍走值你要拿到公式得遍历worksheet的单元格对象for (const key in ws) { if (key.startsWith(!)) continue; // 跳过 !ref !cols !merges 这些内部属性 const cell ws[key]; if (cell cell.f) { console.log(key, cell.f); } }注意cell.f是未计算的公式字符串cell.v才是上次计算的结果。两者同时存在别混用。还有一点js-xlsx 不会重新计算公式如果你用代码修改了某个参与计算的单元格旁边用 SUM 合计的结果不会自动更新。demo 阶段发现「我改了 A1 但总价没变」先别怀疑库有问题这本来就是预期行为要重新计算得引入公式引擎或从 xlsx 重新解析。4.4 空行、合并单元格与 sheet_to_json 的静默丢数据现象导入的行数比 Excel 里看到的数据行数少尤其表格中间有空行时后面的行突然断了。原因sheet_to_json以「第一行为表头」的规则遍历遇到连续空行会提前结束而且它不做「空行跳过继续」的处理。解决先转成header: 1二维数组手动过滤空行和全空列再转换function sheetToRows(ws) { const matrix XLSX.utils.sheet_to_json(ws, { header: 1, defval: }); return matrix.filter(row row.some(cell String(cell).trim() ! )); }row.some(...)表示「这一行至少有一个非空单元格」才保留。过滤之后再决定表头行是哪一行、从哪一行开始读数据。合并单元格也在这类问题中出现被合并的空白单元格在header: 1模式下是空字符串只有合并区域的左上角有值。如果业务上必须要读取合并后的所有行都带同一个值需要在读取后做一次前向填充遍历时记住「上一个非空单元格的值」遇到空值就填上一个值。这个逻辑不要写进循环里的if判断单独抽一个函数否则多 sheet 时到处复制粘贴线上出问题不好排查。5. 本地 demo 上线前数据校验、参数护栏与性能观测demo 能跑只证明链路通了上线意味着要面对真实数据几万行的大表、脏数据、命名不规范的 sheet。这里聊三个我常用的加固手段。5.1 性能与规模护栏导入大文件时read是一次性把整个文件解析进内存sheet_to_json又是全量转换。十万行级别的 xlsx解析耗时基本在秒级到十几秒取决于单元格数量和公式复杂度。常见处理是分页导入先读一个 sheet切片展示前 200 行确认数据正确后再全量入库。切片用数组方法就行不用重新读文件const page rows.slice(start, start 200);内存方面type: array读入的 ArrayBuffer 本来就是文件大小解析后 workbook 对象会比原文件大好几倍因为每个单元格的键值对展开成对象了。demo 里无所谓线上要留意。如果只是预览数据不想全保留用完 workbook 后手动置空引用别挂在全局变量上。5.2 read 参数调优与字段类型护栏read 的可配参数不止type和cellDates还有raw和dense。raw: true会保留原始值类型文本仍是文本、数字仍是数字raw: false会把数字按 Excel 显示格式格式化一次。看起来 raw:false 更友好但它会把「金额」这类数字变成带千分位分隔的字符串前端再排序就出问题了。我的习惯是读取时用raw: true格式化留在 UI 层做。dense: true是另一种存储形式用数组代替对象存单元格大文件解析更快、内存更低代价是单元格取数方式不同。demo 阶段不用开文件超过 5MB 时可以试。上线前的字段类型护栏应该写成独立函数function validateRows(rows, requiredFields) { const errors []; rows.forEach((row, i) { requiredFields.forEach(field { if (row[field] || row[field] null) { errors.push(第 ${i 2} 行缺少 ${field}); } }); }); return errors; }行号写i 2是因为第一行是表头人眼看到的行号从 2 开始。这个很容易错我第一版写i 1用户拿着报错去对 Excel 老是对不上。数字字段还可以加typeof row[field] ! number的检查防止文本型数字混进来。最后提一个我自己的习惯本地 demo 里始终放一份「带故意脏数据」的测试文件里面包含长数字订单号、中文日期、合并单元格、空行、多 sheet每次改完解析逻辑都拿它回归一遍。这个习惯帮我挡掉过至少三次上线事故。希望这篇文章的思路能帮你把一个能跑的 demo 变成能抗真实数据的方案也希望你踩坑时能想起「先看单元格类型再怪库」。本文还有配套的精品资源点击获取
RELATED

相关推荐

SysML/UML建模实战:从需求图到状态机的需求追踪闭环

SysML/UML建模实战:从需求图到状态机的需求追踪闭环

简介:SysML/UML是系统工程领域应用广泛的标准建模语言,这本书围绕OMG相关标准展开,面向系统工程从业者、软件架构师以及希望掌握系统建模方法的工程师。全书以UML为基础,系统讲解SysML的需求管理、架构分析、视图分类和仿真验证等…

📅 2026/10/11 16:11:44
智慧党建系统如何让党务工作更轻松?一文说清楚

智慧党建系统如何让党务工作更轻松?一文说清楚

对于党务工作者来说,按照以往做党务工作的方式,开个会要反复通知确认时间、学习文件要打印一大堆材料、统计党员信息要翻好几本台账,事情不难,但琐碎、耗时、还容易出错。蓝创星智慧党建系统,将繁琐的事情简单化&#…

📅 2026/10/11 16:11:44
D-Link无线路由器桥接设置全攻略:WDS/中继模式与稳定运行

D-Link无线路由器桥接设置全攻略:WDS/中继模式与稳定运行

简介:无线路由器无线桥接(WDS)配置教程,面向需要扩展WiFi覆盖范围、解决信号死角问题的家庭或小型办公用户,以D-Link DIR 600M为例,完整讲解两台无线路由器通过无线方式互联、合并为一个更大无线网络的操作…

📅 2026/10/11 16:11:44
MORE NEWS

更多资讯

📰

IIS短文件名扫描实战:从8.3命名规则到工具包使用与避坑

简介:本资源聚焦 IIS 短文件名泄露这一经典 Web 安全检测场景,面向渗透测试初学者、安全运维人员及 CTF 参赛者,用于校验目标站点是否存在短文件名枚举风险。包内同时提供 Python 与 Java 两套实现,并附带环境包下载地址&#xff…

📰

Redis延时队列+Swoole多进程:PHP订单超时关闭实战

简介:这份资源面向PHP后端开发者与消息队列学习者,提供一套基于Redis延时队列与Swoole多进程模型构建的高并发消费端实现,可用于订单超时关闭、定时任务触发等需要延迟处理的业务场景。压缩包为zip格式,大小约1.66MB,内…

📰

Fiddler抓包实战:从HTTPS解密到弱网模拟,解决联调难题

打开Fiddler的那一瞬间,很多人以为这只是个“看请求”的小工具,但真正用熟之后你会发现,它其实是排查问题时的第一现场。前几天帮一个同事定位接口偶发超时的问题,前端说后端慢,后端说网关在重试,扯了半小时…

📰

Fiddler抓包实战:从代理原理到HTTPS解密与接口调试

提到抓包工具,很多搞开发、做测试的朋友第一个想到的肯定是Fiddler。我在不同项目里用它做接口联调、移动端调试、性能分析,加起来也有好多年了。有人会把名字写成Fidder,其实官方拼法是Fiddler,但大家都知道说的是同一个工具。简…

📰

微博情感分析系统全拆解:从爬虫采集到可视化呈现

简介:基于微博情感分析系统的毕业设计项目,面向计算机相关专业毕业生及有Python基础的实践者,完整呈现从微博数据获取、文本预处理到多种分类器训练与评估的工程链路。压缩包共71个文件,以31个Python脚本为核心,搭配15…

📰

基于Spark的电影推荐系统:ALS算法实战与毕设避坑指南

简介:一份基于Spark的电影推荐系统设计与实现资料包,面向大数据与推荐系统方向的学生、毕业设计者及自学开发者。资源以docx论文为核心,完整呈现从绪论、开发技术到系统设计、实现与测试的规范流程,涵盖课题背景、研究现状、技术选…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬