Univer开源办公套件:在线表格集成与二次开发实战

发布时间:2026/11/5 4:58:23
Univer开源办公套件:在线表格集成与二次开发实战

如果你最近在前端社区里筛在线表格插件,大概率会反复撞见 univer 这个词;再搜下去,又会看到大量教程和演示视频挂在 “univer在线” 这个关键词上。我第一次看到 Univer 时,其实是有点怀疑的:一个开源项目,能把在线文档、在线幻灯片、在线表格全包了?自己动手试过之后,我发现它还真不是个花瓶仓库,而是一整套能塞进自己项目的办公套件。

这篇记录主要写给两类人:一类是普通用户,想搞清楚 univer 和 “univer在线” 到底能做什么;另一类是想在 Web 项目里集成在线表格的开发者,想知道怎么选、怎么接、怎么避坑。我会把它的定位、架构、集成过程和二次开发思路都摊开讲,不会只贴一个“很牛”的结论。

1. Univer 到底是个什么东西:一张在线表格背后的完整套件

很多人第一次接触 univer,是因为它官网那个在线演示页面,也就是大家说的 “univer在线”。你打开网页,就能看到一个和 Excel 差不多的表格界面:可以双击编辑、拖拽填充、写公式、做图表、开数据透视表。如果开个多人房间,还能看到别人光标移动、实时输入内容。

1.1 普通用户眼里的“Univer在线”

对普通用户来说,Univer 解决的是一个很常见的问题:我没有安装 Office,也不想下载一个几百 MB 的桌面包,我就想在浏览器里快速编辑一份表格,最好能邀请同事一起改。

Univer 在线演示做的事情就是这一套。打开页面、注册、创建空白表格或者导入一个已有文件,剩下的操作基本可以无脑上手。因为它把 Excel 用户熟悉的交互习惯都搬过来了:字母列号和数字行号、单元格地址栏、函数输入提示、右键菜单、拖拽填充柄。

以前这类能力几乎被 Google Sheets 或各种商业 SaaS 文档占据,Univer 出现的意义是——它把同样的体验开源了,而且允许你自己部署、自己改代码。

1.2 开发者眼里的代码仓库

从代码仓库的角度看,Univer 是一个基于 TypeScript 的办公应用套件,核心组织方式是 Monorepo,一个仓库下面管理几十个 npm 包。目前主流模块有三个方向:

  • Univer Sheet:在线表格,功能最完整,社区讨论也最多;
  • Univer Docs:在线文档,对标的是文档编辑器场景;
  • Univer Slides:在线幻灯片,目前相对年轻,但方向已经确定。

项目许可证是 Apache 2.0,这一点非常重要。Apache 2.0 允许你自由使用、修改、商用,只要保留版权声明和修改说明。对于公司项目来说,这比很多“仅限非商业用途”的组件友好太多了。

1.3 功能边界和定位

开发者容易把 Univer 理解成“又一个表格控件”,但它更准确的定位是“办公套件内核”。除了基础的单元格编辑,它还覆盖了:

  • 公式引擎:内置大量函数,支持自定义公式和跨工作表引用;
  • 条件格式:根据单元格值动态设置字体、底色、边框;
  • 数据验证:下拉选项、日期范围限制、自定义校验;
  • 图表:柱状图、折线图、饼图等,能和单元格数据联动;
  • 数据透视表:可以拖字段分析数据,这个能力很多开源表格组件没有;
  • 协同编辑:通过官方协作方案实现多人同时操作。

不过,它也不是什么都能做。Univer 默认不提供文件存储服务,不负责账号体系,不自带业务数据库。你可以理解为一个发动机,但整车得自己组装。这个边界想清楚,后面集成的时候就少一点不切实际的幻想。

2. 为什么我最后选了 Univer:一次没有标准答案的技术选型

在线表格组件这个领域,开源的、商业的、半死不活的,我都翻过一圈。实话实说,没有哪个方案是完美的。最后选 Univer,与其说是它无敌,不如说它在我的需求列表里得分最高。

2.1 我对表格组件的真实需求清单

在选型之前,我先把需求列了十七条然后压缩成最关键的几点:

  • 交互完整度:必须能点击、拖动、双击编辑、复制粘贴、撤销重做;
  • 公式能力:至少要支持 SUM、IF、VLOOKUP 这类常用函数,最好还能注册自定义公式;
  • 可控制性:JS 能直接读取单元格内容,能往指定区域写数据,能监听用户编辑后的事件;
  • 外观可定制:工具栏、右键菜单、主题色都要能覆盖,否则和产品风格冲突会很痛苦;
  • 授权安全:公司项目用,不能被 GPL 传染,也不能被商业授权费卡脖子;
  • 维护活力:不能是那种“五年前发过版本后就没动静”的仓库。

2.2 市面上几类方案的横向比较

我把自己真实比较过的几类方案整理成了一张表,方便你对照自己团队情况:

方案开源协议交互完整度公式协同维护状态适合场景
SheetJS / xlsxApache 2.0几乎无无无稳定但停滞纯导入导出
x-spreadsheetMIT基础编辑弱无基本停滞临时内部工具
LuckysheetMIT高中社区方案少停止推进老项目维护
Handsontable非商业免费,商用付费高弱无官方方案活跃数据网格编辑
AG GridMIT 社区版,企业版付费偏网格弱无活跃企业级数据表格
UniverApache 2.0高强有官方协作方案活跃在线办公套件

这个表里,x-spreadsheet 和 Luckysheet 最让人犹豫,因为它们上手简单、界面也像 Excel。但真实接入时,公式能力、协同、长期维护都跟不上。尤其是协同,如果项目只有一个人填表,那无所谓;一旦要多人一起操作,Luckysheet 那一层的方案基本得自己重写。

2.3 我果断接受的三个理由

第一是 Apache 2.0 授权,省去了法务审批环节。第二是它的公式引擎是自研且完整的一条链路,不是套个 eval 糊弄过去,这让复杂一点的业务计算有了底气。第三是工程结构清晰,插件化做得比较彻底,我想要哪个功能就装哪个包,而不是什么都塞进来。

当然它也有明显的学习成本。包多、API 变动频繁、文档更新跟不上发版速度,这些在后文“踩坑记录”里会详细讲。选型结论只有一句话:**如果目标产品本身就是一个在线表格或协同文档,Univer 是目前开源阵营里综合成本最低的选项。**如果只是某个页面里需要一个简单的数值录入网格,那真没必要上 Univer,x-spreadsheet 或者裸写一个表格就够了。选型永远是基于场景的,不是越重型越好。

3. 拆开 Univer 的骨架:Canvas、命令与协同究竟怎么配合

接手新项目时,我的习惯是先看结构,再看细节。Univer 的代码结构第一眼会让人有点晕,但把它拆成“渲染层、命令层、协同层”三层之后就清楚了。

3.1 为什么表格界面要用 Canvas 画出来

传统网页表格用 DOM 渲染,每出现一个单元格就是一堆节点,10 万行表格直接就卡死了,这几乎是所有 Web 表格组件的天花板。

Univer 选择了 Canvas 渲染。你可以理解成:表格界面不是由浏览器排版引擎“摆放”出来的,而是像游戏引擎一样,在画布上一帧一帧把可视区域内的单元格画出来的。滚动的时候,它只绘制当前窗口内能看到的内容,看不见的部分不创建任何 DOM 节点。

这样做带来两个直接好处:

  • 数据量大一点也能保持流畅,因为瓶颈不再是 DOM 节点数量;
  • 滚动和编辑时的视觉表现更稳定,不会出现节点反复创建销毁的闪烁。

代价是:你不能像操作普通 HTML 一样,直接在 DevTools 里选中某个单元格去查样式。所有单元格的渲染、hit test(判断鼠标点到了哪个格子)都要靠 Univer 自己在 Canvas 上计算实现。这也是早期我调试时最不习惯的地方。

3.2 命令系统让任何操作都可撤销、可同步

Univer 内部的交互设计遵循一套命令模式。用户点按钮、敲键盘,实际上都是往命令管理器里派发一个 Command。命令可以是一个操作,也可以是一组操作的组合。

这套设计的好处非常实际:

  • 撤销/重做简单:命令管理器记录了执行顺序和反操作,撤销就是逆序执行对应命令;
  • 协同同步简单:不同用户之间的状态同步,本质上是把命令广播给其他人,而不是同步整张表快照;
  • 扩展按钮简单:你自己做一个工具栏按钮,核心逻辑也是注册一个命令,和内置按钮平起平坐。

我打个比方:这就像把每个操作都当成一次 Git commit。有了 commit 记录,你才能回滚、才能把变更同步到远程、才能知道是谁在什么时候改了哪个单元格。

3.3 协同功能是“Univer在线”的灵魂

很多人把 Univer 在线演示等同于协同办公,但严格来说,协同不是默认就有的。它需要额外的协作插件和一套可以处理并发冲突的服务端逻辑。Univer 的做法是利用 CRDT 一类的冲突解决策略,让多地用户编辑同一份文档时,最终能收敛到一致状态。

具体协作流程大致是:用户产生编辑命令,命令发送到协同服务端,服务端对多个用户的操作做合并处理,再广播给房间内的其他客户端。冲突不是通过“你保存了我保存了,后保存的赢”这种粗粒度覆盖解决,而是在字符、单元格级别做融合。

实际接入时,意味着你要么用官方提供的协作后端示例做二次开发,要么通过 yjs 这一类 CRDT 库与 Univer 的数据层打通。这不是半小时能做完的事,但官方方案已经给了完整的技术链路,比从零写一套并发控制要省力得多。

3.4 插件化思想如何贯穿全项目

Univer 的代码不是一个大泥球,而是按功能拆成了许多独立插件包。你想要公式就引入公式插件,想要图表就引入图表插件,没有勾选的东西不会参与运行。

这种架构在工程上有个实打实的好处:按需加载,控制包体。我见过很多表格组件,不管用不用,反正所有功能都打进产物里,首屏加载体验一言难尽。Univer 的插件机制至少给了你优化的可能。

开发自己的扩展功能时,插件化也是入口。你可以写一个插件,内部监听 Univer 生命周期,注册自己的命令,再挂到工具栏菜单上。后面第 6 部分会有一个具体的扩展示例。

4. 把 Univer 接进前端项目:从 npm 安装到拿到第一份数据

这一部分是我实际接入的过程记录。Univer 的 API 在不同版本之间差异比较大,我先给出一套当前可用的入口写法,再标注哪些地方需要你按自己安装的版本查证。

4.1 环境准备与依赖安装

以 Vue 3 + Vite + TypeScript 的项目为例。Univer 官方推荐最简单的方式是装聚合包@univerjs/preset-sheets,一个包就能把表格相关的核心插件、UI 插件、公式能力全部带进来。

npm install @univerjs/preset-sheets

如果你需要文档或幻灯片能力,可以再装对应的 preset:

npm install @univerjs/preset-docs npm install @univerjs/preset-slides

要注意,这个项目的发版节奏很快,新老版本之间接口变化挺大。我第一次接入时,照着网上半年前的教程写,结果初始化 API 已经换了个写法。建议安装时固定大版本,并且以当前包里的类型定义为准。

4.2 初始化并创建一个在线表格实例

核心代码逻辑大致是这样一个入口模块:

import { LocaleType, Univer, defaultPreset } from '@univerjs/preset-sheets'; export function createUniver(container: HTMLElement) { const univer = new Univer( defaultPreset({ locale: LocaleType.ZH_CN, container, }) ); return univer; }

然后在业务组件的挂载生命周期里调用:

import { createUniver } from './univer-entry'; const univerRef = shallowRef<Univer | null>(null); onMounted(() => { const containerEl = document.getElementById('spreadsheet-container')!; univerRef.value = createUniver(containerEl); }); onBeforeUnmount(() => { univerRef.value?.dispose(); univerRef.value = null; });

容器需要有一个固定高度。很多人第一次跑不出界面,就是因为父容器高度塌陷成了 0。记得给你的 id 容器写上height: 600px或者height: 100vh。

另一个容易踩的是 Vue 的响应式代理。别把 Univer 实例直接塞进 reactive 或 ref 包装的很深的数据结构里。实例内部有大量对象和事件回调,被 Vue 代理之后性能会受影响,行为也可能变得奇怪。我一般用shallowRef或者干脆用一个普通模块级变量保存实例。

在 React 里,等价的做法是放进useRef,不要在组件每次渲染时重新创建实例。

4.3 读取单元格数据与导出工作簿

初始化之后,页面上就出现一个可以交互的表格了。接下来要解决的是如何把用户编辑的数据拿到手。

Univer 提供的工作簿模型访问接口可以这样用:

const workbook = univer.getActiveWorkbook(); const sheet = workbook.getActiveSheet(); // 读取单个单元格 const cell = sheet.getCell(1, 1); // 读取指定范围的二维数据 const range = sheet.getRange(0, 0, 10, 5); const values = range.getValues();

这里的行、列索引默认从 0 开始,不是 Excel 界面上的 1 起始编号,刚开始写的时候容易差一,调一会儿就习惯了。

要不要监听用户变更事件?要。Univer 的插件体系里可以监听单元格内容变化的命令,但高频操作时建议做节流,不要每次按键都往后端发送一次保存请求。我这里的做法是:用户停止编辑后触发一次“离开单元格”事件,再收集整张表的数据做保存。

4.4 主动控制表格内容

除了读取,还需要主动写入。比如有一个“导入上个月数据”的按钮,把后端返回的数组写进表格:

const sheet = workbook.getActiveSheet(); sheet.getRange(2, 0, data.length, columns.length).setValues(data);

再比如批量设置某个单元格公式:

const cell = sheet.getCell(3, 3); cell.setFormula('=SUM(A1:A10)');

这类 API 的命名风格和市面上主流表格组件接近,迁移成本不算高。但要注意,每次修改后最好调用一次重算相关机制,否则公式结果可能不会立刻刷新。如果遇到公式不更新的情况,优先检查是不是批量写入时没有触发数据版本更新。

5. 实测踩坑记录:中文输入、渲染性能、样式覆盖

每接一个重前端组件,都会碰到几个网上教程不会告诉你的坑。我在集成 Univer 的过程中,印象最深的有四个。

5.1 中文输入法候选框错位:到底谁的问题

在线表格这类 Canvas 应用,对中文输入法一直是个考验。DOM 编辑器天然兼容输入法候选框,而 Canvas 里的光标位置和输入框候选框并不在一个坐标系下。我遇到的现象是:输入拼音的时候,候选框找不到了,或者弹到了页面的左上角。

排查后确认,这是内部编辑器在 Composition 事件处理上的兼容问题。在比较新的版本里已经明显改善,但如果你锁定了老版本,就要注意:

  • 不要在键盘事件里提前阻断默认行为;
  • 不要在 compositionstart 还没结束时就执行命令提交;
  • 最好升级到较新的版本再谈中文输入问题。

另外,如果容器嵌在 iframe 里,输入法体验会更难控制。我的建议是:尽量不用 iframe 方案,直接在页面里渲染 Univer,输入法兼容问题会少很多。

5.2 大表格渲染卡顿:别让浏览器背锅

Univer 的 Canvas 渲染已经比 DOM 方案强很多,但这不代表你可以无限生成大表格。我曾经在一个页面里一次性加载了 10 万行、二十多列的数据,首屏依然是肉眼可见的等待。

后来发现,瓶颈不全在渲染,而在于数据初始化时给整张工作簿建了大量 Cell 对象。合理的方式是:

  • 按业务需求控制活跃行数,比如setRowCount(5000);
  • 数据分批导入,不要一个大的二维数组一次性塞进去;
  • 如果只是展示需求,关闭不必要的编辑插件和公式插件,能明显减少初始化时间。

5.3 样式覆盖的黑色三分钟

Univer 自带一套 UI,包括工具栏、工作表标签栏、右键菜单。我们项目里需要把工具栏高度压缩,并让主按钮变成品牌色。刚开始我用普通 CSS 强行覆盖类名,结果发现很多内部类名带了 hash 后缀,根本选不中。

正确做法是去了解 Univer UI 的主题变量机制。它支持通过配置覆盖主题色、文字色、工具栏背景色。我后来把重点从覆盖类名转移到注入图标的颜色和主题配置上,效率和稳定性都高了很多。

记住一个原则:对于组件内部样式,能用主题配置解决的,绝不要用壳外 CSS 硬刚。

5.4 版本漂移:一个升级引发的问题

Univer 在迭代期间有过几次破坏性升级:包名变了,初始化方式改了,一些旧 API 被移除。我在一个分支上升级后,整个工具栏按钮注册逻辑全部要重写。

现在我的策略是:

  1. 仓库里锁死精确版本,不开^自动升级;
  2. 升级前专门去看 changelog 里的 breaking changes;
  3. 核心入口代码保持简单,所有自定义功能都封装在业务模块里,方便版本适配时集中修改。

6. 二次开发:公式、按钮、协同服务,Univer 能玩到什么程度

接入能用只是第一步,真正让业务跑起来的是二次开发。Univer 在这一块给的空间很大。

