☰
Univer开源在线表格引擎:从Canvas渲染到多人协同的深度解析
2026/11/5 4:56:26 网站建设 项目流程

做企业级前端快八年了,表格是我最怕碰的组件之一。需求方一会儿要冻结列,一会儿要下拉校验,一会儿又要批量拖拽。看起来只是画个网格,背地里全是单元格状态、公式依赖、样式栈,任何一个细节都能让人加班到半夜。直到去年在一个协同报表项目里遇到Univer,我才感觉这个方向总算有了一个能接住复杂业务的库。Univer是一个开源的在线表格/文档/幻灯片办公套件引擎,它最擅长的是电子表格部分,核心理念是把Excel那套能力搬进Web,并且从底层就预留了多人实时编辑的能力。如果你正在评估在线表格组件,或者正被自家表格产品的性能、协同和扩展性问题折磨,这篇内容值得从头看到尾。

1. 表格需求从来不是“画个网格”:为什么大家开始盯上Univer

1.1 一张Excel背后其实藏着六层复杂度

很多人第一次接触表格需求时,直觉是“不就是HTML table加几个输入框吗”。真正动手做才发现,一个能用的在线表格,至少要同时解决六层问题:

  • 数据模型层:单元格不只是一个值,它可能存数字、字符串、日期、布尔值,还分“显示值”和“原始值”,公式单元格则要区分“存储的公式”和“计算后的结果”。
  • 样式层:单元格有字体、边框、填充、对齐、数字格式,样式可能应用到单独一格,也可能通过“区域样式”套到整列,合并单元格时样式还要重新计算。
  • 交互层:拖拽填充、选区移动、右键菜单、快捷键、滚动加载、单元格编辑器定位,每一个交互都直接依赖坐标计算。
  • 公式层:公式引用要能随行列插入删除自动调整引用范围,还要求支持跨sheet引用和异步数据获取。
  • 导入导出层:xlsx的文件格式本身是一大堆XML和二进制结构,解析和生成都不轻松。
  • 协同层:多人同时编辑时,两个人的操作顺序不同会导致最终结果不一样,必须有变换算法保证最终一致。

这些复杂度堆在一起,远远超出普通前端组件能承载的范围。所以以前团队做表格功能,要么绑死Excel插件,要么通过iframe嵌一个商业组件,要么硬着头皮自己写还写不完整。

1.2 Luckysheet到Univer:开源项目的被迫进化和平台化布局

Univer这个项目真正引起我注意,是因为它的历史。它脱胎于国内开源社区比较知名的Luckysheet——一个当年在Github上星标很高的在线表格项目。后来原来那个团队在重构中意识到,光做一个表格组件走不远,因为文档、幻灯片、表格本身就是一套办公产品的三件套,于是把核心代码整体重构,变成了现在更底层的Univer。

所以Univer不是Luckysheet的小版本升级,它是一个彻底重新设计的框架。核心代码使用TypeScript编写,前端通过依赖注入管理模块依赖,所有能力都以插件形式挂载。这意味着什么?意味着你拿到的不只是一个表格控件,而是一个可以二次开发的办公套件底座。Univer的仓库里同时规划了Spreadsheet、Docs、Slides三类产品的支持,只是目前电子表格能力最成熟。

1.3 Univer到底能做什么:核心能力地图

我整理了实际项目里比较常用的一套能力清单,方便你快速判断它能覆盖多少需求:

能力分类具体功能说明
数据编辑单元格编辑、多选区操作、拖拽填充、复制粘贴、查找替换、富文本单元格日常表格操作基本齐全
样式系统字体、边框、填充、条件格式、数字格式、冻结行列、隐藏行列多数样式可序列化保存
公式引擎内置常用函数、跨表引用、自动重算、自定义公式、异步公式兼容常见Excel函数写法
数据处理排序、筛选、数据校验、行列插入删除、合并单元格筛选和大数据量配合稍弱
文件交互CSV/XLSX导入导出、JSON快照导出依赖插件,复杂文件有损耗
协同能力基于OT的多人实时协同、操作历史、存在性状态同步官方提供协同服务端示例
扩展生态自定义插件、自定义单元格渲染、自定义编辑器、事件监听这是Univer最强的地方
部署形态纯前端SDK、无厂商锁定、可集成到任意框架文档、Vue、小程序webview都能接

2. 第一次跑通Univer:最小集成里藏着的架构意图

2.1 两天理解API不如跑一个Demo:初始化和加载数据的完整代码

Univer的官方文档其实做得不算差,但第一次看时还是容易被一堆包名绕晕。按照我的经验,别想着先看懂架构,先跑通一个最小Demo,再回头读架构会快很多。

