尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

Univer在线表格集成实战:从架构原理到协同编辑与二次开发

发布时间:2026/9/29 23:41:18

资讯中心
01
ARTICLE

Univer在线表格集成实战:从架构原理到协同编辑与二次开发

Univer在线表格集成实战:从架构原理到协同编辑与二次开发
1. 为什么我盯上了 univer 这个新玩具作为常年跟 office 全家桶、在线表格、协同文档打交道的前端团队我们最烦的事情就是业务方突然来一句能不能在系统里直接加个表格。先不说要不要买第三方授权光是嵌入、自定义、协同、公式、样式这一套组合拳打下来传统方案要么用 iframe 套一个轻量版 Excel要么接付费的在线表格服务。结果往往都是体验割裂、改造成本高、还在线协作功能瘸腿。univer 这个名字最早是我在 GitHub 上逛前端可视化仓库时看到的。起初以为是某个实验性库点进去才发现这是一个基于 TypeScript 的、用 Canvas 自己画格子、画菜单、画公式栏的开源办公套件项目定位不是能用的表格插件而是一套可以在任意前端项目里嵌入的办公套件底座。而 univer 在线指的不只是把表格渲染在网页上更核心的是它具备多人实时协作、命令变更合并、服务端数据同步等一整套可插拔方案。这一下子就命中了我手里还没做完的在线数据协作平台改造需求。连续几个晚上我都在翻它的架构文档和源码越看越觉得这玩意儿值得写一篇长文聊聊。这篇文章不适合纯小白更适合那些手头有真实业务、想把 univer 或者类似的在线表格组件集成进自家系统的开发同学。我会从定位拆解、核心原理、实际接入、踩坑记录几个维度展开尽量把为什么这么设计和实际怎么用都说透。2. 核心架构拆解先搞清楚 univer 在设计上到底想干什么2.1 它不是粗暴的Excel 网页版而是一套办公套件底座很多人第一眼看到 univer会拿它跟 Luckysheet、Handsontable、xlsx 库对比。我最初也这么想但翻了源码和社区讨论后发现定位完全不一样。Luckysheet 和 Handsontable 本质上还是表格控件主要解决的是格子交互、数据绑定、样式展示。而 univer 从一开始就拆成了多个独立单元Univer Sheets电子表格、Univer Docs文档、Univer Slides幻灯片。也就是说它想做的是一整套完整的办公软件前端方案而表格只是其中一个被拎出来最先打磨的模块。这个设计带来的直接好处是如果你只需要电子表格能力你可以只引入 Sheets 相关包如果你未来还要做文档、幻灯片可以在同一套架构下复用协同、命令、插件机制而不用重新造轮子。我实际集成的过程中对这种模块化的好感非常强烈因为你永远不知道业务下一步会不会要求顺便加个文档编辑。2.2 一个 univer 实例对应一套独立的办公文档上下文在 univer 里有一个贯穿所有模块的核心概念univer instance。你可以理解成它是一个虚拟工作空间内部维护当前文档的数据状态、选区状态、命令栈、插件注册表等。代码层面每个 univer 实例通常对应一个 id而且可以并存多个实例。也就是说你完全可以在同一个页面上挂两个表格区域各管各的数据互不干扰。这一点在对接后台管理系统时特别有用比如一个页面左侧放销售报表右侧放库存盘点两个表格各自独立初始化即可不用像某些老库那样全局只有一个 workbook要切换数据还得销毁重建。我自己的项目里就用了双实例方案实测下来状态隔离做得很干净内部分区、各自滚动、各自命令栈互不串扰。2.3 命令系统一切操作都走命令而非直接改数据初次接触 univer 的命令机制可能会觉得有点绕。但用多了就会发现这是它支持协同和撤销重做的基石。简单说所有新增工作表、改单元格值、调样式、增减行列都不是直接操作底层数据模型而是派发一个 command。每个命令都是可序列化的描述我做了什么比如把 A1 单元格的值改成 100。这个设计的好处有三个撤销重做天然支持命令栈 push 进去undo 就是逆操作协同编辑容易实现远端操作通过网络广播命令本地执行后状态自然一致权限拦截方便在命令派发前可以统一做校验禁止某些用户执行写操作我刚开始还嫌麻烦觉得改个值要绕一圈。但实际写业务的时候发现这种命令化思维反而让代码更稳妥因为所有数据变更都走同一套出口排查问题非常轻松。3. univer 在线协同能力详解多人同时改一张表到底是怎么做到的3.1 协同不是定时刷新而是操作变更的实时合并很多做过在线文档的朋友都知道协同最核心的问题不是怎么同步数据而是两个人同时改同一个单元格以谁为准怎么合并不冲突。univer 的协同方案走的是操作转换OT路线。每次编辑动作都会被封装成 Operation然后发送给协同服务器。服务器端会做转换合并确保各客户端的最终数据一致性。这跟传统 CRDT 文档的做法不完全一样它更强调操作的有序化。我在自己搭的 WebSocket 协同服务里主要流程是这样的客户端 A 修改单元格生成一个 command payloadpayload 附带当前操作的顺序标识版本号或时间戳服务端接收后把操作广播给其他客户端其他客户端收到后执行同样命令更新本地数据这套逻辑对于不会同时编辑同一个单元格的场景已经足够用。如果业务真的到了两个人同时改同一个格子这种极端并发OT 算法的深入调校还是绕不开的难题但至少 univer 已经给出了可以沿着走下去的路径。3.2 协同过程中的版本控制与冲突处理我实践下来最怕的不是协同做不到实时而是版本错乱。univer 官方在协同架构上给出的思路是客户端本地维护一个基础版本号每一次操作都基于该版本号生成。服务端接收到操作后如果不等于当前期望的版本号就说明有其他操作先到达需要做合并或丢弃。实际搭这套服务时我给每个操作都加了一个sequence字段服务端用一个自增编号来标记当前最新版本。客户端如果发现自己的操作sequence落后于服务端返回的最新版本就会走一次拉取最新状态 重新应用本地变更的流程。这个过程看起来复杂但一旦跑通体验远胜于那种每 5 秒全量拉数据的伪协同。我现在内部开发环境里已经能做到两三个人同时编辑表格延迟基本感知不到。注意协同服务不是 univer 自带的需要自己实现服务端逻辑可以基于 WebSocket 或者现有实时通信框架来做前端只负责生成和派发命令。3.3 数据持久化与服务端存储协同表格绕不开另一个问题数据存哪、怎么存。univer 前端本身只负责把数据展示和编辑出来它并不强制你要用什么数据库。你可以把整个工作簿的结构化 JSON 存到 PostgreSQL、MongoDB甚至直接存文件。关键在于当命令派发过来时服务端需要同时更新持久层数据并把变更广播给其他在线客户端。我的做法是服务端维护一份「工作簿快照」每次收到命令就修改内存中的快照同时异步写数据库持久化。为了减少写库频率我做了简单的增量合并100 毫秒内的操作打包成一批 commit。这样数据库压力不大崩溃恢复时最多丢 100 毫秒的数据对于内部系统完全可以接受。4. 实操环节从零到一集成 univer 并跑起一个在线表格4.1 环境准备与依赖安装在开始之前先确认一下你要用的版本。目前 univer 的包名是univerjs/sheets、univerjs/core等注意跟早期的univer老包区分开。我选型时用的是 npm Vite 的 React 项目。安装依赖时建议直接这么装npm install univerjs/core univerjs/sheets univerjs/sheets-ui univerjs/ui univerjs/engine-formula univerjs/sheets-formula如果你是用的 Vue也能接因为渲染层是通过框架无关的命令/数据层调度的UI 层可以自己适配。装完后第一件事是引入样式文件。univer 的样式是需要主动加载的import univerjs/sheets-ui/lib/index.css; import univerjs/ui/lib/index.css;这个细节容易漏。我之前就是没引样式结果渲染出来是一堆无样式的 DOM 碎片排查了半天才发现是 CSS 没有进来。4.2 初始化一个最基本的工作簿初始化代码不算复杂核心是创建一个 univer 实例然后注册需要的插件最后指定挂载容器。下面是我这边跑通的最小示例import { Univer } from univerjs/core; import { SheetModule } from univerjs/sheets; import { SheetUIModule } from univerjs/sheets-ui; import { FormulaModule } from univerjs/engine-formula; const univer new Univer({ locale: zhCN, }); univer.registerPlugin(SheetModule); univer.registerPlugin(SheetUIModule); univer.registerPlugin(FormulaModule); univer.createUnit(UniverInstanceType.UNIVER_SHEET, { id: my-sheet-1, name: 销售数据, sheetOrder: [sheet-1], sheets: { sheet-1: { name: Sheet1, cellData: { 0: { 0: { v: 项目, s: { b: true } }, 1: { v: 一季度 }, }, 1: { 0: { v: A产品 }, 1: { v: 1200 }, }, }, }, }, });这里有一个容易看糊涂的地方cellData里的0和1代表的是行索引和列索引而不是 Excel 里的 A1 这种坐标。{ v: 项目 }表示单元格的值s是样式对象比如{ b: true }表示加粗。如果你觉得这种写法太啰嗦也可以先不传初始数据利用命令动态写入后面我会单独讲。4.3 通过命令动态写入和读取数据实际业务里你不可能每次初始化都把整张表的数据写死更多是表格先空着然后后端返回数据再填充。univer 里可以通过派发命令来写入单元格。以我的项目为例封装了一个简单工具函数import { SetRangeValuesCommand } from univerjs/sheets; function setCellValue(univer, row, col, value) { univer.getCommandService().executeCommand(SetRangeValuesCommand.id, { unitId: my-sheet-1, subUnitId: sheet-1, range: { startRow: row, startColumn: col, endRow: row, endColumn: col, }, value, }); }调用时传 0、0、张三就能把 A1 单元格设成张三。这种方式走的是标准命令管道天然支持撤销和协同广播。读取数据相对直接拿到当前工作簿实例后通过快照接口读 cellData 即可const workbook univer.getCurrentUniverSheet(); const snapshot workbook.getSnapshot(); const sheetData snapshot.sheets[sheet-1].cellData;读出来的数据就是标准的cellData对象方便你直接序列化存后端。4.4 常用能力清单公式、条件格式、下拉菜单比起简单写数据实际业务中更常用的是公式和交互功能。我挑几个真实点说一下公式sum、average 这些基础函数开箱即用只要在初始化时注册了FormulaModule。比如在 B1 单元格填SUM(A1:A10)引擎会自动计算条件格式通过ConditionalFormatting相关 API 可以设置大于某个值标红这类规则但配置项偏繁琐建议封装一层数据验证下拉适合做状态列配置 validations 的规则比较复杂我目前还在研究中单元格富文本p属性可以放进 cellData实现在一个格里显示多段不同样式内容整体来说基础能力覆盖度已经接近不少商业库但高级玩法还在逐步完善插件生态也在长。5. 二次开发进阶如何按业务口味给 univer 加上自定义菜单和工具栏5.1 工具栏扩展的入口在哪univer 的 UI 层是基于自己的渲染引擎做的不是普通 DOM 按钮。因此你想在工具栏上加一个按钮不能直接往 HTML 里塞元素需要走它提供的 UI 插件扩展机制。自定义按钮最常用的方式是注册 command。比如我要在工具栏加一个导出当前表格按钮先声明一个命令class ExportCommand extends Command { id custom.export; execute() { // 业务逻辑获取快照走接口导出 } }然后在初始化 univer 时把这个命令注册进命令注册表再通过 UI 层的 button 配置暴露在工具栏上。如果只是临时验证也可以直接在渲染出的工具栏 DOM 上用外部事件绑定但我不建议这么干。因为 univer 的 DOM 是自己绘制和重绘的外部绑定容易被刷新清掉还是走官方扩展点最稳。5.2 自定义右键菜单和快捷键右键菜单扩展是我做项目时觉得最实用的一个点。比如业务方要求右键某个单元格可以直接加备注。univer 的右键菜单支持通过菜单扩展项注册。你需要监听上下文变化当用户右键的位置落在有效区域时往菜单项里追加自定义动作。这个逻辑跟传统 DOM 右键菜单不太一样它需要你去重写onContextMenu的菜单项生成逻辑。快捷键则更简单一些注册一个 KeyCode 监听即可。比如我想让 CtrlEnter 自动提交当前单元格内容univer.getShortcutService().addShortcut({ key: KeyCode.ENTER, ctrl: true, commandId: custom.submit, handler: () { /* your submit logic */ }, });体验很好几乎零延迟。5.3 二次开发避坑心得真正走到二次开发这一步有几个容易踩的坑我先提前说不要试图直接操作内部渲染的 canvas 元素那是框架内部实现改动在刷新后会丢失所有数据的正确操作路径是命令不是直接改快照对象否则协同状态会不同步多实例时每个 univer 实例要用不同的容器 id避免挂载错乱样式文件版本一定要跟着包版本走混合版本会导致 UI 错位6. 常见问题与排查技巧实录6.1 白屏、黑屏表格渲染不出来这是新手最容易遇到的问题我排查过的案例里十有八九是样式没引全或者容器高度为 0。第一步检查你的挂载容器有没有设置高度。univer 不像普通表格会自动撑高它的 canvas 渲染需要明确的宽高。建议这样#univer-container { width: 100%; height: 600px; }第二步检查是否引入了必需的 CSS。如果没有只会有凌乱的 DOM 结构或干脆空白。注意univer 是 Canvas 渲染方案不是 DOM 表格。千万别用表格宽度自适应、高度根据内容撑开的思维去看它Canvas 容器必须主动规划尺寸。6.2 公式不计算显示不了结果有一次我跟同事联调发现填了SUM(A1:A5)但结果就是不出来。排查之后发现是初始化时漏了FormulaModule插件。公式引擎是独立模块不注册就算公式写在 cellData 里也只存储表达式字符串不会计算。还有一点单元格的值如果是字符串SUM(A1:A5)它会被当成文本如果想要走公式计算必须保证值是以开头且单元格类型不是字符串强制。6.3 协同状态下操作互相覆盖多人协同最烦的是你改的被他覆盖了。这个问题通常不是 univer 本身引起的而是服务端没有做版本校验。我建议你在设计服务端时给每个操作都加一个严格递增的版本号拒绝旧版本操作。客户端收到版本落后的返回后提示用户刷新最新快照再尝试重新应用本地变更。这样虽然操作上会多一点处理但至少数据一致性有保障。6.4 大数据量渲染卡顿别看 univer 是 Canvas 渲染数据量大了该卡还是会卡。我的经验是渲染和计算分两层看待。渲染层面它已经做了视口裁剪只绘制当前可视区域所以单纯滚动不会太拉胯。但公式计算和全量快照序列化是大头。建议设置合理的「计算触发策略」避免每次输入都触发整表计算公式重算数据持久化时也不要频繁全量快照尽量增量更新。如果你要一次性加载几万行数据建议采取分页加载或虚拟滚动策略配合 univer 提供的行高列宽配置来优化体验。6.5 卸载和重新挂载时的重复报错在 SPA 项目里这个坑很典型。页面切换再回来或者热更新之后发现 univer 容器里报一堆instance already exists之类的问题。原因就是 univer 实例没有正确销毁。你一定要在组件卸载时调用univer.dispose()把实例的监听、DOM、定时器全部释放干净。不然旧实例还占着容器新实例又挂上去自然冲突。7. 我的真实感受与下一步尝试说句实话univer 目前还谈不上完美替代微软 Excel毕竟它更偏前端集成方案复杂表格的兼容细节还有待打磨。但它的思路和底子我很看好模块拆分清楚、命令驱动彻底、协同路径清晰、渲染性能扎实。尤其对于想自己掌控在线文档体验的团队比直接接第三方收费服务更有长期竞争力。我自己在实际项目中已经把 univer 用在了内部工单管理系统的数据看板和在线排期表模块上。从最初的简单展示到后面加协同和自定义导出整个过程比预期顺利踩的坑大多在文档不全、API 变更频繁这两块。如果你也想试建议从小场景入手先跑通一个最简表格再把协同和自定义命令逐步加上去。版本升级时留意 changeloguniver 迭代速度很快API 变动在早期是常态别被吓到。最后分享一个小技巧遇到问题多去翻 univer 仓库的示例代码比看 API 文档管用得多。官方示例覆盖了大多数场景照着改造往往比自己瞎猜快很多。
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

◈

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

◐

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

▲

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。