6.1 注册自定义公式

我遇到的业务场景是:一张报价表里,价格由“成本 + 服务费 + 税费”组成,每个客户的计算规则还不一样。上百个单元格手写公式不现实,我直接在 Univer 里注册了一个业务公式,让用户在表格里写=QUOTE_PRICE(3000, 5)就行。

伪代码思路如下:

// 真正的接口名以你安装版本的官方文档和类型定义为准 import { registerFunction } from '@univerjs/sheets-formula'; registerFunction({ id: 'QUOTE_PRICE', calculate: (cost: number, serviceRate: number) => { return cost + cost * (serviceRate / 100) + cost * 0.06; }, });

注册完成后,Univer 单元格输入公式就能识别并参与计算。这个能力非常实用,等于把表格变成了一个业务计算引擎,前端用户不需要懂编程,直接填参数就能得到结果。

6.2 在工具栏加一个自己的按钮

Univer 的命令体系让自定义按钮变得顺理成章。我加了一个“保存当前视图”的按钮,逻辑是:注册一个命令,然后在命令处理器里调用业务保存接口。

import { CommandType } from '@univerjs/core'; export const SaveViewCommand = { id: 'business.save-view', type: CommandType.OPERATION, handler: async () => { await saveCurrentView(); return true; }, };

把命令注册进 Univer 之后,再把它挂到工具栏的某个分组,用户点击后就会触发保存逻辑。这样做的好处是,撤销重做、协同同步都走 Univer 的同一套框架,不会出现“操作了但保存不了”的状态。

6.3 把在线协同接到自己的服务端

官方协作方案不是一个能直接一键启动的插件,它需要配套的服务端。大致流程是:部署协同服务,客户端建立 WebSocket 连接,加入同一个文档房间,然后 Univer 的编辑命令通过协作插件同步到其他端。

实际接入时,鉴权是第一个要考虑的问题。协同服务不能裸奔,至少要做好房间级权限校验,避免用户随意拼一个房间名就能进入别人的文档。

其次要考虑持久化。协同服务通常会把文档状态保存在内存,如果服务重启,文档数据可能会丢。我的做法是:客户端关闭时触发一次快照持久化,定期把整份文档的数据同步到业务数据库。

6.4 和低代码表单结合的思路

Univer 还适合做数据填报模块。比如低代码平台里,用户拖一个“明细表”组件出来,底层直接放一个 Univer 实例,通过命令监听把表格数据映射成表单字段。

这种结合方式比用原生的 antd Table 灵活得多:用户能自己改行列、加临时公式、做手动汇总,而平台只需要在最后保存时读取一次整表数据。做内部系统、数据中台、运营后台时,这个组合非常能打动业务方。

7. 如果让我重新开始:集成 Univer 的备忘录

最后这部分不是我讲完课程之后随手加的总结,而是我踩完坑之后实实在在给自己列的清单。你要是现在准备上 Univer,照着这个清单走,能少走不少弯路。

上线前必须验证的五件事:

  1. 中文输入法全流程:在 Chrome、Edge、以及用户实际用的浏览器上各输入一遍中文、数字、英文,确认候选框位置和最终落格内容都正确。
  2. 10 万级数据的首屏表现:用业务真实数据量测试,而不是拿一个 100 行示例表就宣称性能达标。
  3. 权限边界:在协同未开启时,确认用户无法通过某些命令绕过只读限制。
  4. 保存与刷新链路:模拟“用户编辑后直接关浏览器”“编辑中途断网”“多人同时改同一个单元格”三种极端情况,确认数据最终一致。
  5. 版本锁定与升级预案:把当前版本写进 package.json 的精确版本,并在文档里记录升级时需要关注的破坏性变更列表。

团队内推进的经验是:

先不要一上来就追求远方。把一个真实、小范围、业务愿意配合的页面跑通,比在实验室里研究架构更有价值。等这个页面稳定上线,再把 Univer 推广到其他需要在线表格的产品模块。我后来所有对性能、协同、二次开发的判断,都是在这个“第一个真实页面”上线后得到的。

如果你打算用 Univer,建议做好和版本变更共存的准备。它目前仍在快速迭代,今天文档里写的东西,几个月后可能就换了位置。但只要把入口封装得足够薄,核心依赖锁定得住,这个项目能扛的活,远比第一眼看上去要重得多。