我当时的生产实践是用npm方式安装到React项目里。核心代码大概是这样的思路:

import { createUniver } from '@univerjs/presets'; import { defaultSheetPreset } from '@univerjs/presets'; // 实例化表格预设,这里会自动装载基础插件 const univer = createUniver({ locale: 'zhCN', presets: [ defaultSheetPreset(), ], }); // 创建一个工作簿实例 const sheet = univer.createUniverSheet(); // 把数据快照加载进去 sheet.loadData({ sheetId: 'sheet1', cellData: { 0: { 0: { v: '姓名' }, 1: { v: '分数' }, }, 1: { 0: { v: '张三' }, 1: { v: 99 }, }, }, });

以上面的结构为例,cellData的第一层key是行号,第二层key是列号,单元格对象的v字段表示原始值。实际运行后,一个具备网格线、单元格编辑器、右键菜单、拖拽选区的表格就出现了。

不同版本之间的API命名会变,比如有的版本用initSheet,有的版本用createUniverSheet,这些不是你的问题,而是项目迭代太快。建议一律以你安装版本的d.ts类型定义为准,不要照抄博客。

2.2 为什么数据是JSON而不是二维数组:Univer的数据快照协议

如果你以前用过x-data-spreadsheet这类库,会发现它们的数据结构通常是个二维数组。Univer不一样,它用的是类似“文档快照”的结构,也就是一棵可序列化的大树。

这里的关键原因是协同。协同最怕的是两个人基于不同版本操作,Univer的数据模型设计从一开始就为OT算法留了操作对象的位置。节点上有很多字段,不只是v,还包括t(单元格类型)、m(富文本段落)、s(样式id)、p(公式字符串)等等。这种设计让每个单元格都是一个自描述对象,改公式、改样式、改数据都能对应到具体字段,而不是“整行替换”。

另一个直接好处是局部更新友好。后端保存数据时不需要全量传,只需要把变化的单元格编号和值传走即可,前端收到后增量合并。我们当时做增量保存,一天几万个单元格变化,接口负载还算轻松。

2.3 从“控制台能跑”到“项目里可用”:承载时的DOM、路由、构建注意事项

Demo能跑之后,接入真实项目还有几个细节容易被忽略。

第一,Univer的容器本质上是接管一块DOM区域,内部用Canvas绘制表格内容。如果承载区域的父容器宽高是0px或动态变化,表格会出现渲染空白。建议外层容器写死最小尺寸,比如height: calc(100vh - 120px),再监听窗口resize事件,调用刷新接口。

第二,构建时注意体积。Univer全家桶打包下来比较大,尽量用官方提供的预设包,而不是一个个手动引模块。按需加载的粒度在插件级别,内部每个插件的依赖会被Tree-shaking处理,但首屏依然建议配合动态import做懒加载。

第三,如果项目里同时有Vue和React,不要慌。Univer是纯前端SDK,和框架无关,它在组件生命周期的mount阶段挂载,在unmount阶段销毁即可。唯一要注意的是不要在表格内部维护一个“全局状态”,把业务状态放到框架侧,通过事件订阅桥接到表格。

3. 公式、格式、校验:看起来是Excel的那部分,其实最难

3.1 公式引擎不是eval:Univer的公式解析与异步计算

公式是表格产品最容易被低估的部分。很多人以为公式就是对表达式做eval,实际上一个能用的公式引擎要处理引用、优先级、错误传播、循环引用、重算调度、跨sheet查询、自定义函数注册。Univer的公式引擎做到了“类似Excel”的体验,公式字符串在解析后会生成依赖树,某个单元格数据变化后,只有依赖它的单元格会进入重算队列,而不是全表重算。

我在实际项目里遇到过联级下拉的场景:第一列选择部门,第二列的下拉选项根据第一列变化。我第一版用前端事件监听去改校验规则,结果发现数据校验对象要重新设置,且必须保证和原对象deep diff一致,很麻烦。后来改用习惯的“辅助列+自定义公式”的方式,让第二列校验直接引用辅助列的公式结果,逻辑简单了很多。这里想说的是,不要把所有交互都压在业务代码里,表格引擎自己的公式依赖机制往往能帮忙解决状态联动。

异步公式也很实用。Univer支持注册异步函数,比如根据商品编号请求后端返回实时库存。需要注意异步公式的结果不会同步写入单元格原始值,而是通过回调更新,所以做持久化时要把“计算值”和“缓存值”分开处理,否则保存到数据库后再次打开,异步结果可能变成#VALUE!。

3.2 条件格式、数据验证、筛选排序:开箱即用的完整清单

