资讯详情

资讯详情

Univer 表格引擎架构解析:Canvas 渲染、插件化与协同编辑实战

电子表格这东西几乎每个做企业软件的人都绕不开。但真正从零写一个能在浏览器里跑、还能多人同时编辑的表格引擎难度远超大多数人的想象——渲染性能、公式计算、协同冲突、插件扩展每一项单拎出来都够写一本书。Univer 就是冲着这个目标去的开源项目它把自己定位成一个表格与文档的通用底座用 Canvas 做渲染、用插件架构做扩展、用 Node.js 做服务端协同。我前后花了大概两个月时间研究它的源码和架构设计踩了不少坑也摸清了一些门道。这篇就把我对 Univer 的理解完整拆开讲一遍从它到底解决什么问题到核心架构怎么设计再到实际接入时哪些地方容易翻车尽量说透。1. Univer 到底在解决什么问题1.1 从再造一个在线表格说起大多数人第一次听到 Univer第一反应是又一个在线 Excel。这个理解不算错但太窄了。Univer 的野心其实更大——它想做的是一套可编程的电子表格与文档基础设施你可以把它理解成表格领域的操作系统内核上面能长出各种形态的产品。为什么这么说因为传统的在线表格方案基本分两类一类是直接买商业组件比如某些厂商的表格控件你只能在它给的框框里做定制想改底层渲染逻辑基本没戏另一类是自己从零撸但表格这东西的复杂度是典型的看起来简单做起来要命光是公式引擎和协同冲突处理就能拖垮一个团队半年。Univer 走的是第三条路开源内核 插件化扩展。它把表格最核心的能力单元格模型、公式计算、渲染引擎、协同层做成一套 SDK你既可以直接用它的默认实现快速搭出一个在线表格也可以按自己的业务需求替换掉其中任意一层。这个设计思路和很多现代前端框架的内核 生态模式是一脉相承的。1.2 谁适合用 Univer我在实际调研中总结了几类典型场景你可以对照看看自己是不是目标用户要做在线协作表格的团队比如项目管理工具里想嵌入一个可多人编辑的表格视图或者做类似在线 Excel 的产品。Univer 的协同层和公式引擎能帮你省掉大量底层工作。需要深度定制表格交互的产品比如财务系统里要做一个带特殊校验规则的报表编辑器或者教育产品里要做带批注和公式演示的表格。Univer 的插件架构让你能精确控制每一个交互行为。想学习表格引擎架构的开发者Univer 的源码结构相当清晰是研究一个工业级表格引擎怎么设计的绝佳样本。我自己就是从读它的渲染层和公式解析器里学到了不少东西。反过来说如果你只是想要一个静态展示数据的表格那用普通的 HTML table 或者轻量级表格库就够了上 Univer 属于杀鸡用牛刀反而增加复杂度。1.3 和热词里那些技术的关系输入里给的热词很有意思把 Univer 和 Node.js、Canvas、SDK、插件架构这些词绑在一起其实正好点出了它的技术底座Canvas 绘图Univer 的表格渲染不是用 DOM 堆出来的而是基于 Canvas 绘制的。这一点非常关键后面会专门讲为什么这么选。Node.js服务端协同、公式的某些计算场景、以及构建工具链都依赖 Node.js 生态。SDKUniver 对外暴露的是一套 SDK而不是一个成品应用这是它和普通开源表格项目的本质区别。插件架构整个 Univer 的能力都是通过插件拼装起来的这是它最核心的设计哲学。2. 为什么 Univer 要用 Canvas 而不是 DOM2.1 DOM 渲染表格的天花板在哪先说说大多数人做表格的第一反应用table或者一堆div拼。小数据量下这没问题浏览器对 DOM 的布局和渲染优化已经做得很好。但一旦数据量上去问题就来了。我做过一个测试用 DOM 渲染一个 1000 行 × 50 列的表格滚动的时候帧率直接掉到 20 以下卡顿感非常明显。原因很简单DOM 节点数量爆炸。1000×50 就是 5 万个单元格每个单元格至少一个节点加上边框、样式、事件监听浏览器要维护的节点树极其庞大。而且每次滚动都要重新计算布局reflow和重绘repaint这两步都是性能杀手。有人会说用虚拟滚动virtual scrolling啊只渲染可视区域。这确实能缓解但虚拟滚动本身也有开销——每次滚动都要计算哪些单元格进入/离开视口然后增删 DOM 节点这个过程中节点的创建和销毁同样消耗性能。而且虚拟滚动对复杂的合并单元格、冻结行列、公式高亮这些场景支持起来很别扭。2.2 Canvas 的渲染逻辑Univer 选择 Canvas本质上是把渲染这件事从浏览器手里接管过来自己控制。Canvas 是一块画布你给它指令它就画画完之后浏览器只维护一个节点。5 万个单元格也好50 万个也好对 DOM 树来说都只是一个canvas元素。具体到 Univer 的渲染流程大致是这样的视口计算根据当前滚动位置和画布尺寸算出哪些单元格需要被绘制。这一步和虚拟滚动思路类似但算的是绘制范围而不是DOM 节点范围。分层绘制Univer 把表格拆成多个图层——背景层、网格线层、单元格内容层、选区层、悬浮层等。每个图层独立绘制互不干扰。这样当只有选区变化时只需要重绘选区层不用动其他层。脏矩形重绘不是每次变化都全量重绘而是标记出哪些区域脏了只重绘这些区域。这是 Canvas 渲染性能优化的经典手段。这套机制下来Univer 在几万单元格的数据量下依然能保持流畅滚动这是 DOM 方案很难做到的。2.3 Canvas 方案的代价但 Canvas 不是银弹它带来的代价也很明显这也是我实际接入时踩坑最多的地方第一事件处理要自己实现。DOM 里每个单元格天然有事件Canvas 里没有。你得自己根据鼠标坐标反推它落在哪个单元格上然后手动分发事件。Univer 内部有一套坐标转换和命中检测的逻辑但如果你要加自定义交互就得理解这套机制。第二无障碍访问困难。Canvas 里的内容对屏幕阅读器是不透明的DOM 天然支持的无障碍特性在 Canvas 里要额外做很多工作。Univer 在这方面做了一些补偿比如维护一个隐藏的 DOM 结构用于无障碍但相比纯 DOM 方案还是有差距。第三文本编辑要自己造。在 Canvas 里做输入框、光标、选区高亮全部得自己实现。Univer 的编辑器层就是干这个的复杂度不低。提示如果你的场景对无障碍访问有硬性要求或者表格交互极其复杂比如大量内嵌富文本编辑选 Canvas 方案前一定要评估清楚这些代价能不能接受。3. 插件架构Univer 最值得学的设计3.1 为什么是插件而不是继承很多表格库的扩展方式是继承——你继承一个基类重写某些方法。这种方式在扩展点少的时候还行但表格引擎的扩展点太多了渲染、公式、协同、快捷键、右键菜单、工具栏、数据校验……如果全用继承类层次会深得吓人而且多个扩展之间容易打架。Univer 用的是插件架构核心思路是内核只提供最基础的运行时和事件总线所有具体能力都由插件注册进来。每个插件是一个独立的模块它声明自己依赖哪些能力、提供哪些能力内核负责按依赖关系加载和初始化。这个设计的好处是解耦彻底。比如公式计算是一个插件协同编辑是另一个插件它们之间通过内核的事件总线和依赖注入通信而不是直接互相引用。你想换掉公式引擎只要写一个实现了相同接口的插件替换掉就行其他部分完全不用动。3.2 一个插件长什么样从概念上讲Univer 的插件大致包含这几部分依赖声明这个插件运行需要哪些其他插件先就位。生命周期钩子在加载、启动、销毁等阶段做什么。能力注册向内核注册自己提供的能力比如我能处理公式计算。事件监听订阅自己关心的事件比如单元格内容变化了。我实际读源码时发现Univer 的插件注册机制做得相当克制没有搞得很花哨。它没有用复杂的依赖注入框架而是用了一套轻量的模块注册表。这种够用就好的工程取舍我觉得比堆砌设计模式更值得学习。3.3 插件架构带来的实际好处说个我自己的真实体会。我在做一个定制表格时需要让某些单元格的渲染方式跟默认不一样——比如根据数值大小显示不同的背景色阶。如果用传统方案我得去改渲染层的代码改完之后升级库版本又要重新合并很痛苦。用 Univer 的插件方式我写了一个渲染扩展插件注册了一个自定义的单元格渲染器只处理我关心的那类单元格其他单元格走默认逻辑。整个改动是隔离的升级 Univer 版本时我的插件基本不用动。这就是插件架构的价值——把变化隔离在局部。3.4 插件之间的通信怎么处理插件多了之后通信是个大问题。如果插件 A 直接调用插件 B 的方法那两者就耦合了违背了插件化的初衷。Univer 的做法是通过事件总线和共享状态来通信。举个例子用户编辑了一个单元格这个动作会触发一系列连锁反应——数据模型要更新、公式要重算、依赖这个单元格的其他单元格要重绘、协同层要把变更同步出去。这些逻辑分散在不同的插件里它们之间不直接调用而是通过事件串联起来。这种设计的代价是调试起来稍微麻烦一点因为一个操作的影响链路是隐式的。我排查问题时经常要顺着事件流一路追下去。但换来的是模块之间的彻底解耦长期看是值得的。4. 公式引擎与数据模型的设计取舍4.1 公式引擎为什么难做表格的公式引擎是典型的看着简单做着难。表面上是解析SUM(A1:A10)这样的表达式然后算个结果但实际要考虑的东西非常多依赖追踪A1 变了哪些单元格需要重算这需要维护一张依赖图。循环引用检测如果 A1 的公式引用了 B1B1 又引用了 A1得检测出来并报错不能让程序死循环。增量计算不能每次改动都全量重算所有公式那样大数据量下会卡死必须只重算受影响的单元格。函数库SUM、AVERAGE、IF、VLOOKUP……几百个函数每个都要正确实现还要处理各种边界情况。跨表引用公式里引用另一个 sheet 的单元格依赖关系更复杂。Univer 的公式引擎把这些拆成了几个模块解析器负责把公式字符串变成语法树求值器负责计算依赖管理器负责追踪和调度重算。这个分层是标准做法但实现细节里有很多讲究。4.2 依赖图的维护策略依赖图是公式引擎的核心数据结构。简单说就是一张有向图节点是单元格边是引用关系。当某个单元格的值变化时沿着图找到所有下游节点标记为需要重算。这里有个性能陷阱如果依赖图维护得不好每次改动都要遍历整张图那大数据量下就废了。Univer 用的是增量更新策略——只在公式变化时更新相关的边而不是重建整张图。同时它用了拓扑排序来确定重算顺序保证一个单元格在被重算之前它依赖的所有单元格都已经算好了。我实测下来在几千个带公式的单元格场景下单次编辑的重算延迟基本在毫秒级这个表现是合格的。4.3 数据模型的分层Univer 的数据模型不是简单的一个二维数组而是分了好几层层级职责变化频率单元格数据层存储原始值和公式编辑时变化计算值层存储公式的计算结果依赖变化时更新样式层存储格式、颜色、边框等格式化时变化视图层存储行高列宽、冻结、隐藏等视图操作时变化这样分层的好处是不同类型的变更只影响对应的层。比如你只是改了个单元格颜色不需要触发公式重算只是调整了列宽不需要动数据层。这种按变更类型隔离影响范围的思路是高性能表格引擎的通用做法。4.4 公式引擎的扩展性Univer 的公式引擎支持自定义函数注册。你可以写一个插件注册自己的函数然后在公式里就能用了。这对企业场景特别有用——比如你们公司有一套特殊的财务计算规则可以封装成自定义函数让业务人员在表格里直接用。我试过注册一个自定义函数流程大概是实现函数的求值逻辑声明参数类型和返回值类型注册到公式引擎。整个过程不算复杂但要注意函数的纯度和副作用——公式函数应该是纯函数同样的输入永远给同样的输出否则依赖追踪会出问题。5. 协同编辑的冲突处理机制5.1 协同编辑的核心难题多人同时编辑一个表格最难的不是同步而是冲突。两个人同时改了同一个单元格怎么办一个人删了一行另一个人正在这行里输入怎么办这些冲突如果处理不好轻则数据错乱重则整个表格状态崩掉。业界处理协同冲突主要有两大流派OTOperational Transformation操作转换和CRDTConflict-free Replicated Data Type无冲突复制数据类型。两者各有优劣Univer 在设计上对这两种思路都有考量。5.2 OT 和 CRDT 的取舍OT 的思路是把用户的每次编辑抽象成一个操作当多个操作并发时通过转换函数调整操作的执行顺序和内容让最终结果一致。它的优点是传输数据量小适合文本和表格这种结构化数据。缺点是转换函数写起来极其复杂尤其是操作类型多了之后转换规则会爆炸。CRDT 的思路是设计一种特殊的数据结构让并发操作天然可交换、可合并不需要复杂的转换逻辑。优点是冲突处理简单天然支持离线编辑。缺点是数据结构本身有额外开销传输和存储的数据量更大。Univer 在协同层做了抽象理论上两种方案都能接。但从它的默认实现和文档倾向看它更偏向于用 OT 思路处理表格操作因为表格的操作类型相对有限改单元格、插删行列、改样式等OT 的转换规则可控。5.3 协同层的实际接入如果你要接入 Univer 的协同能力需要自己实现服务端。Univer 提供的是客户端的协同逻辑和协议服务端要你自己搭。这一点很多人第一次接触时会误解以为 Univer 自带协同服务。服务端要做的事情大致包括连接管理维护所有客户端的连接处理加入/离开房间。操作广播收到一个客户端的操作后转换并广播给其他客户端。状态持久化定期把表格状态存下来防止服务重启后数据丢失。冲突仲裁在必要时比如操作顺序有歧义做仲裁。我用 Node.js 搭过一个简易的协同服务端核心逻辑不复杂但要做得健壮断线重连、操作去重、版本对齐需要不少额外工作。如果你的团队没有协同后端的经验这块要预留足够的时间。5.4 协同场景下的常见坑我在测试协同功能时踩过几个坑分享出来让大家少走弯路坑一操作顺序不一致导致状态分叉。如果服务端广播操作的顺序在不同客户端看来不一致就会导致各客户端状态分叉。解决办法是给操作编号客户端严格按编号顺序应用。坑二本地乐观更新与远端操作的冲突。为了体验流畅本地编辑通常先乐观更新立即显示然后再等服务端确认。但如果这时收到远端操作本地乐观更新可能和远端操作冲突。处理方式是维护一个待确认操作队列远端操作应用时要先转换掉本地未确认的操作。坑三光标和选区不同步。协同编辑时你不仅想看到别人的内容还想看到别人的光标位置。这需要额外同步光标状态而且光标位置在行列增删后要重新映射。6. 实际接入 Univer 的完整流程与避坑6.1 环境准备与依赖安装Univer 是基于 TypeScript 的前端库服务端协同部分用 Node.js。环境准备这块Node.js 版本建议用 18 LTS 或更高太老的版本可能在构建工具链上出问题。安装核心包大致是这样# 安装 Univer 核心包 npm install univerjs/core univerjs/sheets univerjs/sheets-ui # 如果需要公式能力 npm install univerjs/sheets-formula # 如果需要协同能力 npm install univerjs/sheets-collaboration这里有个容易忽略的点Univer 的包是按功能拆分的核心包只提供最基础的能力你要什么功能就装什么包。新手经常只装了 core 然后发现表格显示不出来其实是没装渲染相关的包。6.2 最小可运行示例搭一个最小的表格实例核心步骤是创建 Univer 实例、注册需要的插件、挂载到 DOM。import { Univer } from univerjs/core; import { UniverSheetsPlugin } from univerjs/sheets; import { UniverSheetsUIPlugin } from univerjs/sheets-ui; // 创建实例 const univer new Univer(); // 注册插件——顺序很重要UI 插件依赖核心插件 univer.registerPlugin(UniverSheetsPlugin); univer.registerPlugin(UniverSheetsUIPlugin); // 创建表格并挂载 const workbook univer.createUniverSheet({}); // 挂载到容器元素 workbook.mount(document.getElementById(app));这段代码看起来简单但有几个细节值得说插件注册顺序有依赖关系的插件必须先注册被依赖的。如果顺序错了运行时会报错说找不到某个能力。挂载容器容器元素必须有明确的宽高否则 Canvas 尺寸算不出来表格会显示成一条线或者空白。销毁组件卸载时记得调用univer.dispose()否则事件监听和 Canvas 资源不会释放长时间运行会内存泄漏。6.3 自定义插件的编写要点写一个自定义插件骨架大概是这样import { Plugin } from univerjs/core; class MyCustomPlugin extends Plugin { static pluginName MyCustomPlugin; // 声明依赖 static getDependencies() { return [UniverSheetsPlugin]; } // 插件启动时调用 onStarting() { // 注册能力、监听事件 } // 插件销毁时调用 onDisposing() { // 清理资源 } }写插件时有几个经验不要在构造函数里做重活构造函数应该只做初始化真正的逻辑放在onStarting里因为那时依赖的插件才保证已经就绪。事件监听记得解绑在onDisposing里把注册的事件监听都清掉否则插件销毁后监听还在会引发难以排查的 bug。能力注册要幂等如果插件可能被重复加载注册能力时要检查是否已存在避免重复注册。6.4 性能调优的实操经验Univer 默认配置下性能已经不错但在大数据量场景下还是需要调优。我总结了几条第一控制初始加载的数据量。不要一次性把几万行数据全塞进去用分页或者懒加载。Univer 支持动态追加数据滚动到哪加载到哪。第二合理使用冻结和隐藏。冻结行列和隐藏行列能减少需要渲染的单元格数量对性能有正面帮助。第三公式要慎用易失性函数。像NOW()、RAND()这种每次计算都返回不同值的函数会导致依赖它们的单元格频繁重算。如果表格里有大量这类公式性能会明显下降。第四注意 Canvas 的分辨率。在高 DPI 屏幕上如果 Canvas 没做分辨率适配会显得模糊如果适配了但没控制好又会因为像素太多而拖慢渲染。Univer 内部有处理但自定义渲染时要注意。6.5 常见报错与排查思路报错现象可能原因排查方向表格空白不显示容器无宽高 / 插件未注册检查容器 CSS检查插件注册顺序公式不计算公式插件未加载确认安装了 formula 包并注册滚动卡顿数据量过大 / 未做虚拟化检查是否一次性加载了过多数据协同不同步服务端广播顺序问题检查操作编号和版本对齐逻辑内存持续增长未销毁实例 / 监听未解绑检查 dispose 调用和插件清理逻辑排查 Univer 问题时我习惯先打开控制台看有没有插件加载相关的警告然后检查数据模型的状态是否正确最后才怀疑渲染层。因为大部分问题其实出在插件配置和数据层渲染层反而比较稳定。7. 我对 Univer 架构设计的一些个人看法用了这么久我对 Univer 的整体评价是架构方向正确工程完成度在开源项目里属于上乘但离开箱即用的商业级方案还有距离。它的插件架构和 Canvas 渲染方案确实是解决表格引擎复杂度的正确路径。尤其是插件化这一点让它的可扩展性远超那些一体化的表格库。我在做定制需求时几乎没遇到过改不动的情况这在其他表格方案里是很难得的。但它的短板也很明显。协同服务端要自己实现这对没有后端协同经验的团队是个不小的门槛文档虽然不算少但很多细节要靠读源码才能搞清楚生态还在建设中一些高级功能比如复杂的图表联动、数据透视要么没有要么需要自己开发插件。我的建议是如果你有较强的前端工程能力且需求确实需要深度定制Univer 是非常值得投入的选择。但如果你只是想快速搭一个标准在线表格且团队没有太多底层开发资源那可能用更成熟的商业方案更划算。技术选型从来不是选最好的而是选最合适的。最后分享一个我踩过的坑Univer 的版本迭代比较快不同版本之间的 API 有 breaking change。我在升级一个小版本时发现某个插件的注册方式变了导致整个表格起不来。所以如果你打算长期用一定要锁死版本号升级前先在测试环境验证别直接在生产环境升。这个教训是用一次线上故障换来的希望你能避开。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →