☰
基于Univer打造在线表格:架构、公式引擎与协同落地实践
2026/11/3 6:29:05 网站建设 项目流程

熟悉前端表格方案的朋友应该都听过 Univer。我第一次注意到它,是因为团队要做一套带在线协同的行业报表工具,翻来覆去找了一圈开源方案,最后发现 Univer 不是又一款表格组件,而是一整套浏览器端的办公套件框架。这篇我打算把这一年来在生产环境里基于 Univer 做二次开发的思路和踩坑记录整理出来,包括架构怎么理解、一个在线表格怎么快速接入、公式与渲染在大数据量下的真实表现,以及协同和导入导出到底能做到什么程度。无论你是准备给项目嵌入一个可编辑表格,还是想从零搭建类 Excel 的在线应用,应该都能在里面找到直接能用的方案。

1. Univer到底是什么:一个被低估的开源Web办公套件

1.1 它的出身:Luckysheet团队的彻底重做

很多人第一次听到 Univer,是从 Luckysheet 迁移过来的。Luckysheet 当年能火起来,是因为它用 Canvas 重写了 Spreadsheet 的渲染,比 x-spreadsheet 那一类 DOM 方案的性能好不少。但用过的人也都知道,Luckysheet 的公式引擎、协同能力基本是"能用但不完整",代码层面的历史包袱越来越重。Univer 就是同一批作者在 2022 年前后重新立项的项目,相当于把旧思路整体推翻,用工程化的方式重新设计了一套面向 Web 的 Office 套件内核。

这里有个关键区别:Univer 从立项开始就不是"做一个 Excel 网页版",而是"做一个可以承载 Excel、Word、PPT 三种在线应用的框架"。所以它的核心包设计非常克制,几乎所有业务能力都靠插件扩展。对我来说,这意味着用它来做产品,而不是用它来应付临时需求——这是两件完全不同的事。

1.2 它到底能做什么:表格、文档、幻灯片与协同

我按下表格这条主路径来梳理能力,毕竟目前最成熟的就是 Sheets 这条线。

  • 单元格编辑、行列操作、合并单元格、冻结窗格、筛选排序
  • 超过 300 个公式函数,支持跨表引用和数组公式
  • 条件格式、数据验证、数字格式、富文本单元格
  • 图表、迷你图、数据透视表(透视表在持续完善中)
  • 打印设置、图片批注、单元格批注(Thread Comment)
  • Undo/Redo 和多级撤销栈
  • 实时协同编辑(客户端能力已开源,服务端需要配套建设)

如果只看表格,它已经覆盖了一个商用报表产品 90% 的交互需求。文档(Univer Docs)和幻灯片(Univer Slides)也在推进,但目前生产可用度远不如 Sheets。我的建议是:文档和幻灯片可以先观望,表格这一块已经值得投入。

1.3 横向对比:它和其他开源/商业方案差在哪

方案渲染方式自定义能力协同公式引擎授权
UniverCanvas插件化、命令体系完整支持(需服务端)自研,较强Apache 2.0
LuckysheetCanvas中等,耦合较重较弱依赖第三方MIT
HandsontableDOM强,但偏组件需配合插件弱商业授权
x-spreadsheetCanvas中等无无MIT
SheetJS/ExcelJS无界面只做解析无无商业

简单说,如果你只是要在页面上展示并允许用户编辑一个表格,Handsontable 或 x-spreadsheet 足够,学习成本也更低。但如果你要的是"嵌在自家产品里、带公式和协同的在线表格工作台",Univer 几乎是唯一在开源层面把自研公式引擎、渲染引擎和命令体系三者都做完整的项目。

1.4 适合谁用:三个我真实见过的落地场景

第一个场景是数据填报系统。之前有个金融项目,用户需要按 Excel 模板批量录入数据,过去是上传 xlsx 再解析,体验非常割裂。用 Univer 之后,直接把录入界面变成"内嵌的 Excel",录入校验、公式联动都是原生的。

第二个场景是报表设计器。业务人员拖拖拽拽生成报表模板,系统读取模板里的单元格结构和公式,再批量替换数据。Univer 的导出能力让模板和 Excel 文件之间能互相转换。

第三个场景是协同评审。多个人同时打开同一张表评审数据,需要看到别人正在改哪个单元格。这个场景对协同的要求不低,后面我会专门讲落地成本。

