ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

luckysheet+luckyExcel 纯前端 Excel 导入导出与公式

luckysheet+luckyExcel 纯前端 Excel 导入导出与公式 上周帮一个做内部报表的团队收拾了一套在线表格的活儿需求说起来就一句话让用户在浏览器里自己挑本地的 Excel 文件页面里打开、看得到样式、公式还能算改完再导回成 xlsx。听着像是要重造一个 Excel真做起来其实没那么玄乎——luckysheet 加 luckyExcel 这两个库凑一起本地导入文件、渲染 excel、公式计算、导出 excel 这条链路基本就通了而且全程不碰服务端文件压根不上传。这套组合最舒服的地方在于白盒。表格数据在内存里就是一份普通的 JSON你想塞什么逻辑进去都行公式算完的结果也能直接读出来做校验。它适合三类人一类是手里有 Excel 报表要搬到后台系统里的前端一类是想给用户一个改完再传回来的轻量编辑入口的后端还有一类是懒得装 Office 就想在网页里翻表看的运维和数据分析同学。下面我把这套东西从选型到落地整理一遍包括几个官方文档里没写、只有自己上手才会撞上的坑。1. 选型前后想清楚的三件事1.1 luckysheet 和 luckyExcel 到底谁管什么刚接触的人最容易把这两个库搞混以为装一个就行。实际上它们是上下游关系职责切得很干净。luckysheet是渲染和交互的主体。它负责把一份符合它自己格式的 JSON 画成表格处理单元格选中、编辑、右键菜单、合并、条件格式、图表这些交互同时也带着一套公式引擎能做 SUM、VLOOKUP、IF、日期函数这类常见运算。注意它不认 xlsx 文件你直接把一个 xlsx 丢给它它是不理的。luckyExcel是格式转换器只干两件事把 xlsx 解析成 luckysheet 能吃的 JSON以及把 luckysheet 的 JSON 反向打包成 xlsx。它内部依赖 ExcelJS 做真正的二进制读写。所以整条链路是这么走的用户选文件 → luckyExcel 解析 → 得到 luckysheet 格式的 JSON → 丢给 luckysheet 渲染 → 用户编辑 → 从 luckysheet 取最新 JSON → luckyExcel 打包 → 浏览器下载。模块主要职责关键 API容易踩的地方luckysheet渲染、交互、公式计算luckysheet.create / getAllSheets / setCellValue必须给容器固定高度否则白屏luckyExcelxlsx 与 JSON 互转transformExcelToLucky / transformLuckyToExcel两个回调参数的用途容易用反我一开始就是只装了 luckysheet对着文件对象发愁怎么塞进去翻源码才发现入口在 luckyExcel 这边。1.2 为什么不直接上商业表格组件市面上能选的其实不止一家我当时列了三个候选做对比最后基于项目实际情况定的。方案公式支持Excel 样式还原授权维护状态luckysheet luckyExcel内置引擎覆盖常用函数中等偏上配色边框合并基本能带过来MIT主仓库更新放缓社区分支还在动商业表格组件完整兼容度高高需采购活跃纯 xlsx 解析 自研表格得自己接公式库需要自己做工作量巨大视依赖而定完全自主真要说动我的是改动成本。这个项目里用户主要是改数字、改几行备注不需要透视表和 VBA 那种级别的东西。商业组件当然更稳但预算审批那一关就得走好几周。自研渲染就更不用想了光是把合并单元格和冻结窗格的坐标算对就够折腾半个月。反过来luckysheet 的 JSON 结构透明出问题能直接打印出来看排查效率高得多。风险也得提前摆到桌面上一是主仓库迭代慢遇到新版本 Excel 的某些特性可能不支持二是包体积不小luckysheet 加上自带的 ExcelJS压缩后也是好几兆首屏直接同步加载会拖慢页面。这两个问题后面章节里我会给出对应的处理办法。1.3 先把技术栈和版本钉死这一步很多人跳过然后被坑。我的建议是装完立刻把版本号写死到 package.json 的 dependencies 里别用^。原因很实在——luckysheet 依赖 jQuery 的老版本而 luckyExcel 打包进去的 ExcelJS 版本又会影响图片和样式的解析行为。你本地跑通、同事拉下来跑不通八成就是某个依赖悄悄升了小版本。我用的组合是 jQuery 3.xluckysheet 官方也兼容 luckysheet 2.1.13 luckyexcel 1.1.0 这个量级的版本构建工具用 Vite。Vite 那边要注意luckysheet 的 CSS 是分散在好几个文件里的得一个一个引漏一个就会出现字体错乱或者右键菜单没样式的情况。这部分放到下一章细说。2. 本地文件导入从硬盘到页面的完整链路2.1 引入方式选 npm 还是 CDN先看你的打包配置两种方式都行看场景。如果是新项目、用 Vite 或 Webpack推荐 npm 装然后按需异步加载npm i luckysheet luckyexcel jquery --save-exact样式文件必须全部引进来少一个都不行import luckysheet/dist/plugins/css/pluginsCss.css; import luckysheet/dist/plugins/plugins.css; import luckysheet/dist/css/luckysheet.css; import luckysheet/dist/assets/iconfont/iconfont.css;注意上面的路径在不同版本里可能有细微差别装完之后去node_modules/luckysheet/dist下看一眼目录结构再写 import比照着别人的博客抄靠谱。如果项目本身是个老系统或者你只想先做个 Demo 验证用 CDN 更省事把 js 和 css 用 script 和 link 标签引进来就直接能用全局变量。但有个前提CDN 引法下 luckysheet 会挂到 window 上如果你同时在用模块化打包可能出现两份 jQuery 打架页面表现为表格能渲染但点击没反应。这个坑我踩过排查了两个小时。2.2 文件读取三种方式只有一种是对的拿到input typefile的 File 对象之后读取方式决定成败。第一种直接把 File 对象交给 luckyExcel。这是最省事的const file document.getElementById(upload).files[0]; LuckyExcel.transformExcelToLucky(file, successCallback, errorCallback);它内部会自己读二进制你什么都不用管。95% 的场景用这个就够了。第二种自己用 FileReader 读成 ArrayBuffer。const reader new FileReader(); reader.onload (e) { const buffer e.target.result; // ArrayBuffer // 有些版本支持传 ArrayBuffer有些只认 File先看文档 }; reader.readAsArrayBuffer(file);什么时候需要这么做当你想在解析之前先对文件做个校验比如判断大小、算个哈希用于去重或者要把文件内容顺手传给服务端备份时。第三种readAsText——千万别用。这一条是我专门拎出来提醒的。Excel 是 zip 压缩的二进制格式用文本方式读会直接破坏字节解析出来的结果要么报错要么变成一堆乱码。只有 CSV 才走文本读取而且 CSV 还要单独处理编码问题。说到编码这里插一句不少业务系统导出的 CSV 是 GBK 编码的用默认的 UTF-8 读出来中文全是问号。这种情况得手动解码const reader new FileReader(); reader.onload (e) { const buf e.target.result; const text new TextDecoder(gbk).decode(new Uint8Array(buf)); // 再用 split(\n) 自己拆行传进表格 }; reader.readAsArrayBuffer(file);浏览器原生TextDecoder对 gbk 的支持在主流内核里都没问题不用额外装库。注意 CSV 不走 luckyExcel 这条路它是纯文本你得自己拆成二维数组然后用luckysheet.create时的 data 参数直接喂进去。2.3 transformExcelToLucky 的两个回调参数别用反了这是最容易出错的地方。它的成功回调有三个参数LuckyExcel.transformExcelToLucky(file, function (exportJson, luckysheetfile) { if (!exportJson.sheets || exportJson.sheets.length 0) { console.warn(没有解析到有效的工作表); return; } // 渲染用 exportJson.sheets luckysheet.destroy(); luckysheet.create({ container: luckysheet, data: exportJson.sheets, title: exportJson.info exportJson.info.name, lang: zh, showinfobar: false, allowEdit: true, enableAddRow: true, showsheetbar: true }); // 留着 exportJson 备用导出时用的是从 luckysheet 里现取的 window.__rawExportJson exportJson; }, function (err) { console.error(解析失败, err); });关键区别exportJson.sheets是给渲染用的包含它自己定义的完整结构luckysheetfile是给导出用的中间产物。很多教程让新手把两者互换结果就是表格能显示但导出全是错位的空白表。我个人的习惯是渲染只用exportJson.sheets导出时重新从 luckysheet 实例里取最新数据不缓存luckysheetfile。还有一点重复导入同一个文件的时候一定要先luckysheet.destroy()。不销毁直接再 create旧实例的 DOM 和事件监听不会自动清掉表现是页面上出现两个表格叠在一起或者滚动条行为错乱。2.4 渲染参数逐条过一遍luckysheet.create的参数很多但真正影响这个场景的就那么几个。我按重要性排一下。参数建议值说明container容器 id容器必须有明确的宽高height 用 px 或 vh 都行height: auto会白屏dataexportJson.sheets导入时用这个别自己拼langzh不改的话右键菜单是英文showinfobarfalse顶部那条文件名信息栏嵌入系统里一般不需要allowEdittrue纯查看场景改成 false能省掉不少事件开销showsheetbartrue多 sheet 文件必须开否则用户切不了页hook见 3.4拿编辑后数据的关键容器的高度问题值得单独说一句。luckysheet 是画在 canvas 上的它在初始化时会去读容器的实际尺寸。如果你的容器是通过display: none隐藏的比如放在一个还没打开的 Tab 里读到的尺寸是 0画出来就是一片空白而且不会报错。解决办法有两个要么等 Tab 切换之后再 create要么用luckysheet.resize()手动触发一次重绘。3. 公式计算官方没讲透的几个细节3.1 公式在 JSON 里长什么样搞明白这一点后面所有异常都好定位。luckysheet 里一个带公式的单元格结构大概是这样{ v: 10, // 计算出来的值 m: 10, // 显示出来的文本 f: SUM(A1:A5), // 公式原文 ct: { fa: General, t: n } // 格式类型和格式串 }f是必须的v和m可以是引擎算出来的也可以是从文件里读出来的缓存值。问题就出在这个缓存值上xlsx 文件里保存公式单元格时同时会保存一份上次打开时计算的结果。luckyExcel 解析的时候把这份缓存值一并读进了v和m。所以你会看到一个诡异现象——表在别的软件里改过数据但没保存计算导入到网页里显示的公式结果就是旧的。3.2 强制重算的三种办法想让公式按当前数据重新算一遍有三条路我按推荐度排。第一条清掉缓存值只留公式。这是最稳的做法不依赖版本差异function forceRecalc(sheets) { sheets.forEach(sheet { (sheet.celldata || []).forEach(cell { const cv cell.v; if (cv typeof cv.f string cv.f.startsWith()) { delete cv.v; delete cv.m; } }); }); return sheets; }清完之后再传给luckysheet.create引擎发现只有公式没有值就会自己算一遍。注意 datac 字段有时也会带缓存结果如果清完还是显示旧值把这个字段一并处理掉。第二条找官方提供的重算 API。版本之间的方法名不太统一有的版本是luckysheet.refreshFormula()这一类有的版本干脆没有暴露。我建议装完之后直接在node_modules/luckysheet/dist里全局搜recalc或者formula看看你这版到底提供了什么比照文档猜要快。第三条自己算。引一个第三方公式库在 Web Worker 里算完再回写v和m。这条路工作量大但好处是完全可控还能顺手支持一些引擎不认的函数。除非你的表里有大量冷门函数否则不建议一上来就走这条。3.3 拿蔡勒公式当验证用例公式引擎靠不靠谱得找个能手工验算的例子来对。我习惯用日期函数因为出错最容易发现。蔡勒公式算星期几的经典案例2027 年 2 月 6 日是星期几按蔡勒公式2 月要当作上一年的 14 月处理所以年份取 2026。分解一下日 q6月 m14年后两位 K26世纪数 J20。代入公式h (q ⌊13(m1)/5⌋ K ⌊K/4⌋ ⌊J/4⌋ 5J) mod 7 (6 ⌊13×15/5⌋ 26 ⌊26/4⌋ ⌊20/4⌋ 100) mod 7 (6 39 26 6 5 100) mod 7 182 mod 7 0h 等于 0 对应星期六所以答案是2027 年 2 月 6 日是星期六。可以再用另一个办法验一遍2027 年 1 月 1 日是星期五1 月有 31 天31 除以 7 余 3所以 2 月 1 日是星期一往后推 5 天就是星期六。两边对上了。验证表格里公式的时候我用WEEKDAY(DATE(2027,2,6),2)返回 6这种模式下周一是 1所以 6 代表周六和手算结果一致。这里有个坑要提醒TEXT(DATE(2027,2,6),aaaa)这种写法在 Excel 里能直接输出星期六但在部分 luckysheet 版本里对aaaa这种自定义格式串的支持不完整可能返回一串数字或者原样输出。稳妥起见用WEEKDAY配合CHOOSE自己拼中文兼容性好得多。3.4 编辑回写靠 hook 拿最新数据用户改了单元格之后你得知道改了什么。luckysheet 提供的是 hook 机制hook: { workbookCreateAfter: function () { console.log(渲染完成此时取数据最安全); }, updated: function (operate) { // operate 里带着操作类型和受影响的单元格 console.log(用户操作了表格, operate); // 别在这里做重活会卡输入 }, cellUpdated: function (row, col, oldValue, newValue, isRefresh) { // 单元格级别频率很高只做轻量记录 } }关键经验hook 里不要做耗时操作。cellUpdated一次输入可能触发好几次里面如果塞了网络请求或者整表遍历用户打字会明显掉帧。正确做法是在 hook 里只打标记然后用防抖debounce在外面统一收集。需要整表快照的时候调luckysheet.getAllSheets()它返回的是当前所有工作表的最新结构包含了用户的所有改动。这个结果既能用来导出也能用来存到后端。4. 导出 Excel把渲染结果打包回去4.1 基本调用与下载导出这条链路比导入简单核心就一句function exportExcel() { const sheets luckysheet.getAllSheets(); LuckyExcel.transformLuckyToExcel(sheets, function (blob) { const url URL.createObjectURL(blob); const a document.createElement(a); a.href url; a.download (window.__rawExportJson?.info?.name || 导出结果) .xlsx; document.body.appendChild(a); a.click(); document.body.removeChild(a); // 一定要释放否则内存会一直涨 setTimeout(() URL.revokeObjectURL(url), 1000); }); }三个注意点。第一一定要用getAllSheets()的返回值不要用 create 时传进去的原始 data那份数据是渲染前的快照用户改过的内容它不知道。第二URL.revokeObjectURL必须调不然连续导出几次内存就上去了用户下载文件夹里也会莫名多出临时文件。第三导出是异步的如果表比较大中间要给个 loading 提示否则用户以为按钮没反应会连点。4.2 样式、合并、图片的还原边界很多人关心导出来的文件和原文件看起来一不一样。我的实测结论是配色、字体、边框、合并单元格、行高列宽这些基础样式还原度不错图表、数据透视表、条件格式的部分高级特性会有损失。下面这张表是我实测下来最实用的参考。元素类型导入还原导出还原备注单元格底色和字体好好基本无损边框、合并单元格好好复杂嵌套合并偶尔错位数字格式百分比、货币较好中等自定义格式串容易丢公式好中等依赖引擎支持的函数范围单元格内嵌图片中等需要额外处理见 4.3图表、透视表差差建议保留原文件做兜底数字格式这里有个隐形坑如果你的表里是日期导出的 xlsx 里可能显示成一串数字比如 45678 这种。这是因为日期在 Excel 内部就是序列号如果格式串丢了它就按普通数字渲染。解决办法是在导出之前遍历一遍单元格把日期类单元格的ct.fa显式设成yyyy-mm-dd这类格式串再打包。4.3 导出带图片走 base64 这条路前端导出 Excel 带图片这个问题被问得特别多。luckyExcel 对单元格内嵌图片的支持不稳定我的做法是导出主体数据用 luckyExcel图片用 ExcelJS 单独补上去。首先要保证图片在内存里是 base64 格式。如果用户插入的图片是本地文件渲染时读进来就是 dataURL直接能用。但如果图片来自网络地址就会遇到跨域问题canvas 拿不到像素数据base64 也生成不了。这种情况要么让图片走同源代理要么提前下载转成 base64 再喂进去。补图片的代码大致是这样import ExcelJS from exceljs; async function exportWithImages(sheets, imageMap) { const wb new ExcelJS.Workbook(); const ws wb.addWorksheet(sheets[0].name || Sheet1); // imageMap 形如 { A1: data:image/png;base64,xxxx } for (const [addr, dataUrl] of Object.entries(imageMap)) { const match dataUrl.match(/^data:image\/(\w);base64,/); if (!match) continue; const ext match[1] jpeg ? jpeg : match[1]; const imageId wb.addImage({ base64: dataUrl, extension: ext }); // 把 A1 这种地址换算成行列索引 const col addr.charCodeAt(0) - 65; const row parseInt(addr.slice(1), 10) - 1; ws.addImage(imageId, { tl: { col, row }, ext: { width: 160, height: 120 } }); } const buffer await wb.xlsx.writeBuffer(); return new Blob([buffer], { type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet }); }这里我踩过一个坑wb.addImage里的base64字段如果带了data:image/png;base64,这个前缀某些版本会报错得把前缀剥掉只留纯 base64 串。另外坐标换算只处理了 A 到 Z 列超过 26 列就得写个正经的列名转索引函数别偷懒。4.4 什么时候该放弃纯前端导出纯前端导出不是万能的三种情况下我建议转服务端处理文件超过十兆、需要设置打开密码、原文件里有复杂图表必须原样保留。这三种情况前端库都搞不定或者搞起来很不划算。一个折中方案是前端把getAllSheets()的 JSON 传到后端后端用 Node 里的 ExcelJS 或者别的库生成文件再返回下载地址。这样前端逻辑不变只是把最后的打包动作挪走图片和样式反而更好控制。5. 踩坑记录与问题速查5.1 导入阶段的典型报错现象大概率原因处理思路页面空白控制台无报错容器高度为 0检查 CSS或调 luckysheet.resize()报解析失败文件不是真正的 xlsx确认扩展名和实际格式一致老式 .xls 不支持中文全是乱码CSV 走了二进制或编码不对用 TextDecoder(gbk) 手动解码表格出来但没样式CSS 文件漏引对照 dist 目录逐个补齐一个表能显示另一个空文件里有隐藏 sheet 或空 sheet过滤掉没有 celldata 的 sheet.xls 这个格式要特别说一下。luckyExcel 认的是基于 zip 的新版 xlsx 格式那种很老的二进制 .xls 它读不了。如果用户上传这种文件你应该在解析之前就给个明确提示而不是让它在回调里报一个看不懂的错。判断办法是读文件头几个字节xlsx 是PK开头老 xls 是D0 CF 11 E0开头。5.2 公式相关的异常排查公式这块的异常有个通用的定位顺序我整理成了固定动作。第一步console.log打出单元格的完整对象看f字段在不在。f不在说明公式压根没解析进来问题在导入环节f在但结果不对问题在计算引擎。第二步看ct.fa的格式串。很多公式算错其实是格式问题比如0.1 0.2显示成 0.3 看着正常但实际存储是 0.30000000000000004一做相等判断就挂。第三步把公式单独拎出来在 Excel 里跑一遍。如果 Excel 也不对那是公式本身写错了Excel 对而页面不对那就是引擎的兼容问题得换写法或者自己实现。常见的结果异常还有#VALUE!和#NAME?。前者一般是参数类型不对比如文本参与算术运算后者基本可以断定是函数名不被引擎支持。遇到不支持的函数我的做法是给用户一个降级提示同时把原始公式和缓存值一起保留导出时原样带回去让 Excel 自己去算。5.3 性能大表格必须做的取舍luckysheet 是 canvas 渲染几万行数据它本身能撑住但会明显吃内存。我实测过一份两万行、三十列的表导入耗时大概两三秒滚动起来开始有轻微卡顿长时间停留后浏览器内存能涨到几百兆。能做的优化有这么几件。一是导入前裁剪数据范围把末尾大量空行空列砍掉——很多从别的系统导出的表出于格式原因会带着几万行空行这些空行用户根本看不到纯属白白占内存。二是限制单次导入的 sheet 数量一张表里塞十几个 sheet 的按需只加载用户点开的那个。三是及时销毁切换文件或者关闭页面时调luckysheet.destroy()不销毁的话旧实例的定时器和事件监听会一直挂着。还有一个容易被忽略的点如果你的项目里同时用了 ECharts 之类同样基于 canvas 的库页面上的 canvas 数量会迅速堆上去移动端上表现尤其明显。这种情况建议把表格放到独立的 Tab 或者全屏弹层里别和图表堆在同一屏。5.4 完整的速查表场景关键动作记住的一句话本地导入transformExcelToLucky传 File 对象别传文本渲染luckysheet.create容器必须有高度重复导入先 destroy公式重算清空 v/m 只留 f缓存值是万恶之源取最新数据getAllSheets千万别用 create 时的原始 data导出transformLuckyToExcel记得 revokeObjectURL带图片导出ExcelJS 手动 addImagebase64 前缀可能要剥掉大文件预先裁剪空行空列用户看不见的数据就别加载6. 上线前才想到的几件小事6.1 自适应和容器管理页面尺寸变化的时候表格不会自己重排需要手动触发luckysheet.resize()。我的做法是在 window 的 resize 事件上加个防抖间隔 200 毫秒再调不然拖拽窗口的时候会疯狂重绘。另外如果你的表格嵌在弹窗里弹窗关闭时别忘了 destroy。有些弹窗组件为了性能会把 DOM 缓存起来下次打开还是旧的那份这时候再 create 就会出现两个实例。6.2 数据存哪里存 JSON 比存 xlsx 更划算这是个架构层面的选择。用户编辑完之后你当然可以把文件导出来存到对象存储里但更推荐把getAllSheets()的 JSON 存进数据库的一个文本字段。原因有三JSON 可以直接读取回显不用再解析一遍JSON 可以做差异对比知道用户改了哪个格子体积通常比 xlsx 小。回显的时候判断一下数据来源如果是从数据库读的 JSON直接传给 create 的 data 参数如果是用户上传的文件走 luckyExcel 那条路。两条路最后都汇到同一个渲染函数上代码不用写两份。6.3 版本锁定和后续迁移luckysheet 主仓库更新放缓这件事得有个应对预案。我的做法是在项目里包一层薄薄的适配层——所有对 luckysheet 的调用都走自己封装的sheetAdapter.js里面只暴露 init、loadData、getData、exportFile 这几个方法。这样将来真要换成别的表格方案比如后来出现的 Univer 这类新库只需要重写适配层业务代码一行不动。这层封装带来的另一个好处是测试方便。适配层可以做纯函数式的数据转换导入导出逻辑都可以写单元测试不用起浏览器就能跑。6.4 一点个人体会这套东西真正花时间的不是 API 调用而是处理各种不那么标准的真实文件。我拿到的测试文件里有从采集软件导出的几十列纯数值表有从地理信息工具导出的带大量合并单元格的统计表还有同事为了对账手工拼的、公式套了五六层的表。每一份都能暴露一两个新问题。我的建议是动手写代码之前先攒一批真实文件当测试集覆盖各种奇怪情况然后每解决一个问题就往测试集里加一个。这么搞下来你会发现后面再遇到类似的文件心里基本有数了——看一眼结构就知道它会卡在哪一步。这比对着文档把 API 都试一遍要有用得多。最后再分享一个小技巧调试导入问题的时候把exportJson打印出来存成 JSON 文件用编辑器打开看结构比在控制台里一层层点开快得多。表格数据是嵌套很深的数组控制台折叠起来能让人抓狂导出成文件之后用编辑器的折叠功能看效率完全不一样。
返回列表