ARTICLE DETAIL

资讯详情

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

Univer接入实战:用Canvas渲染打造高性能在线Excel表格

Univer接入实战:用Canvas渲染打造高性能在线Excel表格 如果你做过任何一个带数据录入、导入导出、汇总分析的企业后台大概率躲不过一个需求要一个像 Excel 一样的在线表格。我最初的做法是在项目里引入成熟表格组件结果发现要么交互像古董要么改样式要覆盖一大堆私有类名要么提供了“Excel 体验”却在 10 万行数据面前卡成幻灯片。后来我把目光转向开源项目 Univer——一个基于 TypeScript、用 Canvas 渲染的办公套件不仅支持表格还覆盖文档和幻灯片。这个项目彻底改变了我做数据产品时的选型思路。这篇文章就围绕 Univer 的实际接入和使用展开适合正在做 Web 端表格/数据产品、想用开源方案又不想被坑的开发者和技术决策者参考。1. 从 Excel 到表格组件Univer 到底在解决什么1.1 一个被表格组件反复折磨的场景先说一个我真实遇到过的需求某内部订单管理系统业务方明确要求“录入界面要跟 Excel 一样能多 sheet 切换、能合并单元格、能条件格式、能公式联动”。当时系统里已经有一张自研的表格用的是一堆 Div 拼出来的网格。前几千行没问题一旦加上公式、条件格式和复制粘贴交互就开始变得很别扭。尤其是用户从本地 Excel 复制一大片数据粘贴进来时浏览器会卡住几秒钟然后粘贴过去的内容格式全丢了。这一幕我至今印象很深产品经理在旁边捏着手机录像等表格恢复响应等了大概 20 秒。这不是组件库好不好看的问题而是底层渲染思路的问题。传统的 DOM 表格在单元格数量上去之后DOM 节点数量爆炸布局计算、样式重算、事件绑定都会成为瓶颈。你优化了横向滚动、冻结首行但本质上还是在跟浏览器渲染机制作对。Univer 出现之后我重新理解了这件事。它不把自己定位成“表格组件”而是一套自底向上的办公套件基础平台。核心思路是表格、文档、幻灯片都跑在同一个 Canvas 渲染引擎之上业务逻辑通过插件方式按需加载。对开发者来说这意味着你可以像搭积木一样往平台里加能力而不是在一个组件的回调里面写各种 hack。1.2 渲染层和业务层的彻底解耦Univer 最值得先理解的一点是它把渲染层跟业务层拆得很干净。它的底层渲染引擎直接操作 Canvas绘制单元格、边框、文本、图表跟游戏引擎的思路更像而不是前端组件库的思路。这种设计带来的直接好处是当你需要渲染一张 1 万行乘以 30 列的工作表时DOM 里不会出现几十万个节点。Univer 只渲染可视区域内的那些格子滚动的时候动态补齐视口内容。你操作的还是“工作簿”“工作表”“单元格”这样的高层 API但底层实际画出来的只有当前用户眼睛能看到的部分。拆分还体现在插件化架构上。Univer 通过 IoC 容器来管理依赖每个业务能力都是一个插件。你想给表格加公式就引入公式插件想加条件格式就引入条件格式插件想支持 XLSX 导入导出就引入对应的导入导出插件。不想用的能力可以不加载不会一股脑塞给你一个动辄几 MB 起步、没法裁剪的全家桶。再加上 Univer 本身框架无关官方提供了一组适配层可以接入原生 TS 项目、React 项目也可以接入 Vue 项目。团队内部有人纠结“你用的 React 组件我们 Vue 项目怎么接”时这个问题在 Univer 这里是基本不存在的。1.3 不只是表格一台基于 Canvas 的迷你办公平台大多数人第一次知道 Univer 是因为表格但它的目标远不止于此。目前 Univer 已经覆盖了三类主要文档形态Univer Sheet、Univer Doc、Univer Slide。换句话说Univer 的核心能力其实是一整套“文档引擎”而表格只是这套引擎上运行最成熟的一个应用。它读取数据、把数据交给渲染引擎绘制、通过插件扩展交互这套逻辑在三种文档类型上是共通的。正因为底层一致未来可以在同一份工作区里混排表格、文档内容甚至把图表从一个区域拖到另一个区域。这种统一底座带来的长期价值其实比“做了个好看的表格”大得多。对于中小研发团队来说与其分别调研电子表格组件、富文本编辑器、幻灯片组件再想办法拼在一起不如从一开始就站在一个统一架构上按需点亮对应的能力。当然Doc 和 Slide 目前的成熟度还比 Sheet 要弱一些正常业务里我还是建议主力用 Sheet但架构上它值得你持续关注。2. 2.x 时代的新手接入preset 让你 10 分钟跑起一个在线表格2.1 依赖安装与最小初始化Univer 目前的稳定版本是 2.x 系列。在这个版本里官方推荐使用“preset”预设模式来初始化不再需要像 1.x 那样手动安装十几个分散插件并组装依赖树。我建议你直接在一个干净的前端工程里试比如 Vite 项目。创建完工程之后安装基础依赖npm install univerjs/presets如果需要更明确的业务模块也可以手动按需安装npm install univerjs/presets univerjs/core univerjs/sheets univerjs/sheets-ui然后在入口文件里初始化。这里给一份实际能跑的 2.x 最小示例import { LocaleType, Univer, UniverSheetsCorePreset, UniverSheetsUIPreset } from univerjs/presets; import univerjs/presets/lib/styles.css; const univer new Univer({ locale: LocaleType.ZH_CN, }); // 先注册核心预设再注册 UI 预设 univer.addPlugin(UniverSheetsCorePreset()); univer.addPlugin(UniverSheetsUIPreset({ container: app, }));这段代码的作用是创建一个 Univer 实例指定中文语言加载“电子表格核心能力”和“电子表格 UI 界面”并把编辑器挂载到页面 id 为 app 的 DOM 容器上。这里容易忽略一个点插件注册顺序不能乱。核心预设负责创建文档、管理工作簿、路由计算公式UI 预设负责工具栏、右键菜单、单元格编辑框等交互层。如果 UI 预设先注册在核心预设尚未就绪时可能拿不到模型层的数据表现就是表格区域一片空白或者控制台报错。对应地你的页面里需要一个指定高度的容器div idapp stylewidth: 100%; height: 600px;/div高度不能省略。Univer 的布局计算依赖容器尺寸容器高度为 0 时表格区域什么都画不出来。我在第一次接入时就在这里踩过坑页面里 div 确实挂了但忘了给高度看起来像初始化失败其实只是渲染区域没有尺寸。2.2 跑通第一张可交互表格初始化完成后Univer 会自动创建一个空白工作簿。打开页面你能看到一个接近本地 Excel 体验的界面顶部有工具栏左侧有行号顶部有列号底部可以切换 Sheet 标签页。双击单元格可以直接进入编辑状态输入内容、敲回车光标会自动向下移动。这个默认体验已经能覆盖相当多基础场景。你不需要再额外写什么逻辑就能获得以下能力多 Sheet 的创建、删除、重命名、拖拽排序单元格的合并、拆分、边框、底色、对齐行高列宽的拖拽调整支持基础运算的公式输入例如SUM(A1:A5)撤销重做右键菜单里的复制、粘贴、插入、删除等操作我觉得这一步最核心的意义是验证你的工程环境。如果初始化能跑通说明打包工具、依赖解析、样式加载都是正常的。后面接业务数据仅仅是调用 API 的问题不会再花时间排环境。如果你需要让表格“开箱即用”地支持更多能力比如条件格式、数据透视表、打印、XLSX 导入导出也是在 Univer 实例上继续 addPlugin。例如import { UniverSheetsFormulaPreset, UniverSheetsConditionalFormattingPreset, UniverSheetsImportXLSXPreset } from univerjs/presets; univer.addPlugin(UniverSheetsFormulaPreset()); univer.addPlugin(UniverSheetsConditionalFormattingPreset()); univer.addPlugin(UniverSheetsImportXLSXPreset());这种按需加载的好处是你不需要一开始就引入全部功能等产品经理提新需求时再加插件就行而且插件机制保证了这些功能不是依赖“移植一段代码”或者“覆盖组件内部状态”而是官方定义好的扩展点。2.3 通过 API 写入和读取业务数据跑通空白表格之后下一步就是让你的业务数据进入表格。Univer 的模型层是独立于 UI 的这意味着你既能通过用户界面编辑表格也能通过 API 从代码侧写入数据两边操作实时同步。我通常会在初始化完成后获取当前工作簿然后拿到活动工作表再通过 Range 对象写入数据。大致思路如下const workbook univer.getCurrentWorkbook(); const sheet workbook.getActiveSheet(); const range sheet.getRange(0, 0, 5, 5); range.setValues([ [订单号, 商品, 数量, 单价, 金额], [A001, 机械键盘, 1, 399, 399], [A002, 显示器, 2, 1299, 2598], [A003, 鼠标, 3, 99, 297], [A004, 扩展坞, 1, 249, 249], ]);这里需要注意 API 的定位方式getRange(0, 0, 5, 5)的四个参数分别代表“起始行索引、起始列索引、总行数、总列数”。第一次接触时我习惯写 Excel 的 A1 坐标很容易理解成行列坐标是 1 开头结果数据整体往下错了一行。实际 Univer 的行列下标是从 0 开始计数的。公式也可以通过代码写入sheet.getRange(1, 4, 4, 1).setFormula(C2*D2); sheet.getRange(2, 4, 3, 1).setFormula(C3*D3);写入之后表格界面会实时刷新。用户如果再在界面上修改 C 列或 D 列对应行的金额会自动重算。这是 Univer 公式引擎在起作用而不是你自己监听单元格变化再去更新。读取数据也比较直接const values sheet.getRange(0, 0, 5, 5).getValues(); console.log(values);返回的是一个二维数组每一行对应表格里的一个数据行。用它来回显业务数据非常方便不需要自己去维护 DOM 输入框值。有一点要提醒Univer 是典型的多端共享数据模型也就是说如果你在代码里写了一个setTimeout延迟去调用setValues但这期间用户在界面上手工改了单元格那么你的代码写入行为与用户操作是并发的。真实业务里我倾向于在表格初始化完成、用户还没开始大量录入之前一次性把初始数据灌进去后续增量更新通过监听数据变更事件来做而不是粗暴地反复 setValues。3. 接真实业务数据时踩过的坑版本断层、样式丢失与自定义控件3.1 教程和代码版本对不上先确认 2.xUniver 这个项目演进的节奏非常快尤其从 1.x 到 2.xAPI 经历了一次大范围的整理。你在网上搜到的很多教程、博客、公司分享文章可能是基于 1.x 甚至更早期版本写的照抄下来大概率跑不通。我当时遇到一个典型的错误按照一篇社区文章引入了univerjs/core和一堆univerjs/sheets/plugins/xxx路径结果初始化时直接报错提示找不到对应的 plugin 类型。后来翻官方 GitHub 仓库的示例工程才发现这个版本已经用univerjs/presets一把梭了以前分散的插件名大量被合并、改名或移动到其他包路径。所以我的建议非常简单遇到 API 不确定不要先搜博客先看官方仓库 example 目录里的最新示例。Univer 的主仓库里带了不少可以直接跑的示例工程覆盖了基础表格、公式、导入导出、协同等场景。把示例工程跑起来再对照文档把代码搬到自己项目里是最少走弯路的方式。还有一个更隐蔽的版本坑即使都是 2.xpresets包内部也经历过多轮细节调整。例如UniverSheetsUIPreset({ container: app })这个参数形式在较新版本里是推荐的但一些早期 2.x 版本可能需要额外传入 layout 或 theme 配置。保险的做法是锁住版本号不要用latest漂移。在 package.json 里固定一个你已经验证过的版本例如univerjs/presets: 0.x.x具体版本号以官方发布为准这样可以避免一段时间后重新 npm install 时拉到不兼容的新 API。3.2 XLSX 导入导出样式和公式的取舍接入 Univer 之后用户第一个高频需求往往是“能不能直接把本地 Excel 文件打开编辑”。Univer 目前通过插件支持 XLSX 的导入和导出这个能力是加分项但作为实际用过的人我提醒你几点现实问题。导入一个大一点、样式复杂一点的 XLSXUniver 不会百分百还原所有内容。我测试过一个带条件格式、数据条、复杂公式、跨 sheet 引用、自定义数字格式的文件整体表现已经很不错但部分条件格式规则会被简化个别单元格的字体和边框设置有细微差异。这不算 Univer 特有毛病任何非微软原生的表格引擎去解析 XLSX都会遇到类似问题因为 XLSX 本身是一个格式细节非常繁杂的压缩包。应对方式上我在项目里做了两层第一层导入之前先用脚本做数据清洗。比如把所有单元格转成“常规”格式避免大量自定义日期格式干扰渲染去掉不必要的条件格式规则保留数据颜色高亮即可。第二层导入之后给用户一个提示条“文件已打开部分高级样式可能被简化”。这不是免责声明而是让业务方对数据呈现有正确预期避免后续验收时出幺蛾子。导出 XLSX 时Univer 的机制是把内部数据模型序列化输出而不是把 Canvas 截一张图塞进文件。所以导出的文件是可以被 Excel 编辑的公式也会保留。这一点实测下来比某些“导出图片式 PDF”的方案要靠谱很多。如果你需要导出 PDFUniver 也有对应的打印/导出插件但坦白说我在打印分页这类细节上还没做到极致内网项目一般还是导出 XLSX 为主。3.3 自定义控件单元格 renderer 与跳出表格的 UI企业产品里真正让人头疼的从来不是“把表格显示出来”而是“在表格里塞自定义控件”。比如在某一列显示状态标签比如“已完成/待审核/异常”或者在某些行首放一个操作按钮。Univer 在这块提供了自定义渲染的扩展机制。你可以注册自定义单元格渲染器在 Canvas 上绘制你自己的内容。这个机制很灵活但代价是你得在 Canvas 上下文里自己画文本、画矩形、处理 hover 状态。跟传统 DOM 组件“写个 JSX 就完事”的体验完全不同初期上手有一定成本。我的实际取舍是如果只是“看起来像控件”的展示型内容比如一个状态圆点、一个进度条、一个分类标签我会用自定义渲染器画出来性能好、也不破坏表格的滚动体验。如果涉及真正的交互比如点击单元格内容弹出一个抽屉、打开一个日期选择器、或者让用户在下拉框里选人我不再死磕表格内部渲染而是监听表格单元格的点击事件在表格上方叠加浮层来处理。Univer 社区也有不少示例展示这种“Renderer Overlay”的组合姿态。这种方案的好处是交互逻辑更贴近传统前端开发团队里的普通前端也能维护不会把业务代码全部逼进 Canvas 世界。另外提醒一句不要一开始就把业务逻辑全部塞进单元格渲染函数。渲染函数只负责根据数据画形状真正的状态管理应该放在你的业务 Store 里。否则后续涉及到单元格数据更新、Undo/Redo、协同同步时会多出一堆边界情况等着你填坑。4. 渲染与公式机制Canvas 分层、计算链为什么值得信任4.1 Canvas 分层渲染的直觉理解大多数开发者对 Univer 性能的信心来源是“用了 Canvas”但 Canvas 本身也分很多种玩法。直接在一个 Canvas 上把所有内容画出来滚动时会全量重绘性能同样会崩。Univer 的聪明之处在于分层渲染。可以把它想象成一个电影制作流程背景层负责画单元格底色和网格线文字层负责画单元格内容浮动层负责画图表、图片、下拉框指示器。每次滚动或编辑时不一定要全部重新画只有内容变化的那一层需要刷新其他层继续沿用上一次的绘制结果。再加上“离屏 Canvas 缓存”的做法把那些重绘成本高、又不经常变化的内容比如表头、边框、行号列号先画到隐藏的 Canvas 上滚动时直接整块贴上去而不是重新执行几十万次绘图命令。这跟游戏引擎里把静态背景烘焙成一张贴图的思路是一个道理。我实测过一个 5 万行、20 列左右的工作表里面填了大量测试数据常规滚动和键盘方向键移动都非常顺畅。同样的数据量如果用之前 DOM 表格方案第一步生成那么多行节点就已经能让人等待到失去耐心了。但要注意Canvas 渲染也带来了非技术层面的代价。文本无法被浏览器的“查找”功能直接命中DOM 选择器选不中单元格文本盲人读屏软件默认也读不到内容。Univer 团队意识到了这个问题并在持续完善可访问性支持做无障碍要求较高的政务、金融类产品时你仍然需要预留额外的工作量来做 A11Y 适配这也是我在项目里实际体会到的一个盲区。4.2 公式引擎计算链让公式不拖垮整体性能Univer 的公式引擎不是简单的“单元格变了就全表重算”。它内部维护了一张公式依赖图知道哪个单元格引用了哪些值哪个单元格被哪些公式所引用。你可以把这张依赖图想象成一套“连锁反应清单”。当用户修改了 A1 单元格引擎会顺着依赖关系找出所有“直接或间接引用 A1 的公式”然后只重算真正受影响的那些单元格。如果 B1 的公式是SUM(A1:A100)而 C1 是个不相关的常量文本那么用户改 A1 时 C1 完全不需要参与计算。这种局部更新机制决定了公式数量多的情况下交互仍然能保持顺畅。我压测过一个接近 4 万行、带约两千个 SUM 和 IF 函数的工作簿在普通电脑上修改中间某一行数据公式结果刷新基本在百毫秒级别以内远远没有达到让人感觉卡顿的程度。当然这里也有边界如果工作表里出现了大量数组公式或者某个公式引用了一整列几万行数据引擎需要构建的计算范围会非常大首次构建依赖图和第一次全量计算仍然有压力。我在使用时对这些场景做了约束比如尽量避免整列引用改用有限范围如A2:A10000而不是A:A。这个习惯本来也是 Excel 性能优化的常识放到 Univer 里一样适用。Univer 还支持异步公式也就是公式结果不是同步算出来的而由外部网络请求或服务端计算返回。这种设计对对接后端模型计算、机器学习推理结果很有潜力我目前还没有深度用到但它在架构上给未来留了空间。5. 要不要选 Univer与 Luckysheet、Handsontable 的横向对比和适用边界5.1 和主流开源表格的横向对比我身边不少人会拿 Univer、Luckysheet、Handsontable 三者比较。这里用一个表来给出我的观察对比维度UniverLuckysheetHandsontable渲染方式Canvas 分层渲染DOM 渲染DOM 渲染完整 Excel 交互较好公式、条件格式、透视表等逐步完善功能丰富但大数据量卡顿明显数据网格强Excel 文档体验弱授权方式开源可商用需关注具体协议开源商业版另谈商业授权为主多框架支持框架无关官方适配 React/Vue以原生/jQuery 为主接入现代框架需要包一层官方提供 React/Vue/Angular 封装协同编辑官方架构支持可自建服务协同能力有限不具备完整协同文档编辑生态成熟度快速发展但 API 变化快曾很流行目前社区更新节奏明显放缓企业级成熟文档完善我要说明一下这里不是要“踩”Luckysheet。当年它确实是国内最流行的开源在线表格方案给很多人解决了问题。但它的 DOM 渲染底层决定了在数据量上来之后会遇到天花板而且社区活跃度这几年的下滑非常明显很多人在评估新项目时已经不太敢选了。Handsontable 在“数据网格”这个定位上做得非常成熟官方绑定框架也做得很好适合做类似 Excel 编辑界面但不需要完整办公套件体验的偏数据产品。主要门槛是商业授权费用以及它本身不准备做“一整套文档平台”这件事。Univer 在这个牌桌上最核心的差异化是它是从渲染引擎开始设计的目标是成为一套基础办公 SaaS 平台而不是一个表格控件。它更重但上限更高。5.2 什么场景适合选 Univer结合我这段时间的使用经验以下场景我认为可以放心把 Univer 放上候选名单要做“内嵌 Excel”体验不是简简单单地展示数据而是希望用户在界面里操作 Excel 的常用能力比如合并、公式、筛选、条件格式。要私有化部署。Univer 是开源的基础能力你可以把整套表格引擎嵌入到公司内网系统数据不需要上传到任何第三方 SaaS。技术栈多样。团队既有 React 项目又有 Vue 项目想统一表格方案Univer 的框架无关特性会省掉很多重复建设。愿意投入前端工程力量去跟随上游演进而不是买完一个商业组件就指望十年不动。反过来说如果你只需要一个“能排序、能筛选、能翻页”的数据表格Univer 属于大炮打蚊子。用 Ant Design Table、Element Plus Table或者 ag-Grid 的数据网格模式接入成本更低、团队最熟悉、文档也更多。如果你的产品已经上线且没有专职前端维护表格这块我也不建议中途强行引入 Univer因为版本变更带来的维护成本需要有人持续承担。还有一个常见的误区有人把 Univer 当成“免费版 Excel API”来用期望它能打开所有 XLSX 并完美输出。真实情况是开源表格引擎的解析导出能力跟商业 Office 套件相比仍有差距。你在选型时务必把“复杂的本地 Excel 文件导入”作为一个风险项并在项目早期做一个 PoC拿业务里最复杂的真实文件测试一遍再决定要不要全面铺开。最后分享一个我的使用习惯写了这么多最后说一个我实际总结的习惯。每当一个开源项目处于高速发展期我通常会做一个“版本跟随机制”在自己的业务代码里把 Univer 的初始化封装成一个独立模块业务方不直接 import Univer 内部的散装 API而是通过自己的封装层读写工作簿。这样当 Univer 升级到 3.x、API 再次变动时我只需要修改封装层的适配代码不需要跑到业务代码里到处找setValues和getRange改调用方式。这个习惯帮我避开了很多因为版本升级导致的连坐式改动。如果你决定用 Univer我强烈建议从一开始就做这一层隔离。毕竟在开源生态里项目活跃是一把双刃剑更新快说明生命力强但也意味着你需要和变化共处。
返回列表