2. 从组件到框架:Univer的插件化架构是怎么组织的

2.1 包划分:core、sheets、ui、formula 各管一段

Univer 是一个 Monorepo,npm 包非常多,刚开始容易懵。我按使用频率把通常要接触的包分成几层:

  • 核心层:@univerjs/core,负责命令、服务、数据结构、IOC 容器、撤销栈等一切"表格无关"的底座。
  • 业务流程层:@univerjs/sheets,定义 Sheet 的数据模型、选区、单元格操作等。
  • 渲染交互层:@univerjs/sheets-ui+@univerjs/ui,负责渲染引擎封装、工具栏、右键菜单、弹窗。
  • 公式层:@univerjs/engine-formula+@univerjs/sheets-formula,负责公式解析、依赖树、计算执行。
  • 功能扩展层:条件格式、数据校验、筛选、排序、导入导出、协同,各有独立包。

这套划分带来的直接好处是:你可以只装core + sheets + sheets-ui做一个只读表格展示,不引入公式引擎和一堆编辑器功能;反之,你也可以在自定义渲染框架里只用 core 的数据结构。按需加载不是"宣传说法",是买包的时候真的能省体积。

2.2 命令、服务、门面:理解 Univer 的姿势

这是我觉得理解 Univer 最重要的一步。它内部有三层东西特别容易混淆:

  1. 命令(Command):用户每次操作,比如设置单元格值、插入行、撤销,都会变成一个命令对象。命令可复用、可记录、可分发,这也是协同能实现的基础。
  2. 服务(Service):无 UI 的业务逻辑单元。公式服务负责计算,命令服务负责调度命令,剪贴板服务管理复制粘贴。
  3. 门面(Facade API):对外暴露给业务用的简化接口,比如getActiveSheet()、getRange()。官方说明 Facade API 会长期保持稳定,而内部 API 可能随版本调整。

我踩过的一个认知误区是:以为 Univer 是"改配置就能干活"的组件。实际使用中,大多数业务需求不是写 UI,而是写命令或监听命令。比如用户改了某个单元格,你要同步到后端,正确的姿势是监听commandExecuted事件,而不是在 UI 组件里挂回调。命令流的这种设计虽然前期理解成本高,但后期做审核流、操作回放、协同同步都顺很多。

2.3 框架无关:React、Vue 还是原生都能用

Univer 的三方框架绑定很薄。官方给的示例确实常配 React,但那只是因为示例仓库用 React 写外壳;真正的渲染层是 Canvas,DOM 内部结构严格受控。也就是说,Vue、Angular、Svelte 甚至纯 JS 项目都可以把 Univer 作为"一块画布"嵌进去。

我在 Vue3 项目里接入时,只需要在onMounted里创建一个 Univer 实例,在onBeforeUnmount里调用销毁接口,不需要任何 Vue 插件。组件间通信全部通过事件和命令来解耦,这比很多宣称"双框架支持"的库都干净。

3. 30分钟接一个在线表格:初始化、配置与常用API

3.1 安装什么包

我建议从官方 preset 起步,它帮你组合好常用插件,避免自己去凑版本:

npm install @univerjs/core @univerjs/presets

如果你需要更细粒度控制,再按需安装:

npm install @univerjs/sheets @univerjs/ui @univerjs/sheets-ui @univerjs/sheets-formula @univerjs/engine-formula

需要留意的是,Univer 的版本推进很快,不同小版本之间插件注册 API 可能有变化。我的经验是:安装时锁定同一个具体版本,不要用^让多个包各自浮动,否则大概率会撞出"插件注册顺序不对""拿不到某个 service"这类问题。

3.2 最小宿例:新建一个能编辑的 Sheet

HTML 里给一个容器:

<div id="app" style="width: 100%; height: 100vh;"></div>

初始化代码:

import { Univer, LocaleType } from '@univerjs/core'; import { defaultSheetUniverConfig } from '@univerjs/presets'; const univer = new Univer( defaultSheetUniverConfig({ locale: LocaleType.ZH_CN, }) );

