Univer接入实战:把在线编辑表格无缝嵌入你的前端应用
如果你的工作恰好是“给产品加一个在线编辑表格的功能”,那你大概率已经听过 univer 这个名字。就算没听过,在 GitHub 上搜电子表格相关开源项目时,它也会频繁出现在视野里。
univer 的官方定位是一套开源的、可自托管的协同办公套件。但这么说太文绉绉了,用项目场景讲更直接:它让你能在自己的前端项目里,内嵌一个接近 Google Sheets 体验的电子表格,支持公式、样式、图表、数据透视表,甚至多人同时在线编辑。和传统做法相比,最突出的一个差别在于,它不是一个“静态展示表格的组件”,而是把一套完整的表格引擎装进了你的应用里。
这篇文章我打算用自己的实际接入经验展开,讲清楚 univer 能做什么、为什么值得选、怎么快速集成、以及一路踩过来的一些坑。适合正在做在线办公、数据中台、ERP、低代码平台、BI 看板这类产品的前端团队参考,也适合那些光看文档觉得“好像很强大”但迟迟没敢上手的朋友。
1. univer 到底是什么:不是普通表格组件
1.1 从一个真实的需求场景说起
我在去年接手一个企业级数据管理平台的前端改造,需求方提了一个听起来非常“简单”的需求:在 Web 端打开一个项目数据看板,用户能像用 Excel 一样直接编辑数据、拖拽排序、下拉筛选,并且多个部门的人同时打开时,修改要能实时同步。
一开始团队里的直觉方案是找个现成的表格库渲染数据,比如用 Ant Design Table 配上可编辑单元格,再自己写一套 WS 同步逻辑。做到第 5 个功能时我开始发现不对,用户想要的不是“像 Excel 的输入框”,而是真正的 Excel 行为:公式联动、条件格式、跨表引用、合并单元格、撤销重做、甚至是复制粘贴带格式的数据。这些需求如果全部自己造,那不是几周的工作量,是几个月的工程量。
当时调研下来的选择是:要么接 OnlyOffice 的在线编辑实例,要么在 Luckysheet 基础上做二次封装,要么押宝 univer。考虑到我们需要的是高度可定制的内嵌体验而不只是跳转到独立编辑器,同时还要具备协作能力,univer 最终胜出。这个名字看起来没有中文资料和社区热度那么高,但它的定位恰好命中了我们的需求。
1.2 univer 的核心功能和解决的核心问题
univer 不会强占整条业务链路。它提供的是一套基于原生 JavaScript/TypeScript 开发的嵌入式 SDK,而不是一个独立部署的开源软件实例。用技术圈的话说,它不是 SaaS 产品,而是你可以嵌入到任意前端工程里的“办公套件内核”。
它有三个维度让我觉得值得投入时间:
- 电子表格引擎:支持超过 300 个公式函数,包括 VLOOKUP、SUMIF、数组公式和动态数组。这些不是纸面功能,是真正跑在纯前端计算引擎里的逻辑,离线可用。
- 三种文档类型:不止是表格,它还支持文档和幻灯片。这个信息很关键,意味着同一个团队后续要做在线 Word 或 PPT 功能,不需要换技术栈,底层架构是通的。
- 协同能力:univer 的可协同模式基于 WebSocket 实现实时多人编辑,它把“文档快照 + 操作序列”的合并过程封了一层,前端代码不太需要关心 OT(Operational Transformation)或 CRDT 的内部实现细节。
所以,它真正解决的问题不是“显示一个表格”,而是“怎么不重复造一套桌面级办公软件的轮子,同时还能把办公能力无缝缝进自家的 Web 产品”。
1.3 univer 和主流开源方案的横向对比
我见过不少团队在技术选型时,把 univer、Luckysheet、OnlyOffice 放在同一个维度上比。从我的实践感受来看,这其实不太公平,它们解决的问题维度并不完全一样,但为了避免踩坑,还是做一个直观的对比:
| 方案 | 渲染模式 | 协作支持 | 文档类型 | 集成方式 | 适合场景 |
|---|---|---|---|---|---|
| Luckysheet | Canvas + DOM | 需要自己接后端 | 仅表格 | npm 包 | 简单表格展示、已有系统快速嵌入 |
| OnlyOffice | Canvas(文档服务) | 内置协作 | 表格/文档/PPT | 独立部署服务 | 需要完整 Office 体验、公司内部网盘 |
| univer | Canvas | 内置协作骨架 | 表格/文档/幻灯片 | npm 包 + SDK | 产品深度定制、需要内嵌和二次开发 |
只看表格的话,univer 不一定是功能最全的那个。但它最大的特点是“软件形态”和“自由程度”。你可以把它当成一个 npm 包装进自己的单页应用,按需唤起工具栏、编辑框、公式栏、状态栏,界面风格可以改成自己想要的样子。OnlyOffice 要嵌入到自己的业务界面中,通常是靠 iframe 或者改独立前端页面,二开成本高得多。
还有一个容易被忽略的点是 univer 新版重构后的插件化体系。它把核心引擎和 UI 层做了解耦,你甚至可以实现一个自定义插件,直接拦截用户的复制粘贴事件做数据清洗,然后再交给表格引擎渲染。哪怕只看这一条,二次开发的空间就远超普通表格库。
2. 技术选型背后的核心考量与技术原理
2.1 为什么选择 Canvas 渲染而不是 DOM 渲染
在做技术选型时,最需要想清楚的一个问题是:表格的渲染层应该如何设计。传统 Web 表格组件,比如 Ant Design Table、Element UI 的 Table,本质上还是在 DOM 上生成成千上万个单元格节点来展示数据。这样做的优点是浏览器原生支持,开发者好调试,但缺点是当表格达到 1000 行、50 列这种中低规模时,页面的 DOM 节点数量会迅速膨胀,滚动、编辑时频繁触发重渲染,交互掉帧就会非常明显。
univer 走的是 Canvas 渲染路线,整个工作表是在一个 Canvas 画布上绘制出来的,而不是逐单元格生成 DOM。这意味着,一个区域内哪怕有再多的行和列,实际存在于页面上的节点也远远少于传统方案。Canvas 绘制需要自己管理命中检测、选中区域框、编辑器定位、滚动视口裁剪等一系列逻辑,难度确实高,但换来的收益就是在大数据量场景下的帧率显著提升。
我当时在 2000 行 x 30 列的数据集上做过一次实测:DOM 类表格在每次输入一个字符后,从事件触发到界面刷新大约要 80 到 120 毫秒,如果开启了单元格公式重算,往往能到 300 毫秒以上;而 univer 在这个规模下普通编辑操作基本稳定在 50 毫秒以内。对于做数据分析类的产品,这个差距就是“能用”和“好用”的区别。
2.2 univer 的协同同步机制到底怎么理解
关于 univer 的单人模式,大家基本不会担心,它就是一个非常强大的本地表格引擎,所有公式、样式、渲染都在浏览器里完成。真正让人疑惑的是它的多人协同模式:同一份表格,多个用户同时打开,A 改了 B2 单元格,B 在另外一边应该如何看到变化?
univer 的做法在架构思路上借鉴了类 OT 模型,但和 Google Docs 那种完全去中心化的协议不同,它主要是中心化服务端负责多端操作的处理。简单来说,服务端保存一份权威文档快照,每个客户端本地操作后生成一个操作命令,通过 WebSocket 推给服务端。服务端把操作合并进文档状态后,再把变更广播给其他在线客户端。
这里最核心的一个概念是“命令”而非“数据”。协同同步的不是修改后的整张表内容,而是一笔一笔的操作记录,例如“在 A1 单元格设置值为 100”“合并 B2:C3”“插入一列,在 D 列之前”。这样做的好处相当明显:操作的传输体积远小于全量数据,而且任何一端的操作都具备可追溯性,也方便做撤销重做和权限审计。
不过需要注意,univer 虽然内置了协同服务端的骨架逻辑,但它不是“装上 npm 包就自动多人协同”的。你要有人负责部署和接入协同服务,具体实操我会在后面的章节展开。如果只是单人编辑,是不需要服务端的。
2.3 插件化架构给扩展带来了什么想象空间
插件化是 univer 和第一代 Luckysheet 拉开差距的核心原因之一。如果说表格引擎是操作系统内核,那么插件就是内核之外的驱动和应用程序。univer 的插件可以注册自己的一整套命令、UI 组件、快捷键绑定和数据变更监听器。
举一个我们项目里的实际例子:我们需要做到表格里输入设备型号后,自动联想出设备的基本参数、价格和库存状态,并当库存低于阈值时在单元格里给出标红提示。在过去,要么拿到表格值变更事件后去后端查询再手动修改 DOM 结构,要么干脆放弃联动交互。但在 univer 的插件体系之下,我实现了一个监听器,在单元格值变化时触发异步请求,再通过命令通道更新相邻单元格内容。整个过程完全在表格引擎的“操作系统”内完成,不会出现数据与界面不同步的脏状态。
这类扩展能力对团队的意义在于,表格组件从“买来的功能”变成了“可生长的平台”。只要愿意投入,你可以把任何业务逻辑通过插件注入表格交互中。
3. 实操第一步:五分钟在 Vue3 项目里跑起 univer 表格
3.1 环境准备与依赖安装
从安装依赖到页面显示出一个可编辑表格,我实测下来确实可以在五分钟内完成,前提是你有一个现成的前端工程。以目前常见的 Vue3 + Vite 工程为例,安装 univer 相关依赖的方式很简单。
npm install @univerjs/preset-sheets @univerjs/core @univerjs/design有一点值得提醒,univer 的包名设计一开始容易让人懵。@univerjs/core 是核心引擎层,@univerjs/preset-sheets 是官方打包好的表格预设场景,相当于一个“一键配置”,它把表格渲染、公式、UI 相关插件一起装配好,适合快速起步。如果你只需要简单的只读展示,也可以只装这个预设,不做二次开发。
安装过程中可能会遇到版本不一致的 peer dependency 警告。univer 的多个包之间版本需要严格对齐,我不建议用 npm 的自动修复去强行拉平版本,最好统一锁定一个具体的小版本,比如全部使用 0.1.x 的某一版。我在实际项目里就遇到过因为 core 版本升级到了 0.2 而 preset 还停留在 0.1 导致白屏的情况,排查下来发现是 API 名不兼容,这种问题只能通过锁定版本规避。
3.2 初始化一个可编辑的表格实例
依赖装好后,在 Vue 组件里的接入方式相当直白。univer 的编程模型是创建一个 Univer 实例,然后在实例之上创建具体的文档单元。我用一个简单的 setup 过程说明:
import { Univer } from '@univerjs/core'; import { UniverSheetsPlugin } from '@univerjs/sheets'; import { UniverPresetSheetsPlugin } from '@univerjs/preset-sheets'; const univer = new Univer(); univer.registerPlugin(UniverSheetsPlugin); univer.registerPlugin(UniverPresetSheetsPlugin); const univerInstance = univer.createUniverSheet({ id: 'my-first-sheet', name: '项目数据看板', sheetOrder: ['sheet-01'], sheets: [ { id: 'sheet-01', name: '销售明细', rowCount: 500, columnCount: 50, cellData: { 0: { 0: { v: '产品名称' }, 1: { v: '销量' }, }, }, }, ], });核心逻辑就是两步:创建 Univer 核心实例;用 createUniverSheet 在工作区内注册一个工作表。从数据结构上看,sheet 配置里的 cellData 就是二维对象映射,key 是行号、列号,value 是单元格对象。这个初始化过程是同步执行的,所以组件挂载后表格会直接呈现。
对于 Vue 的响应式,这里有一个重要的实操心得:千万不要把 univer 实例直接包装成 reactive 对象。因为 univer 内部维护着自己的状态机,它的变更不走 Vue 的响应式系统,如果强行 proxy 包装,轻则渲染异常,重则触发无限递归更新。正确做法是用一个普通模块级变量或者 shallowRef 保存 univer 实例,业务侧需要读取表格数据时,通过 univer API 主动获取。
3.3 如何按需配置工具栏和数据填充
如果只是显示一张空表格,那任何库都能做到。真正要接入业务,最常见的是这三件事:工具栏显示、初始数据填充、行高列宽设置。
工具栏配置方面,univer 是通过注册 UI 插件来控制的。例如需要显示公式栏,就要安装对应的 plugin。如果团队只需要编辑和基础样式修改,那最少需要 @univerjs/ui 和 @univerjs/sheets-ui。官方预设包默认会带齐全套 UI 能力,但要注意,文档服务和前端 UI 插件在 univer 中是可以独立增减的,这点对需要裁剪界面的团队很重要。
import { UniverUIPlugin } from '@univerjs/ui'; import { UniverSheetsUIPlugin } from '@univerjs/sheets-ui'; univer.registerPlugin(UniverUIPlugin, { container: 'app', header: true, toolbar: true, footer: true, }); univer.registerPlugin(UniverSheetsUIPlugin);在数据填充上,初始配置里的 cellData 如果数据量很大,配置对象会写得非常臃肿。我建议初始配置只放表头和空壳结构,数据在实例创建之后通过 API 批量写入。比如用 getActiveSheet() 拿到当前工作表,然后用 getRange().setValues() 一次性写入二维数组,代码清晰得多,性能也要好很多。下面这段是我的常用写法:
const sheet = univerInstance.getActiveSheet(); sheet.getRange(1, 0, 499, 50).setValues(realDataArray);这个 API 的行为是一次操作批量设置一片区域的值,和 Excel 的粘贴行为类似,内部会统一计算并触发一次界面刷新,比逐单元格 setValue 高效得多。
3.4 公式与数据格式化配置
公式是用户感知最强烈的功能之一。univer 对公式的支持度很高,基本覆盖了 SUM、AVERAGE、IF、VLOOKUP 这些高频函数。要在表格里使用公式,可以直接在单元格对象的 f 字段里传入。
cellData: { 2: { 5: { v: null, f: '=SUM(B1:B20)', }, }, }这里有个细节需要特别注意:当公式存在时,v 字段允许为空,因为计算引擎会负责结果赋值。但如果你同时传了 v 和 f,那 v 会被当作缓存值,这通常没问题,可一旦公式引用的区域数据发生变化而缓存没有失效,界面上就可能显示老结果。我在接入时干脆不让后端回传计算值,只回传公式表达式,计算结果完全交给前端引擎。
另一个高频需求是数字格式化,比如金额千分位、百分比、日期格式。这类需求不需要写公式,而是通过单元格样式层面的配置来实现:
import { UniverNumberFormatPlugin } from '@univerjs/number-format'; const format = { type: 'custom', pattern: '#,##0.00', }; sheet.getRange(1, 5, 10, 1).setNumberFormat('0.00%');格式化的坑主要在 pattern 字符串的语法上,它遵循的是 Excel 的自定义数字格式语法,不是 JavaScript 的 Intl.NumberFormat 的语法。比如百分比要写成 “0.00%” 而不是 “percent”。这一点如果不留意,很容易把界面搞成意想不到的显示结果。
4. 在线协同:univer 最具想象力的能力落地
4.1 协同模式的整体架构:不只是表格的同步
之前说过,单机模式下 univer 完全靠前端计算。但多人协同不是那么简单的事,它至少牵扯三层:前端表格引擎、协同消息通道、文档状态权威服务端。
从架构图上我习惯这样理解:前端是所有用户看到的操作面板;协同通道就是 WebSocket,负责实时传递操作命令与状态同步;权威服务端保存文档主版本,并且负责把各客户端的操作按顺序合并,让多端状态收敛。
univer 的协同服务端是基于 Node.js 的,项目里提供了类似 @univerjs/engine-formula 底层的计算迁移方案,方便做服务端计算。但在实际场景中,绝大多数业务不需要服务端处理公式计算,公式算得动的地方就应该留在前端。所以我建议朝着“前端负责大部分本地计算,服务端只负责同步操作指令和最终快照”的方向去架构。
有一点要清醒认识:多人协同并不是 univer 装好就有的,它至少需要开发人员自己维护房间概念、连接认证、文档权限校验。对这类工作,我个人的建议是,在首次接入协同能力时,尽量不要一上来就做全流程的权限系统,先跑通两个浏览器之间能够互通修改,再逐步加鉴权。
4.2 实操接入协同服务的配置方法
实际的接入步骤主要分三部分:启动协同服务端、创建文档并通过房间 ID 关联、前端通过 WebSocket 连接。
第一步,需要安装并启动 univer 的协同服务。常见做法是在项目里引入 @univerjs/network 相关包,配置服务监听端口和文档存储位置。更推荐在官方文档服务基础上做二次开发,或者直接使用 Redis 做文档快照和操作流的持久化。用一个官方支持的启动脚本来说明:
npm install @univerjs/server npx univer-server --port 3010 --storage redis://localhost:6379第二步,前端创建表格时要指定一个文档 ID。多人要看到同一份文档,靠的就是这个 ID 一致。你不用管它是 UUID 还是自增 ID,只要它能唯一标识一份工作簿即可。在业务里,通常把这个 ID 和数据库里的项目 ID 绑定。
第三步是在前端初始化时连接协同服务:
import { UniverCollaborationPlugin } from '@univerjs/collaboration'; univer.registerPlugin(UniverCollaborationPlugin, { serverUrl: 'ws://localhost:3010', docId: 'project-001-sheet', userId: 'user-1024', });连接建立后,表格里的编辑操作会被自动转换为命令同步到其他端。多人同时改同一个单元格也不是什么问题,服务端会按操作到达顺序处理,后到达的操作覆盖先到的,和大部分在线文档的行为一致。
4.3 协同落地时的策略选择和注意点
真正上了真实项目,我发现有几个策略必须提前定好,否则后期返工成本非常高。
第一个是文档快照机制。用户编辑时产生的操作流如果不做持久化,一旦服务端重启,所有历史操作就全部丢掉了。所以需要在服务端落一个快照,比如每隔 5 分钟保存一份全局状态,同时把 5 分钟内的增量化操作记录也存下来。之后即使在编辑过程中刷新浏览器,也能通过快照加操作回放恢复最新状态。快照频率取决于你们的业务容忍度,对数据敏感型业务,我建议快照间隔不超过 2 分钟。
第二个是操作合并和压缩。多人频繁编辑时,同步的操作流会非常密集,比如单元格的值可能三秒内被改了十次。真实线上没有必要让十次操作全部同步过去,可以让服务端做节流合并,比如 500 毫秒内同一个单元格的操作只保留最后一次。这样大幅降低 WebSocket 消息量和前端渲染频率。
第三个是权限控制。univer 的协同本身不限制谁能编辑谁只能看,但业务里肯定要做。我实践下来比较稳妥的方式是在接入层做判断:客户端连接时携带用户身份 token,WebSocket 服务端在握手阶段校验权限,只有具备编辑权限的连接才会被允许发消息。
5. 实战中的高频问题与排查思路
5.1 数据更新后界面刷新不出来
这是新手几乎必踩的第一个坑。我第一次接入时,给表格设置了一个外部变量,然后通过 Vue 的响应式原理去修改表格单元格,结果发现表格没任何反应。
原因是 univer 并不是一个受控组件,它的数据流是从表格引擎内部发起的。你要从外部改数据,必须走它提供的命令 API。最常用的方式是:
const command = { type: 'set-cell-data', payload: { row: 3, column: 5, value: '新数据', }, }; univerInstance.executeCommand(command);一开始你可能不太习惯这种模式,但理解之后就明白了,它和 Redux 的 dispatch 很类似:不是直接改状态,而是通过动作描述“你要做什么”。好处是所有变更都被统一拦截,方便做协同同步、撤销栈的记录和权限校验。
5.2 公式不计算和引用范围过期问题
遇到的公式问题里,最常见的是公式结果不自动更新。排查思路首先要分清楚:是公式本身写错了,还是计算依赖链断了。univer 的公式引擎默认会在单元格值变更时触发依赖关系图的重算,但如果你是通过 API 批量写入的数据,有些版本下需要手动触发一次全量计算。
univerInstance.getCommandService().executeCommand({ type: 'recalculate', });另一个更隐蔽的坑是公式里写入绝对引用时,区域范围写错了。由于 univer 的行列索引是 0-based,如果你从 Excel 里复制公式过来,习惯性地把 A1 当作第 1 行第 1 列,那么在 univer 里它实际是 row 0、column 0,这个没有大问题,问题在于比如 Excel 的 B2,在 univer 里应该是 row 1、column 1。写错一个数字,公式经常指向完全不同的区域。
我建议所有公式字符串统一由业务侧生成,流程上避免用户手工从 Excel 原样复制不受控的公式。对面向内部员工的系统,可以做一个公式输入辅助弹窗,用户填参数,我们组公式。
5.3 多人协同状态下偶发的内容不一致
多人协同上线后,我最担心的问题是没有发生惊天动地的大故障,反而是一些零零碎碎的可疑现象,比如 A 看到的数据和 B 不同步,刷新后又正常了。
大多数情况下,这是 WebSocket 断线重连后操作丢失导致的。移动端网络切换,或者 WiFi 短暂断连,消息没有成功发送到服务端,而前端界面本身已经改了,就造成了本地和服务端的分叉。
针对这种情况,必须实现一个连接状态同步校验逻辑:前端只在 WebSocket 状态为 OPEN 时才允许执行受协同保护的编辑操作,一旦连接断开,界面要么进入只读状态,要么明确提示用户“离线模式,重新连接后同步”。不要想当然地让离线操作进入本地队列,因为一旦队列里的操作和服务端快照产生冲突,解决起来远比想象的复杂。简单粗暴的方式最稳:断开就锁编辑,恢复后强制拉取服务端最新快照刷新页面。
5.4 性能优化与超大表格的滚动体验
虽然 Canvas 渲染解决了大部分性能问题,但当数据规模到几万行时,仍需要按需渲染配置。univer 本身支持 skt 超大网格的 streaming 渲染,但前端仍要配合做视口裁剪。
此处最大的性能杀手不是渲染而是数据接入方式。很多人习惯在 createUniverSheet 配置阶段把十万行数据一次性塞进 cellData。这样会导致初始化耗时成倍上涨,因为每一格都要经过类型检查与格式化。正确的姿势是先创建一个空壳表格,等用户真正滚动到需要数据的位置附近时,再通过 API 分块注入可见区域的数据。这和图片懒加载的原理是一样的。
5.5 实际使用心得:部署、版本与测试策略
最后分享一些偏工程管理的经验。
版本锁定是最重要的一个工程决策。univer 的迭代速度很快,API 变化也频繁,如果你的项目打算长期维护,务必在 package.json 里精确锁版本,并安排固定时间升级。不要每次都是 “npm install univer 最新版” 然后发现问题再回退,这种反复对团队的信心打击很大。
单元测试不要只测公式计算,更多的是要测命令执行链。可以针对“修改单元格 A1,触发公式 B1 重算,同步命令发出”这种完整链路写用例。前端表格最怕的是改一处崩一处,有了一批基础链路测试,后续升级版本时会安心得多。
部署方面,因为 univer 属于纯前端资源,打包出来的 JS 产物比较大,可以做按需加载和 CDN 分包。建议将 univer 相关包单独用 Vite 的 manualChunks 配置拆包,加载性能会明显好很多。
6. 关于 univer 在线场景的进一步思考与个人体会
如果项目定位是轻量级表格展示,用 antd 表格就足够,完全没必要上 univer。但如果你的产品明确走向“在线协同办公”或“数据协作平台”这个方向,那 univer 会是一个极有长期价值的底座。它解决的不只是“画表格”,更是把表格从一个静态 UI 组件升级成了可交互、可协同、可编程的业务容器。
我在接入过程中最大的体会是,univer 最大的门槛不在 API 使用,而在思维模式的转变。它做任何数据变更都建议走命令总线,协同同步又要求命令可序列化。这种设计对开发者的代码组织能力要求不低,但一旦适应了,后续加功能会非常丝滑。
最后还想分享一个非常务实的小技巧:如果你的团队没有专职研究协同算法的人,一定不要把协同协议想得太复杂,就直接信任 univer 提供的服务端骨架,把精力重点放在业务字段的映射、权限控制、异常恢复上。尤其在账号体系、消息确认机制、断线重连这些工程细节上多花时间,远比改协议重要。作为一个开源项目,univer 的代码质量在同类里相当扎实,翻开源码能看到很多可借鉴的设计。对于想深入理解现代 Web 办公应用底层原理的人来说,这也是一个非常值得研究的样本。