条件格式是我在需求清单里见到频率最高的功能之一。Univer做了比较完整的支持,可以设置色阶、数据条、图标集,也可以基于公式做高亮规则。它把条件格式也是当作一种样式插件实现的,监听数据变化后重新计算命中区域。

数据验证方面,下拉列表、数字范围、日期限制、自定义公式校验都能做。前端校验只是第一道防线,真正写在保存接口里还需二次校验,因为这个库的校验偏向“交互提示”,不是强约束。

筛选排序则要注意:如果数据量特别大,比如两三万行以上,筛选期间全表扫描会让Canvas渲染交互掉帧。我的建议是,如果业务数据量真的到了这个量级,先和后端约定好“服务端筛选”,只把筛选结果集灌给Univer,别让一个纯前端表格扛几十万行。

3.3 给我一个真实业务场景:做一张带联动计算的人事评分表

我拿一个相对简单的案例来讲怎么把这些能力组合起来。假设要给员工做季度评分,表格里有三列:基础分、绩效系数、最终得分,最终得分需要由前两列相乘得出。

在Univer里可以先把最终得分列的单元格写入p字段:

{ 1: { 0: { v: '张三' }, 1: { v: 80 }, 2: { v: 1.2 }, 3: { v: null, p: { f: '=B2*C2', id: 'formula_1' } }, }, }

保存并刷新后,公式引擎会自动算出240。此时如果通过界面上把张三的基础分改成90,最终得分列无需人工干预自动更新为108。这种机制省掉了很多业务代码中的“依赖后置计算”。

单元格校验则用来控制绩效系数只能填0.8到1.5,数据验证加一个区间规则即可。条件格式再做一层:最终分低于80的格子标红,高于120的标绿。整体下来,这个页面用的基本全是Univer自带能力,没有写一行Canvas渲染逻辑。

4. 协同编辑是Univer最值钱的地方:OT协议和WebSocket的接入体验

4.1 在线表格的协同为什么难做

如果只是单机编辑,Univer能帮你解决80%的表格体验问题。但“univer在线”这个关键词背后,大家真正想要的是多人同时改同一张表。这个需求一旦开启,复杂度立刻翻倍。

难点不在WebSocket长连接,而在冲突处理。两个人同时修改同一个单元格,如果不做处理,后保存的人会覆盖先保存的人。文档类协同可以用“整体文档替换”的思路,但表格不行,表格的粒度是单元格和选区,操作单位频繁且细小。因此Univer选择了OT算法(操作变换)。它的思路是:每个操作都有一个基于文档版本的上下文,服务端收到并发操作后,不是直接应用,而是先做变换,再按顺序落库。每个人收到的最终结果,操作顺序和服务端一致,所以界面保持一致。

Univer把这种数据结构定义得比较干净:有op队列,单元格crud都是有类型的操作。协同的核心只是把这些op通过WebSocket广播出去。

4.2 官方协同流程长什么样:接入协同服务需要准备什么

Univer官方仓库里有一个协同服务端示例,基本链路是:前端创建文档实例后,通过WebSocket连接协同服务器,服务器维护文档版本号和操作队列,收到操作后做OT变换、持久化、广播。

我建议接入时按下面这个顺序推进:

  1. 先跑通本地协同:自己搭一个WebSocket服务,前端两个标签页打开同一份文档,验证是否能做到互相看到对方光标和操作。
  2. 再做鉴权链路:页面加载时通过业务接口拿token,建立WebSocket时带上token,服务端校验后决定是否允许订阅文档。
  3. 最后做持久化:把服务端接收到的op落库,落库策略建议“多op合批”,不要每次操作都写数据库,否则高并发时数据库压力很大。

官方示例偏向演示性质,生产环境要自己补充文档权限、房间管理、断线重连后的全量同步机制。这些不属于Univer的API范围,而是任何协同系统都要做的公共设施。

4.3 协同过程中容易踩的坑:保存频率、快照、版本管理

第一次接协同时,我们犯过一个低级错误:前端每收到一个op就向后端发送一次全量保存。结果协同开着十分钟,数据库就多了几万条记录,接口直接超时。后来改成“操作日志增量保存+定期全量快照”,快照每5分钟一次,增量日志实时落,恢复时先找最近快照再重放日志,乱序问题也就通过OT解决了。

另一个值得小心的地方是协同和undo/redo的冲突。本地undo只影响自己的操作栈,服务器上已经广播出去的操作不会自动撤销。Univer的做法是撤销操作本身也是一种op,协同下要主动把“撤销”作为新操作广播出去。这个逻辑如果不弄清楚,会出现“你撤回了,别人的界面里还保留着你那步”的情况。