就这么简单,浏览器里会出现一个带工具栏、标签栏、公式栏的完整表格应用。defaultSheetUniverConfig会帮你自动注册核心插件、UI 插件、公式插件、导入导出插件等,省掉逐个registerPlugin的胶水代码。如果你需要在初始化时就带数据,可以在挂载级别把数据以 workbook 配置的形式传入,而不是等渲染完再逐格写入,后者在大数据量下会很慢。

3.3 常用配置项:语言、主题、皮肤和只读

UniverConfig和 sheets 的 config 能控制不少东西,我常用的几个:

  • locale:多语言,内置中英文等,也可以传自定义字典包做产品化词条。
  • theme:颜色变量,默认主题之外可以覆盖主色、背景、边框。
  • container:指定挂载 DOM;不传则用预设。
  • 只读模式:可以在外层把编辑类命令禁掉,比"CSS 禁用输入"可靠得多。

工具栏的增删不是纯配置项能解决的,官方推荐的方式是自定义 UI 插件。前期建议先用默认工具栏,确认交互细节后再裁剪。

3.4 业务接入中最常用的 API 操作

初始化之后,最常打交道的几个方法:

// 获取当前激活的工作簿和工作表 const workbook = univer.getActiveWorkbook(); const sheet = workbook.getActiveSheet(); // 写入一片区域(按行数组) sheet.getRange(0, 0, 10, 3).setValues([ ['编号', '名称', '金额'], ['A001', '服务费', 1200], ['A002', '咨询费', 800], ]); // 读取一片区域 const values = sheet.getRange(0, 0, 10, 3).getValues(); console.log(values);
// 监听命令,拿到用户的操作信息 univer.getCommandService().onCommandExecuted((command) => { if (command.id === 'sheets.command.set-range-values') { // 这里能拿到 params,包含 sheetId、单元格范围、新值 // 适合把变更推给后端或者做审计日志 } });

命令 ID 在每个版本里并不完全一致,正式接入时以自己打印出的实际 ID 为准。这不是偷懒,是因为这套东西还在快速演进,依赖内部字符串是脆的,最好做成映射表统一管理。

4. 公式引擎与Canvas渲染:大表格场景下的真实表现

4.1 公式引擎不是"套个JS算一下就完了"

如果只是SUM(A1:A10),任何脚本都能算。但一个在线表格的公式引擎要面对的是:单元格之间互相引用、循环依赖、跨工作表引用、数组公式、用户频繁修改单元格后只重算受影响的部分。Univer 自研的引擎核心思路是维护一张依赖图,每个单元格记录"我依赖谁、谁依赖我"。某个单元格值变化时,引擎只把依赖它的节点标记为脏(dirty),然后按拓扑序重算,而不是全表傻算一遍。

这带来一个很实际的好处:两张工作表之间存在大量跨表引用时,修改 A 表一个值,B 表关联单元格能秒级联动,不需要业务方自己写监听重算逻辑。公式里还支持类似LAMBDA()这类新式函数,对喜欢把复杂逻辑写进单元格的高级用户很友好。

4.2 Canvas 渲染能撑住多少数据

Univer 的表格渲染完全在 Canvas 上进行,DOM 只保留很少的交互锚点,滚动、缩放都是重绘画布,不触发布局抖动。官方宣传和社区实测里,几十万行的静态数据可以在初始化"不逐格写入"的前提下正常滚动浏览。至于编辑密集场景(高频 setValue),主要瓶颈反而在命令链路和重算上,不在绘制本身。

我自己压过一组数据:6 万行 × 8 列,带 10 个公式列。初始化时把 value 数组一次性塞进 workbook JSON,首屏渲染大约 1 秒上下(和具体机器有关),滚动丝滑,公式列修改单个单元格后重算范围可控。这个表现远好于我此前用 DOM 方案实现的同类功能——后者在 2 万行时滚动就已经明显掉帧。

4.3 调优经验:别在渲染完成后逐格写数据

最容易踩的坑是:Univer 渲染完成后,用getRange(r).setValue(v)循环 6 万次往里塞值。每次 setValue 会产生一条命令、进一次撤销栈、触发一次相关区域刷新,几万次下来必然卡死。正确做法是:

  1. 后端把数据组织成 workbook JSON 或大块二维数组;
  2. 在new Univer(config)时随初始数据结构传入;
  3. 后续业务更新尽量用setValues的批量范围写入,而不是单格循环;
  4. 不需要撤销的场景,可以临时关闭撤销栈或合并成一次命令。

