去年年底我们在重构一个内部运营中台,当时最让我头疼的模块就是报表区。业务方提的需求听起来很简单:“就在页面上给我一个像 Excel 一样的表格,能在线编辑,能多人同时改,别让我们再去下载附件来回传。” 一开始我想自研一个在线表格组件,断断续续做了两个月,公式、合并单元格、条件格式、撤销栈……越做越觉得这是个无底洞。后来在一次技术分享里看到 Univer 这个开源项目,试用一周后直接决定把自研方案全部扔掉。这篇文章就从实际接入者的角度,聊聊 Univer 能做什么、怎么集成、协同怎么落地,以及官方文档里没详细写的那些坑。
如果你现在也在做低代码平台、B 端后台、知识库应用,或者只是想找个可靠的开源表格引擎,这篇文章应该对你有用。
1. 为什么是 Univer:在线表格选型背后的真实权衡
1.1 项目需求:不是要一个组件,而是要一个“能嵌进业务的 Excel”
很多团队一开始的误区是去选一个“好看的表格组件”,比如数据表格类库,或者带有编辑功能的网格控件。但运营中台的需求其实和这些组件完全不一样:表格不是只用来展示的,它本身就是一个应用入口。业务人员会在里面填数、改数、计算,甚至要把一份表作为整个业务流程的载体。这意味着你需要的是一个完整的电子表格引擎,而不是一个增强版的 table 组件。
我当时的核心需求大概有这几条:
- 单元格级别的编辑能力,支持公式、合并单元格、条件格式、数据校验
- 能嵌入到现成的 React 后台项目里,而不是独立部署一套服务
- 支持多人协同编辑,至少要能看到实时光标和内容同步
- 允许深度定制:自定义右键菜单、自定义单元格、自定义工具栏按钮
- MIT 或类宽松的开源许可,能进行二次开发和商用
逐条对比下来,能同时满足这些的成熟项目并不多。
1.2 我把市面上的开源方案挨个试了一遍
说实话,在锁定 Univer 之前,我先花了两周时间把几个主流选手试了个遍。这里不写表格不下结论,只讲我个人大量实际测试后的主观感受,仅供参考。
| 项目 | 维护状态 | 协同编辑 | 嵌入模式 | 定制难度 | 我的体验 |
|---|---|---|---|---|---|
| Luckysheet | 基本停更 | 需要自研或依赖旧方案 | 前端组件 | 中等 | 功能全,但代码架构偏旧,2023年后没什么有效更新 |
| OnlyOffice | 活跃 | 自带协同 | 需要独立部署文档服务器 | 高 | 适合整套系统,不适合轻量嵌入 |
| Grist | 活跃 | 自带协同 | 偏数据管理应用 | 中高 | 更像 Airtable,不是 Excel 语义 |
| Univer | 活跃 | 正式支持 Yjs 协同 | 纯前端 SDK | 中等 | 模块化清晰,TypeScript 友好,扩展性最好 |
这里要特别说一下 Luckysheet。它功能确实丰富,Excel 里常见操作基本都有,社区里也有很多基于它的老项目。但它的代码结构属于早期前端工程模式,协同方案还停留在 demo 阶段;另外这几年主要开发者已经不再高频维护,很多 issue 挂着没人处理。对于新项目来说,从长期风险角度我不太愿意押注。
OnlyOffice 相反,它太重了。虽然协同能力成熟,但它是整套办公套件,不是可嵌入的“表格组件”,要在你的业务系统里塞进一个完整 Office 环境,无论资源消耗还是部署复杂度都很高。如果你做的是网盘配套的预览编辑场景,OnlyOffice 合适;但如果你只想在某个页面的某个区域提供一个表格能力,它就是大炮打蚊子。
Univer 最打动我的地方是它的定位:它不是一个写死的组件,而是一个文档引擎。它包含 Sheets、Docs、Slides 三条产品线,你可以像搭积木一样只加载需要的模块。底层渲染走的是 Canvas 而非大量 DOM 节点,大数据量下的性能表现也比传统 DOM 表格好很多。
1.3 Univer 不是“又一个表格库”,它重新定义了可嵌入文档引擎
Univer 的架构核心是一个渲染引擎,通过插件体系把公式计算、协同编辑、用户交互、UI 组件、格式解析等能力解耦。每个能力都是一个插件,你可以按需注册。这种设计带来的直接好处是:我只需要表格能力,就不必把文档、幻灯片的代码打包进来;我希望支持多人在线协同,就单独加一个协同插件;将来如果产品需要文档模块,同样的核心上扩展即可。
对比传统表格组件,Univer 更像是一个“微型办公平台”。它的数据模型、视图模型和控制器是分开的,你可以把数据通过 JSON 喂给它,也可以在界面上操作然后监听变更事件拿回来。这种设计对于需要把表格嵌入复杂业务系统的人来说,方向完全正确。
2. 接入 Univer 的三种路径:从 CDN 到前后端分离
2.1 最快速的验证方案:CDN 引入 UMD 包
如果你想在五分钟内看到效果,直接用 CDN 引入 Univer 的 preset 包。这个过程不需要任何打包工具,写一个 HTML 文件就能跑起来。
<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>Univer Demo</title> <link rel="stylesheet" href="https://unpkg.com/@univerjs/presets/styles.css" /> </head> <body> <div id="container" style="width: 100%; height: 600px;"></div> <script src="https://unpkg.com/@univerjs/presets/univer.umd.js"></script> <script> const univer = new Univer.Presets.Univer(); univer.createUnit(Univer.Presets.UniverInstanceType.UNIVER_SHEET, { name: 'Demo', sheets: [ { name: 'Sheet1', cellData: { 0: { 0: { v: 'Hello' }, 1: { v: 'Univer' } }, }, }, ], }); </script> </body> </html>注意,不同版本的 UMD 包暴露的全局命名空间略有差异。上面这个示例是基于较新的 preset 模式,如果你用的是 v0.1 系列,可能需要改为new Univer({})的写法。我的建议是:CDN 只用来做本地原型验证,真正接入项目还是用包管理器。
2.2 React 项目中常用的 npm 集成方式
在 Vite 或 Webpack 工程里,推荐使用官方提供的 preset 来减少配置成本。先安装依赖:
npm install @univerjs/presets @univerjs/presets-sheets然后在你需要的组件里初始化:
import { Univer, UniverInstanceType } from '@univerjs/presets'; import { createSheetsUIPlugin } from '@univerjs/presets-sheets'; const univer = new Univer({ plugins: [createSheetsUIPlugin()], }); univer.createUnit(UniverInstanceType.UNIVER_SHEET, { name: '销售日报', sheets: [ { name: 'Sheet1', rowCount: 200, columnCount: 20, cellData: { 0: { 0: { v: '日期' }, 1: { v: '销售额' }, 2: { v: '毛利' } }, 1: { 0: { v: '2024-01-01' }, 1: { v: 12580 }, 2: { v: 3890 } }, 2: { 0: { v: '2024-01-02' }, 1: { v: 19280 }, 2: { v: 6120 } }, }, }, ], });这样创建出来的 Univer 实例会绑定到一个默认的容器上,具体取决于你的 UI 插件。如果你是在自定义布局中使用,需要传入container参数。我的实践经验是:把 Univer 实例放在一个独立的模块里统一管理,不要散落在各个组件中,否则后续做多实例管理或销毁时会非常痛苦。
2.3 从表格数据到 Univer 数据模型的映射
Univer 内部的数据模型和传统二维数组不太一样,它更接近 Excel 的一个“行列索引 + 单元格属性”的结构。每个单元格的数据是{v: 值, t: 类型, s: 样式}这样的结构,其中t是数据类型枚举,v是值,s是样式索引。如果你直接用 JSON 往里塞值,需要知道这个映射规则。
比如要插入一个带背景色的单元格:
univer.getUnit<ISheetData>()?.setCellData(0, 0, { v: '重要', s: { fill: '#ff9900' }, });不过我更推荐的方式是:把数据处理的逻辑封装成工具函数,从服务端拿到的字段名映射到 Univer 数据模型时按需转换,而不是让业务代码直接依赖 Univer 的底层结构。
3. 协同编辑落地:基于 Yjs 的实时同步
3.1 Univer 的协同不是黑魔法,底层是 CRDT
Univer 的协同方案不是自己写一套通信协议,而是基于 Yjs 的 CRDT(无冲突复制数据类型)机制。Yjs 可以看作一个“共享数据状态的容器”,多个客户端对同一份数据的修改会被转换为不同的操作,通过合并算法自动解决冲突。Univer 在 Yjs 之上封装了针对文档模型的协同插件,这样用户操作就会同步到 Yjs 文档中,再由 Yjs 通过网络传输到其他客户端。
理解这层关系很重要:Univer 处理的是“表格编辑哪个单元格、谁撤销了、光标在哪”,Yjs 处理的是“这些改动如何同步到所有端”。所以只要你接入了 Yjs 生态的 provider,理论上就能实现实时协同。
3.2 协同 provider 怎么选:y-websocket 和 y-webrtc
Yjs 本身不负责网络传输,它需要搭配一个 provider。最常见的方案是y-websocket,由一个中心服务器转发文档更新。这个方案简单可靠,适合大多数内部中台和低代码产品。
y-webrtc是点对点方案,可以在没有中心服务器的情况下完成端到端同步。它的最大优势是省服务器带宽,缺点是不太稳定,容易受 NAT 网络环境限制,也不适合大文档。如果你做的是几十个人的小规模协作,可以用它低成本试水;但作为正式产品,我建议还是用 websocket。
接入协同插件的代码大致如下:
import * as Y from 'yjs'; import { WebsocketProvider } from 'y-websocket'; import { Univer } from '@univerjs/presets'; import { createSheetsUIPlugin } from '@univerjs/presets-sheets'; import { createYjsPlugin } from '@univerjs/yjs'; const doc = new Y.Doc(); const provider = new WebsocketProvider('wss://your-server.com/collab', 'room-sheet-001', doc); const univer = new Univer({ plugins: [createSheetsUIPlugin(), createYjsPlugin({ doc })], });这里要注意的是,createYjsPlugin的版本必须和 Univer 主版本匹配。官方文档里给的示例比较简洁,但实际项目里还要处理身份标识、客户端 ID、权限控制等。
3.3 权限控制和光标同步的实际处理
接入协同后,你会立刻遇到两个现实问题:一个是权限控制,另一个是远端光标显示。
权限控制方面,需要把“谁能读”“谁能写”下沉到数据层。简单做法是:房间维度控制读写权限,写入操作通过后端鉴权,只允许有权限的客户端向服务器提交更新。Univer 前端本身不承担权限逻辑,所以一定要在你的 provider 服务端做拦截。我的方案是自建了一个 websocket 网关,在客户端握手时校验 JWT,然后将文档更新转发到 Yjs 持久化层。
光标同步则相对简单。Univer 的协同插件会自动把当前用户的选中状态和光标位置同步出去,你只需要为每个连接分配一个稳定的用户 ID 并绑定颜色即可。在业务侧,可以维护一个 map 来存储在线用户信息,实时显示在线成员列表。
4. 踩坑记录:5 个让我查了一整天文档的问题
4.1 命中的第一个坑:样式文件没有引入,表格直接白屏
Univer 的组件样式是独立发布的 CSS 文件,如果只引入 JS 不引入 CSS,页面不会报错,但会出现表格区域空白、工具栏错乱。这个问题在新手里极其常见。
我的建议是直接在你项目的入口文件引入:
import '@univerjs/presets/styles.css';如果你使用的是自定义插件而非 preset,还需要根据插件目录引入对应的样式文件。比如只用 Sheets 而不用 UI,可能不需要引这个样式;但如果你要用工具栏和右键菜单,样式就必须进来。当初踩坑时肉眼看不出来问题,最后打开 DevTools 看到一堆 class 未定义才反应过来。
4.2 自定义单元格渲染闪烁:原因在每次渲染都重建节点
Univer 支持自定义单元格,允许你返回一个 DOM 控件放在单元格区域。我一开始在自定义单元格里渲染一个公司选择下拉框,结果每次滚动或数据更新时,下拉框都会闪烁一下,甚至从视觉上闪一下又重新渲染。
排查后发现,Univer 的渲染机制会为单元格生成一个矩形区域,自定义渲染器在每次重绘时都执行了清除和重建。虽然 Canvas 渲染性能不错,但 DOM 控件的创建依然有开销。解决办法是:在自定义渲染器中保留上一次创建的 DOM 节点,通过数据比对决定是否需要重建,而不是每次 render 都清空重来。这个优化做完后,滚动时的性能显著提升。
4.3 公式不重算:需要显式触发计算或注册计算引擎
用 Univer 时我最初手动往单元格里写了=SUM(A1:A5),结果编辑 A1 到 A5 后,总和不变。查了源码发现,Univer 的公式计算是作为独立插件存在的,而且计算依赖命令模式触发的recalculate。
如果你是用自定义数据喂给 Univer,比如通过setCellData改值,很可能不会自动触发公式重算。正确做法是在修改数据后手动调用计算相关命令,或者在初始化时确保公式插件注册完整。这是一个很隐蔽的坑,因为直接通过 UI 编辑单元格时一切正常,但通过 API 写数据就会卡住。
4.4 大数据量表格的假死:不是渲染问题,是数据对象过大
我测试一张 2 万行、20 列的表格时,Univer 的 Canvas 渲染表现还算平稳,但初始化时却卡了两秒多。后来分析发现,瓶颈不在渲染,而在将 JSON 数据转换为 Univer 内部数据结构的耗时。尤其当单元格样式各不相同、每格都带独立样式对象时,内存涨幅非常大。
优化思路是:给相同样式的连续区域复用样式索引,而不是每个单元格单独设置样式;对于纯文本和纯数据,避免构造大量结构相同的样式对象。另一个技巧是将初始数据拆成多个setCellData分批写入,配合requestIdleCallback分帧处理,避免白屏期。
4.5 版本升级的破坏性变更:从 v0.1 到 v0.2 的 API 调整
Univer 目前还处于快速迭代期,版本升级时 API 变动是常态。我从最早接触的 v0.1 系列升到 v0.2 时,UniverSheet组件的用法直接废弃,改成了Univer.createUnit的方式。这种激进变更对生产项目影响很大,所以我现在的做法是:锁版本号,小版本更新只在非核心环境先验证;官方迁移文档更新后再考虑升级。如果你刚接触 Univer,建议直接从较新版本开始,避免踩历史包袱。
5. 深度定制:把表格变成业务系统的一部分
5.1 自定义菜单项:从“表格”到“业务流程”
只用默认工具栏,Univer 就只是一个表格工具。但真正有价值的场景是把表格和业务流程打通。比如在运营中台里,我需要让用户拖选一批单元格后点击“标记为已审核”,或者右键菜单里增加“发送审批”。这些都可以通过 Univer 的 Command 机制实现。
官方提供的命令系统类似 Redux 的 action 模式。你可以注册自己的 command,绑定快捷键和菜单按钮。我在这里做了一个小实践:将选中的单元格区域数据封装成业务事件,通过自定义命令提交到后端流程引擎。这样用户在表格里操作后,下一步系统逻辑会自动触发,而不需要复制粘贴到另一个表单。
5.2 与服务端双向同步的最佳实践
如果你的表格数据需要持久化到数据库,最核心的就是“服务端权威数据”和“前端编辑数据”之间的同步。Univer 支持监听变更事件,你可以通过onCommand或onChange钩子捕捉每次编辑,然后将增量变更推送到服务端。但要注意,协同环境下每个键盘操作都会触发事件,直接每条同步会产生大量请求。
我的做法是:在前端做变更收集,通过 300ms 的防抖后打包为单位块,批量提交到服务端;同时服务端保存每份文档的版本号,如果版本冲突则通过后端版本合并接口处理。对于非协同场景,简单地将整个文档 JSON 保存即可;对于协同场景,需要将 Yjs 状态持久化到后端,更稳妥的方案是使用 y-indexeddb 或 LevelDB 存储 Yjs 更新记录。
5.3 模板化的应用场景:让不懂表格的用户也能用起来
Univer 适合做表格模板引擎。我们的系统上线后,业务人员总爱把表格列改乱,直接把公式覆盖掉。后来我在前端做了一套模板锁定机制:使用 Univer 的单元格保护属性,把模板中不需要编辑的列设置为只读;只有管理员切换“设计模式”时才能解除保护。
这个功能听起来不大,但业务反馈非常好。用户不用再担心误操作,也不会把自己的表格搞坏。同时,我把模板的初始化逻辑封装成一个 JSON 配置文件,业务方可以通过界面配置模板字段,再由程序自动生成 Univer 初始化数据。这个思路就是让表格组件既能承受重度 Excel 用户,也能照顾轻度操作者。
6. 写到最后:你的项目适合 Univer 吗
6.1 我推荐的典型场景
如果你符合以下条件,Univer 基本可以闭眼入:
- 项目需要一个嵌入式的在线电子表格,而不是完整部署一个办公套件
- 团队技术栈是 TypeScript,且前后端分离
- 需要定制界面,希望保持自身产品设计语言,而不是继承一个重型框架的外观
- 未来可能从表格扩展到文档或多页签产品
- 对开源协议的灵活性有要求,希望保留商用和二次开发能力
在这些场景下,Univer 的模块化优势非常明显。尤其是你已经有一套成熟业务系统,只想在某个模块增加“表格能力”时,Univer 的接入成本远低于部署完整办公套件。
6.2 需要谨慎的场景
反过来,如果出现下面这些情况,你至少要再想一想:
- 你需要的是非常成熟的协同产品,比如带细粒度权限、审计日志、版本历史完整套件,Univer 还需要你围绕它开发很多周边能力
- 你的项目要求极致稳定、API 长期不变,而当前 Univer 仍处于快速迭代期,版本升级会带来破坏性变更
- 团队没有前端高级工程师,集成和定制过程可能会比较吃力
- 你只是需要一个普通表格库,并不需要公式、协同、自定义渲染等能力,那杀鸡用牛刀反而给自己增加维护成本
6.3 我的实际体会
我在生产环境里跑了三个多月 Univer,整体稳定性超出了我的预期,但也没有到“无脑吹”的程度。它的核心渲染性能和架构设计在开源项目里是第一梯队,但周边生态还在完善中,很多问题需要翻源码才能找到答案。如果你决定使用,我建议你从一开始就建立一套测试用例,重点覆盖公式计算、协同并发、大数据量加载这三个高风险模块。把这些问题在开发阶段解决掉,后续维护会顺畅很多。
最后分享一个小技巧:遇到 Univer 的怪问题时,不要只搜 issue list,多去官方仓库看最新的 demo 和代码,很多文档没更新的用法,demo 里其实已经演示了。用它的过程其实也是一个从“用组件”到“读懂组件”的过程。这种收益,比单纯集成一个库要值很多。