最后建议协同模式下的样式操作简化。表格样式修改往往会把一大片区域当作操作对象,协同广播的op包会变得很大。最好限制用户不能一键修改全表字号,要么后端压缩op,要么前端限制格式刷范围,否则网络差时体验会很顿。

5. Canvas渲染 + 插件体系:Univer能走多深,取决于你懂多少

5.1 为什么用Canvas重绘而不是DOM表格

在线表格早期的实现方式是用DOM表格,比如一些老牌库render几万个tr/td。副作用很直接:修改一个单元格的背景色,需要操作对应的DOM节点,浏览器重排重绘的成本极高。表格动辄几万格,滚起来就像在看PPT动画。

Univer选择了Canvas渲染方案。表格主体区域的每个可视单元格,不是DOM元素,而是画布上的图形。用户看到的所有网格线、文字、选区、边框都是绘制出来的。滚动时只重绘可视窗口内的内容,配合行列虚拟化,即便单sheet有几十万数据,滚动依然能保持流畅。

但这不代表你完全不能操作DOM。Univer在Canvas上层还叠加了一层HTML DOM用于放置输入编辑器、下拉框、右键菜单这类强交互元素,因为它意识到Canvas不擅长处理文本光标和IME输入法。这个“Canvas主体+DOM辅助”的双层结构,是同类产品的主流方案。

5.2 渲染三层模型:Scene/Layer/Object与自定义单元格

要扩展Univer的渲染能力,得理解它的三个层级:

  • Scene(场景):整个表格画布是一个Scene,场景里可以叠加多个Layer。
  • Layer(层):每个Layer承载一类绘制内容,比如网格线层、单元格内容层、选区层、边框绘制层。
  • Object(对象):Layer里的具体图形对象,比如一个单元格矩形、一段文本、一个选区高亮框。

自定义单元格扩展的核心,就是在单元格内容层里插入自定义绘制逻辑。做法类似写一个CustomComponent,传入的单元格上下文里有坐标、缩放比、数据,你可以自己调用Canvas绘制任意图形。

我们曾经把表格里的“状态列”做成一个徽标:绿色圆点代表正常,红色圆点代表异常,旁边再加上一行说明文字。如果用纯文本模拟,会很丑;用自定义渲染写一次,之后所有sheet页都能复用。这就是Univer相对普通表格组件拉开差距的地方。

5.3 写一个自定义业务单元格:从基类到渲染的具体思路

自定义单元格的第一步不是说画就画,要先继承单元格编辑器的逻辑,否则鼠标双击进入编辑时,输入框还是默认的文本编辑。我在写评分表里的“星级评分”单元格时,大致思路是:

  • 渲染层面:根据数字大小绘制1到5颗星的Canvas图形。
  • 编辑层面:继承默认编辑器,类型是数字校验,输入0到5之间的值。
  • 数据层面:底层仍然用数字存储,这样其他单元格可以拿这个值去计算平均分。

自定义单元格的渲染函数只在可视线程区调用,所以性能可以接受,但不要在渲染函数里做复杂算法,否则滚轮时每帧都执行会影响帧率。建议只做坐标和图形的映射,真实业务数据提前算好缓存。

5.4 插件生命周期:哪些Hook值得监听,哪些不要滥用

Univer的插件系统暴露了很多生命周期钩子。我用得最多的几个:

  • 文档加载完成后触发的事件,用于初始化自定义菜单。
  • 单元格内容变更事件,用于业务侧自动记录日志。
  • 公式重算完成事件,用于更新外部统计面板。
  • 选中区变化事件,用于底部状态栏显示行列信息。

插件不是万能的,事件监听也不是越多越好。表格内部每动一次单元格就会抛事件,如果你在事件回调里再去触发同步更新、弹窗、接口请求,很容易形成死循环。建议在监听回调里先做一次“值对比”,只有业务关心的字段变化才继续处理。

6. 数据导入导出与集成的“最后一公里”

6.1 CSV/XLSX导入导出:官方插件的能力边界

Univer对CSV和XLSX的支持是通过插件提供的。导入时把文件解析成Univer自己的JSON快照,导出时再逆向转回标准格式。这个过程的兼容性取决于SheetJS和开源解析库的覆盖度。

日常使用下,数字格式、公式、样式、合并单元格、边框这些都能带过去。但如果你手头有一个带复杂图表、图片批注、数据透视表、宏的文件,导入后大概率会有不同程度的丢失。指望把Excel文件无损搬进开源引擎,目前还不现实。建议在产品设计时就明确边界:平台拿到的是“类Excel”的表格,不是Excel本体。