另外,如果页面里同时存在多个 Univer 实例(比如标签页切换),务必管理好生命周期:切走时销毁实例,而不是 hide 掉。Canvas 持有 GPU 和内存资源,隐藏不销毁会让整个应用的帧率和内存双双恶化。这也是我反复叮嘱前端团队的一句。

5. 在线协同不是送外卖:Univer协作能力的落地边界

5.1 协同到底要解决什么问题

协同编辑的核心不是 WebSocket 推送,而是"并发修改冲突怎么合并"。两个用户同时编辑同一个单元格:A 输入 100,B 输入 200,最终结果应该是什么?如果只是简单按后到者覆盖,体验会有大量错乱。所以需要操作转换(OT)或 CRDT 这类算法,对不同用户的操作做等价变换,保证每个客户端的本地结果最终一致。

Univer 之所以命令体系设计得重,正是因为它希望在协同场景下复用同一套"操作记录"。每个命令都带有可序列化的参数,正好可以作为 OT 的操作单元。看完它的实现思路后我有一个很强烈的感受:很多产品做多人在线表格,最难的不是打开 WebSocket,而是你根本没有"操作"这个东西的通用抽象。Univer 从源头把这一步做掉了。

5.2 开源部分和商业部分的边界在哪

根据我使用时的版本情况,Univer 官方把协同的客户端能力逐步开源,比如记录命令流、应用远端操作、处理快照恢复等;但完整的协同服务端(负责连接管理、操作转换、文档持久化、版本存储)属于商业版 Univer Pro 的一部分。也就是说:

  • 开源:客户端能生成操作、能应用操作,你能看到协同的整体数据流;
  • 商业:服务端一站式解决,包括房间、鉴权、落库、可靠推送;
  • 自建:你也可以自己写服务端,用官方客户端能力 + 自研 OT 服务。

对中小团队来说,"自研 OT 服务端"的成本被低估了。它不只是转发消息,还要做操作版本管理、缓冲、乱序处理、历史回溯。如果没有多人同时编辑同一文档的强需求,我的建议是先做"文档锁 + 操作广播":同一时刻只允许一人编辑,其他人实时看到同步渲染,这是成本最低且能规避大量冲突的过渡方案。

5.3 自建协同服务的最小闭环

假如你确定要走自建,核心链路大致是:

  1. 前端把每次命令序列化,通过 WebSocket 发给服务端;
  2. 服务端为文档维护一个递增版本号和操作队列;
  3. 对新到达的操作做冲突检测和转换(至少要有基于操作位置的变换逻辑);
  4. 把转换后的操作广播给房间内其他客户端;
  5. 客户端用 Univer 提供的应用远端操作接口把变更合入本地。

客户端只负责"产生操作"和"应用操作",业务的服务端需要自己处理鉴权、断线重连、全量快照与增量操作的持久化策略。首次快照建议在文档首次创建时全量存一份,之后每次协同操作增量追加;快照与增量之间要能根据版本对齐。

这一步如果走通,后面扩展评论、光标位置、在线用户列表这些功能都只是锦上添花。但如果只是想让表格"能多人看",千万别急着上协同,先用只读共享 + 手动刷新观察模式,成本和风险都小很多。

6. 导入导出与数据打通:xlsx、csv和样式保真的那些事

6.1 官方能力与第三方解析的区别

很多项目真正需要的不是协同,而是"把用户手里的 Excel 导进系统,再把结果导回 Excel"。Univer 官方提供了 import/export 相关插件,底层会结合 SheetJS 等解析库,把 xlsx 文件解析成 Univer 的 workbook 数据模型,再走初始化或命令注入。

如果你只用 SheetJS 自己解析 xlsx,再调用 Univer 的 setValues 填数据,也能跑,但有几个点会丢:合并单元格信息、条件格式、数据校验、数字格式、列宽行高。官方导入插件会把更多结构信息映射到数据模型里,所以"从文件到在线表格"的保真度,官方路径明显更高。

6.2 样式保真度:哪些能还原,哪些会丢

我实际导过不少带格式的 Excel,结论如下:

内容类型还原程度备注
基础样式(字体、背景、边框)高绝大多数能还原
数字格式(日期、百分比、自定义)高特定自定义格式串可能降级
合并单元格高
单元格批注较高版本不同表现有差异
图片/浮动元素中图片位置可能偏移
条件格式中复杂公式规则可能简化
数据透视表低官方能力还在完善,建议绕道
图表中简单图表可迁移,复杂图表不保证

所以如果业务文件里有大量透视表、复杂条件格式,别指望"原样打开原样导出"。大多数报表产品实际是"模板在 Univer 里做,数据在系统里填,导出为 Excel 交付"的模式,而不是把 Excel 当唯一真源。

6.3 大文件导入的优化

几百 MB 的 xlsx 直接塞进浏览器,解析和渲染都会卡。我的处理方式:

  1. 服务端负责解析,把 xlsx 转成轻量的中间 JSON(去掉不必要的历史版本、外部链接);
  2. 前端拿到 JSON 后直接作为 Univer 初始数据结构,绕开浏览器端 xlsx 解析;
  3. 如果必须前端解析,用 Web Worker 跑 SheetJS,解析完再 postMessage 回主线程;
  4. 渲染层分批显示:先注入前 N 行可见区域,滚动时再补数据。

这里的思路跟前面性能部分一样:数据注入要尽量往前走,能服务端做就不在前端做,能批量就绝不循环。

7. 我踩过的坑和推荐的生产级配置

7.1 版本锁定的教训

Univer 的发布节奏很快,包与包之间严格同版本才稳定。我见过最典型的报错是locale is not defined或者Can not find ${service} from IOC container。查半天,最后发现是@univerjs/core和@univerjs/sheets差了一个 patch 版本,IOC 容器里的服务定义对不上。所以从第一天开始就在 package.json 里锁死统一版本号,升级时全家桶一起升,别单独升某一个包。

7.2 CSS 覆盖的边界:DOM 不是你想的那样

Univer 的 UI 部分(工具栏、右键菜单、弹窗)确实是 DOM,可以加样式覆盖,颜色变量也可以自定义;但表格区域是 Canvas,你没法用 CSS 去改单元格内部的东西。想改变单元格展示逻辑,要走到渲染层插件或数据层配置去。这一点如果不提前告诉 UI 同事,他们会在 Canvas 上反复写 CSS 然后来问你"为什么没反应"。

7.3 实例生命周期管理

页面里有多个工作簿、或者做单页应用路由切换时,一定要在组件卸载时销毁 Univer 实例。之前在一个后台系统里,用户每打开一次报表就 new 一次 Univer,关掉报表却不销毁,结果内存一路涨,界面越来越卡。Univer 自身提供了销毁方法,把它绑到页面路由的对应生命周期里,是必备的工程习惯。

7.4 权限控制不能靠前端

在线表格的权限(谁可以编辑哪些单元格)一定要放在命令层和服务端校验去控制。前端只把"禁编辑"当用户体验优化。道理很简单:任何前端能判断的规则,懂一点开发的人都能绕过。Univer 提供命令拦截的能力,但这只是客户端的门禁,服务端必须有操作合法性校验,协同场景尤其如此。

7.5 推荐的生产级组合

最后叠一层我目前觉得比较稳的组合:

  • 表格:Univer Sheets + 官方预设,锁版本统一
  • 渲染:默认 Canvas,数据初始化用大块注入
  • 导入导出:官方导入导出插件,服务端解析大文件
  • 协同:前期先做"文档锁 + 操作广播",强度上来再评估自研 OT 服务端
  • 权限:命令层做前端收口,服务端做最终校验
  • 监控:监听命令流,记录操作日志,配合业务审计

这套组合在表格能力、开发效率和可控成本之间比较均衡,也是我目前会推荐给团队的方向。

文章写到这里,Univer 在我项目里的主线体验基本都覆盖了。最后再说一个让我印象深刻的点:初次接触时觉得它的插件体系太重,习惯了之后反而觉得这才是办公套件该有的骨架——把命令、服务和渲染分开,业务才能在上面持续堆功能而不失控。如果你也在评估开源表格方案,建议先拿一个真实业务的模板文件跑通导入导出,再决定要不要往深处走。表格类产品最大的风险从来不是新特性不够炫,而是数据和格式的保真细节撑不住真实业务;Univer 至少给了这个层面一个很好的起点。

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

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

立即咨询