6.2 常见坑:合并单元格、数字格式、图片批注丢细节

合并单元格是最容易出问题的点。Univer内部支持合并,导入xlsx的合并规则一般也能还原,但如果你在Univer里做了合并,再导出xlsx,验收时务必检查跨列合并、跨行合并、跨区域大合并这三种场景。

数字格式也要小心。比如保留两位小数,Excel存储的是“数字值+显示格式”,Univer同样的存储方式。问题出在一些本地化格式代码,比如中文日期格式、货币符号,不同版本之间解析可能有偏差。最稳妥的做法是统一用标准的格式串。

图片批注和浮动图片,Univer目前支持有限。如果业务强依赖批注图片,比如财务审核截图,建议提前告知用户“批注只做展示不做编辑”,或者按附件方式外挂到业务系统,而不是塞进单元格。

6.3 与后端数据源打通:增量更新不是全量覆盖

把Univer嵌进业务系统,迟早要面对“数据库里的数据如何同步给表格”。最直观的做法是后端返回一个二维数组,前端转换成cellData结构,全量替换。这种方式简单,但数据量大时浪费带宽和渲染资源。

优化的思路是增量更新:后端只传变化单元格的行列坐标和值,前端用setCellData之类的接口定点写入。我们后来还做了字段映射配置,后端下发的id、name、score字段能自动映射到对应列,前端不需要关心业务字段和列的对应关系,后续加列也只改配置。这个机制让“业务系统数据库实时生成在线表格”变成了很顺手的能力。

7. 从能力角度看选型:Univer适合你的项目吗

7.1 一张表对比开源电子表格组件

维度UniverHandsontablex-data-spreadsheetLuckysheet
开源协议宽松非完全开源MITMIT
协同能力内置OT,官方有服务端示例商业版才有协同无社区版有服务端方案但久未维护
自定义渲染强,Canvas+插件体系中等,依赖DOM单元格弱中等
公式引擎较完善商业版公式才完整微弱常用函数可跑
导出XLSX有插件有有有
文档和社区文档齐全,迭代快成熟,商业支持好简单,停更趋于停更
学习成本偏高,概念多中低中

大概可以这样判断:如果只是给一个后台管理系统提供轻量表格编辑,x-data-spreadsheet就够了,别再往复杂里拉。如果要做一个真正的在线表格产品,Univer是开源阵营里综合完成度最高的选择。

7.2 什么时候不该用Univer

Univer不是万能的。以下几种场景建议绕开:

  • 需求只是展示表格,不需要编辑,那用Ant Design Table这类数据表格组件更轻。
  • 需求要求强Excel像素级还原,包括复杂图表、宏、VBA脚本,目前开源方案都做不到,想想别的路子。
  • 团队完全没有前端工程化基础,只想拖拽一个组件,Univer的配置量会让人头皮发麻。
  • 有强离线编辑需求,Univer虽然可以把数据快照本地化,但协同层面的离线编辑支持还不算正统解法。

7.3 我个人的经验建议

给技术选型的人一句话:把Univer当成一个“可编程的Excel前端”,而不是“Excel替代品”。它能给你的完整度和扩展性远超普通开源表格,但所有高级能力都需要配合额外工程投入。接入前先想清楚:你要的到底是“表格工具”,还是“表格平台底层”。想清楚这一点,后面每一步都会好办很多。

8. 最后聊聊“univer在线”:大家都在搜索的东西其实是什么

我在搜索热词里看到“univer在线”出现频率挺高。很多人找这个关键词,期待的是一个类似腾讯文档、飞书表格那样的在线协作网站,可以直接打开网址开始填表。但Univer目前的定位不是一个SaaS产品,而是一个开源引擎。你拿到的是一个SDK,要把它变成在线表格服务,自己需要负责部署前端、搭建后端接口、实现账号体系、配置协同服务、做数据存储。

这其实是好事。正因为它是引擎而不是封闭产品,你才能把表格嵌进自己的业务后台、数据中台,甚至嵌入到客户的系统里。我们当时做内部的“运营数据工作台”,核心就是Univer渲染一份来自多数据源的汇总表,用户改完还能回写审批流。这种自由度,商业在线表格服务给不了。

如果你搜“univer在线”是为了找现成可用的在线办公服务,那可能要失望了;但如果你想做一款属于自己的在线表格产品,Univer恰好是你能拿到的开源选项里最值得投入研究的一个。有一次我们遇到Excel打开都卡顿的十万行设备台账,用Univer渲染后浏览器里滚动依然顺畅,那一刻我觉得,这条路走对了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询