1. 初见 Univer:为什么说它不是又一个在线表格 Demo
第一次刷到 Univer 的时候,我以为是又一个在线表格 Demo。但在自己的业务里试跑两个月后,我改主意了:它不是玩具,而是一套可以承载在线协同办公场景的前端基座。Univer 最抓人的一点,是所有渲染全部走 Canvas,表格、文档、幻灯片三种场景共用一套核心模型,这在开源项目里相当少见。如果你只是需要一个表格展示控件,Univer 可能不是最轻的选择;如果你要做的是嵌入到 SaaS 里的在线 Excel,或者一套带协同的文档产品,那它值得你花几天时间研究。
我最早接触这个项目是因为团队要做“类 Excel 的审批数据明细页”,传统表格库在 500 行以内还能凑合,一旦上千行、十几列冻结、公式实时联动,整个页面就开始发飘。当时搜了一轮,发现国内讨论最多的是 Luckysheet,国外是 Handsontable,但真正想找的“体验接近 Excel 而不是接近表格组件”的开源方案一直没定下来。直到有人把 Univer 的仓库丢给我,我看完仓库里的在线演示才意识到,这个项目想解决的问题比“一个表格控件”要大得多。
1.1 官方在线版给了我什么第一印象
搜 univer 的时候大概率会看到“univer在线”这个词。这是官方提供的在线体验站,打开就是一套能编辑的 Excel 界面,可以直接输入公式、合并单元格、调整样式、做数据透视。对技术选型阶段的人来说,在线版比看文档更能快速建立直觉。我第一次用在线版主要验证了三件事:交互跟不跟手、公式支不支持、导出还靠不靠谱。测完这几个问题,才决定下载源码往项目里集成。
这里必须提醒一句:官方在线版是一个“产品化完整版”,里面有些能力不一定等于你本地开源包开箱即用的能力。比如在线版可能有官方托管的协同后端,而你从 npm 装下来的只是一堆前端库,协同服务端还是得自己解决。所以评估 Univer 千万别只看在线演示,一定要把核心包装进自己的项目里跑一遍,才知道本地集成后能拿到多少。
1.2 Univer 的核心定位
从技术角度讲,Univer 是一套基于 TypeScript 的办公文档套件。它不只覆盖 Sheet,还规划了 Doc(文档)和 Slide(演示),而且三种文档类型共享同一个核心框架。这个定位很容易让人联想到“开源版的 Office 前端内核”。其工程结构被拆得非常细:@univerjs/core管数据模型,@univerjs/engine-render管渲染,@univerjs/engine-formula管公式计算,@univerjs/sheets-ui管表格交互,每一层都能按需注册。
这种拆分方式对使用者来说好处很明显:你不需要把整个 Office 套件都拖进页面。只做表格就只注册表格相关插件,后续想要文档能力再加对应的单元类型。坏处也很明显——第一次接触的人会被一堆 npm 包吓到。我当时看到一个最简单的示例要装六七个依赖也很头疼,但跑通之后再回头看,这些包之间的边界反而让问题排查变得清楚了。
1.3 和 Luckysheet、SpreadJS 放在一起看
很多人谈到 Univer 都会和 Luckysheet、SpreadJS、Handsontable 对比。我自己的感受是,它们其实不完全在一个赛道上。Spark 表格控件类的工具更偏向“编辑控件”,Univer 更像“文档内核 + 可扩展渲染平台”。为了直观,我列一个粗颗粒度对比:
| 方案 | 渲染方式 | 开源程度 | 文档协议完整性 | 二次开发难度 |
|---|---|---|---|---|
| Luckysheet | Canvas 渲染,曾广泛用于 H5 表格 | 部分开源 | 公式、基础样式较完善 | 中,但社区活跃度起伏较大 |
| Handsontable | DOM 渲染,社区版功能较多限制 | 非商业开源 | 以表格编辑为主,Excel 协议较弱 | 低,主要在使用层面 |
| SpreadJS | Canvas 渲染,商用授权 | 闭源 | 非常完整,Excel 兼容性强 | 中高,受制于闭源但要省事 |
| Univer | Canvas 自研渲染引擎 | 开源 | 正在快速补齐 Sheet/Doc/Slide | 高,但扩展空间也最大 |
我的判断是:如果项目预算充足、只求 Excel 兼容性稳定,SpreadJS 依然是可靠选择;如果团队有前端架构能力,想长期打磨一套嵌入式编辑产品,Univer 的开放性更值得投入。它可能不够成熟,但它的架构方向是对的,很多东西可以从社区里借力。
2. 架构拆解:Canvas 渲染、命令机制和“一套核心覆盖三种文档”
Univer 之所以让我觉得值得深入,是因为它没有简单复刻“表格组件”,而是从架构层把办公文档的共性问题抽象了。这一章我拆几个关键点,讲清楚它为什么会在性能、扩展性和协同方面具备想象空间。
2.1 渲染层和逻辑层的分离
最核心的设计是“逻辑与渲染彻底解耦”。Univer 的表格单元格、行列头、选区状态都不依赖 DOM,而是存在一套可序列化的工作簿模型里;渲染器拿到模型后,用 Canvas 把视口内的内容画出来。这样做最直接的原因就是性能。一个 5000 行 × 200 列的表格如果用 DOM 渲染,几十万个节点足以让浏览器崩溃,但 Canvas 只需要画用户当前能看到的几十行几十列。
代价也很明显。所有文字测量、光标定位、选区绘制、边框线渲染都得自己实现。我在使用中也观察到,如果字体加载时机不对,Univer 绘制出来的文字宽度会计算错误,整个单元格内容都会错位。这就是典型“用 Canvas 换性能”带来的复杂度,它把浏览器的排版系统拿掉了一半,剩下的一半需要你自己补。
Univer 把这块叫engine-render,内部有场景(Scene)、对象(Object)、视口(Viewport)的概念。滚动表格时实际上是在移动视口的坐标变换,而不是更新每个 DOM 节点的位置。设计上参考了游戏引擎的思路,所以首帧绘制和连续滚动的表现都比老的 DOM 表格方案平滑得多。
2.2 插件化的边界在哪里
Univer 的插件体系不是简单的外挂,而是贯穿整个生命周期。注册UniverSheetsPlugin后,模型才具备Workbook和Worksheet的能力;注册UniverSheetsUIPlugin后,界面工具栏、右键菜单、选区和编辑器才会出现;不注册公式引擎插件,输入=SUM(A1:A2)只会被当成普通字符串存进单元格。
这种设计带来的好处是业务集成时可以按需裁剪。我在实际项目里只注册了表格和公式插件,把不需要的底栏、缩放按钮等 UI 全部关掉,确实减少了不少不必要的视觉噪音。但坏处是,插件之间的依赖关系需要理顺。官方在文档里会标注每个插件依赖哪个基础包,如果你跳过依赖直接注册,运行时会报一些很隐晦的错误,比如Cannot read properties of undefined,多半是因为底层插件没注册。
另一个值得注意的是,插件体系让二次开发有了明确入口。团队可以自己写一个“审批状态列插件”,或者做一个“批量导入校验插件”,把业务逻辑放在独立的模块里,而不是直接改 Univer 源码。这对长期维护来说价值非常大。
2.3 为什么命令系统是协同的前提
Univer 中对工作簿的所有修改都统一走命令(Command)机制。比如编辑一个单元格、插入一行、合并区域、设置公式,最终都会变成一个命令对象,由命令系统执行后更新模型。这个设计在单机场景下看起来多此一举,但对协同编辑来说是救命稻草。
协同编辑最怕的就是“各改各的,最后合并不了”。如果所有操作都通过命令广播给远端,那么服务端只需要处理命令的排序、冲突合并和重放,就能让多端看到一致结果。Univer 的数据模型天然适合序列化成命令流,协议上可以做 OT 或 CRDT 的改造。虽然官方开源版不直接提供完整的协同服务端,但客户端已经把最难的那部分——可序列化状态和命令通道——给做好了。
我们团队当时的做法是:用 WebSocket 把自己的命令数据包转发给后端,后端按文档 ID 排序后广播给所有在线客户端。冲突解决最初用了简单的时间戳 + 版本号,后来复杂区域合并场景多了,才逐步接入更完整的 CRDT 逻辑。Univer 命令系统让这个演进过程没有推翻重来,这是它和传统表格控件最大的区别。
3. 从零跑通:把 Univer 塞进一个 Vite 项目
理论说得再多,不如跑一个最小示例。Univer 版本迭代比较快,我下面写的是测试过的思路,具体 API 名以你安装的版本为准,但整体流程基本不变。
3.1 安装依赖
先建一个普通的 Vite + TypeScript 项目,然后安装核心包:
npm i @univerjs/core @univerjs/sheets @univerjs/sheets-ui @univerjs/ui @univerjs/design @univerjs/engine-render @univerjs/engine-formula如果版本里还有@univerjs/engine-numfmt,也建议一起装上,数字格式、日期格式处理都会用到。这个依赖清单看起来长,但每个包职责清晰:core是数据模型,sheets是表格逻辑,sheets-ui是表格界面,engine-render是渲染器,engine-formula是公式计算器。不要试图省掉engine-render,没有它 Canvas 根本画不出来。
安装时最容易踩的坑是 peerDependencies 版本不一致。如果你在项目里已经用了某个 React 或 Vue 版本,npm 可能会报Could not resolve dependency。这时候推荐用npm install --legacy-peer-deps,或者直接用 pnpm 的resolution-mode=time策略锁定一个互相兼容的版本组合。
3.2 最小启动代码
以下示例代码基于 0.4.x 时代的结构,跑通一个空白表格:
import { Univer, LocaleType } from '@univerjs/core'; import { defaultTheme } from '@univerjs/ui'; import { UniverSheetsPlugin } from '@univerjs/sheets'; import { UniverSheetsUIPlugin } from '@univerjs/sheets-ui'; import { UniverFormulaEnginePlugin } from '@univerjs/engine-formula'; // @umdjs/design 等样式文件按官方引入 const univer = new Univer({ locale: LocaleType.ZH_CN, theme: defaultTheme, }); univer.registerPlugin(UniverSheetsPlugin); univer.registerPlugin(UniverSheetsUIPlugin); univer.registerPlugin(UniverFormulaEnginePlugin); // UniverSheet 是表格单元的类型标识,不同版本从 core 或 sheets 导出 univer.createUnit(UniverSheet, { sheets: [ { id: 'sheet1', name: 'Demo', rowCount: 1000, colCount: 100, cellData: { 0: { 0: { v: 'Hello Univer' }, 1: { v: '表格' }, }, 1: { 0: { v: '=SUM(1,2)' }, }, }, }, ], });这段代码有两条关键经验。第一,注册插件必须在createUnit之前,否则新建的 Sheet 实例可能缺少对应的模块能力。第二,公式引擎插件如果不注册,=SUM(1,2)这类的值不会真正参与计算,渲染结果就是字符串文本。我第一次测试时就是忘了注册公式插件,发现公式不生效,排查了大半天。
另外,容器必须有明确的宽高。Univer 会在初始化时读取父容器尺寸,如果父元素高度是 0 或者display: none,画布会白屏或只显示一条缝隙。建议在组件挂载且容器尺寸稳定后再调用初始化代码,或者监听ResizeObserver后调用二次布局。
3.3 写入数据的正确姿势
实际业务中很少会像上面那样静态写死cellData,更多是运行中动态写入。需要养成“发命令”的习惯,而不是直接改workbook对象。Univer 官方把常用操作都做成了命令,例如设置单元格值、设置合并区域、插入行列。你可以从@univerjs/sheets里找到相关命令类,然后通过命令服务执行。
这样做的好处是撤销重做可以直接复用命令历史栈。如果你的业务有“Ctrl+Z 撤销”的需求,只需要把命令对象推入历史管理器即可。如果直接改模型属性,撤销时就要自己写一套 diff,维护成本极高。
如果只是想在业务侧监听表格变化,不一定要主动发命令。Univer 的命令服务支持全局监听:
univer.getCommandService().onCommandExecuted((command) => { if (command.id === 'sheet.command.set-range-values') { // 表格单元格内容变化,更新业务侧缓存 } });这里注意不要频繁在回调里做复杂计算。Canvas 渲染和命令执行是高频动作,回调里如果有同步的 JSON 深拷贝,页面很快会出现卡顿。我的做法是先把变更批量收集到队列,再用requestAnimationFrame合并刷新业务页面。
3.4 用“univer在线”验证本地功能差异
集成过程中建议把官方在线版当作“标尺”。每实现一个本地功能,就去在线版点一遍,确认默认行为是什么,再对比自己项目里的表现。我踩过印象最深的一个差异是导入 xlsx 后的样式还原度:在线版能保留大部分单元格宽度、字体、边框,但本地集成如果不引入额外的导入解析配置,某些合并单元格的边界会有偏差。
这个差异不一定是 Univer 开源版缺功能,更可能是你没有配置对应的导入插件。官方仓库为了保持核心库精简,把 xlsx 解析归到了扩展包。所以遇到“在线版可以但本地不行”的情况,先别急着怀疑,去看文档里是不是有额外的sheet-import-xlsx类插件没注册。
4. 公式引擎、协同数据流和渲染刷新:深入观察 Univer 的几个底层机制
跑通 Demo 之后,我开始关注更底层的运作机制。Univer 不是一个把公式算完就画出来的简单流程,它的公式引擎、协同通道和渲染刷新之间有非常明确的分工。理解这三者关系,才能在复杂业务里准确诊断性能问题。
4.1 公式引擎独立的意义
@univerjs/engine-formula是一个相对独立的计算模块。它维护了公式的依赖图,某个单元格公式引用了其他区域时,依赖关系会被记录。当被引用的单元格变化,公式引擎只需要重算受影响的节点,而不是全表扫描。这种设计类似 Excel 的 Calc Chain,在大型表格里能显著减少无谓计算。
实际测试中,我在一个 2000 行 × 20 列的区域里写了大量SUM和IF公式,初次计算会有几百毫秒的等待,但修改其中一个源数据后,后续更新只重算关联单元格,反应很快。公式引擎做得好不好,直接影响用户在编辑大表时“打字有没有延迟”。Univer 这块的完成度已经能支撑日常编辑场景。
如果你要做公式扩展,可以自己注册自定义公式到公式引擎里。官方提供了注册函数和参数定义规范。我试过写一个“提取工龄等级”的自定义函数,过程比想象中顺利:引擎负责运行时调度,UI 层的公式输入提示也能自动识别。
4.2 协同编辑不是官方集成的终点
很多人在官网看到“协同”两个字,误以为装了 npm 包就能直接多人在线编辑。实际上开源版更侧重于提供“可协同的客户端内核”,完整协同服务还是需要业务方自己搭建。核心逻辑是:所有操作都已经是命令了,你要做的是把命令包传输、排序、冲突合并做好。
我在设计协同方案时,把一条命令当成一个最小传输单元。当用户在 A 端输入一个值时,本地不马上提交模型,而是发到服务端,服务端打上版本号后广播给包括发送端在内的所有人,然后各端按版本序执行命令。这里最大的坑不是命令序列化,而是“选区状态同步”。A 端用户正在选择的区域,在 B 端如果看不到,协同体验会非常割裂。Univer 客户端有选区管理器,你需要在协同协议里额外传输 Selection 状态。
如果团队没有专门的实时协同工程师,我建议初期先做“文档锁版本管理”,也就是编辑时锁定整张表或区域,提交后解锁。这虽然降低并发度,但远比做一个不成熟的 CRDT 方案稳得多。等用户量上来,再逐步让锁粒度变细,改造成多用户区域协同。
4.3 监听数据变更的正确姿势
前面提到了onCommandExecuted监听,这里补充一个容易忽略的细节:命令执行有 before 和 after 两种时机。如果你在 before 阶段读取模型更新后的值,很可能读到的是旧快照;在 after 阶段读取则数据已经落库。业务侧订阅数据变化,一定要挂到 after 或者专门的数据变更事件上。
另一个经验是:不要用“监听所有命令”的方式来维护业务状态,除非你只是想打个日志。全局监听会导致每次光标移动、选区变化都会触发你的监听回调,产生大量无效计算。建议只监听你关心的具体命令 ID,例如设置单元格值、插入删除行列、合并单元格。在回调里再做一次类型判断就能把无效调用挡掉一大部分。
渲染刷新和公式计算之间也有时序关系。Univer 执行一次命令后,通常先更新模型,再触发公式引擎计算,最后才由渲染引擎标记需要重绘的区域。如果三者之间有较长异步链路,极端情况下可能出现“单元格值变了,界面还没刷新”的瞬时状态。我遇到后不是去改源码,而是在模型更新后调用一个refresh方法,强制渲染器重绘一次,体验上会好很多。
5. 真正接生产时才遇到的坑:容器尺寸、字体测量、性能、国际化
项目从 Demo 走到生产环境,才能看到一套框架的另一面。Univer 在示例项目里跑得很顺,一接入业务开始暴露各种边界问题。我把自己踩过的几类坑整理出来,希望能帮你少走弯路。
5.1 容器尺寸:Univer 最经典的“白屏”问题
我遇到过不止一个同事在集成时发现页面白屏,最后定位出来的原因都不是代码写错,而是容器尺寸不对。Univer 在初始化时对父节点进行尺寸探测,如果父节点是display: none、高度为 0、或者正在某个动画过程中,渲染器拿到的可用区域就是 0,画布自然画不出内容。
解决方案有两个。一个稳妥的办法是在容器完全可见后再初始化 Univer,例如页面弹窗打开后延迟几百毫秒执行创建逻辑。另一个更动态的方法是监听容器尺寸并主动触发渲染器的 resize。Univer 的 UI 内部有 ResizeObserver,但业务侧如果在框架层加了transform: scale()之类的样式,内部 Observer 可能感知不到的实际绘制尺寸变化,这时候就需要你在业务代码里手动调用一次布局刷新。
我在组件里写了一个工具函数:每 500 毫秒检测一次容器getBoundingClientRect().height,如果连续两次超过 100 且和上次不同,就触发 renderer 重新测量坐标。这个方法虽然不算优雅,但很管用。
5.2 字体加载引起的文本错位
Canvas 绘制文本不像 DOM 那样可以自动回流。Univer 需要计算每个字符在画布上的宽度,然后决定单元格内容显示、省略号位置、列宽自适应。如果自定义字体还没加载完成就开始绘制,文字宽度会按默认字体计算,画出来就会互相重叠或者被截断。
解决思路是在字体加载完成后再刷新渲染。标准 API 是document.fonts.ready.then(...),需要在这个 Promise 里拿到自定义字体族名。如果你的业务字体来自 CDN,推荐用FontFace对象手动加载,加载完成后再初始化 Univer。
另一个相关坑是字体监测。Univer 内部可能通过ctx.measureText来算文本宽度,但这个 API 对字体是否加载完成很敏感。建议在样式表中预加载所有有用到的字体文件,避免运行到一半突然加载导致整体布局跳动。
5.3 数据量大时,关掉还没用到的能力才是第一优化
很多人一聊性能就问“Univer 能撑住多少行”,我反而觉得先问“你不需要哪些功能”更重要。Canvas 渲染本身能撑过很多 DOM 方案,但如果公式、条件格式、数据验证、自动筛选全部打开,再大的渲染引擎也架不住业务方各种叠加。
我处理过一个线上卡顿案例:表格只有 3000 行,但每一行有十几个条件格式规则,且公式引用了整列。每次输入一个值,条件格式要重算一遍,公式引擎也被触发,页面交互从流畅变成顿挫。后来我们把范围改小,把条件格式从整列引用改成实际数据区域引用,帧率立刻回来了。
Univer 也提供了一些关闭开关,例如隐藏行标题、关闭网格线、取消自动换行。如果业务场景是纯数据录入,这些视觉特性全部关掉能省不少计算量。在创建 Sheet 单元的时候,要养成“按需开启”的习惯,而不是把默认功能一股脑全打开。
5.4 国际化和日期格式
Univer 支持多语言标签,但业务侧不只要设置界面语言,还要处理数据格式的本地化。比如中文环境里日期显示是2025年4月1日,英文环境是4/1/2025,货币符号和数字分位符也不同。如果不配置数字格式引擎,数据会按照底层存储值直接渲染,很容易出现一万显示成 10000 而不是 10,000。
我在使用中建议把locale配置从后端接口下发,不要写死在客户端。因为表格本身可能被嵌入到不同租户的服务里,用户语言是动态的。首次进入页面时,先请求用户配置,再初始化 Univer,这样公式名称、菜单文案、格式解析规则才能保持一致。
还有一个很容易被忽略的点:导入 xlsx 时,文件里可能会显式设置单元格格式,例如yyyy/mm/dd。如果本地没有注册数字格式插件,导入后这些格式会丢失或退化成普通字符串。所以线上问题往往不是“Univer 不支持”,而是“你没有把对应扩展能力带给它”。
6. 选型经验:什么项目我会用 Univer,什么项目我会绕开
最后聊一点偏决策层的体会。Univer 确实是个好项目,但它不是万能药。选型前建议先拿一张项目需求清单,逐条对照,能省去很多后续的痛苦。
6.1 适合的场景
第一类是嵌入式在线表格编辑。典型场景是 SaaS 产品里的预算录入、审批表单、数据批量修正页。用户需要的是“点开就能编辑,体验尽量像 Excel”,同时又不可能跳转到外部表格软件。Univer 的 Canvas 交互和公式能力,比传统表格组件更贴近这个诉求。
第二类是有较强二次开发需求的团队。Univer 插件机制、命令机制和数据模型的可扩展性,让业务方可以在不改内核的前提下增加自定义单元格类型、自定义公式、自定义右键菜单。我的一些“审批状态”“项目关联”功能,都是通过扩展命令和单元格渲染实现的。
第三类是想要“统一办公套件”体验的客户端产品。如果未来产品规划里既有表格又有文档,甚至还有演示文稿,Univer 一套核心覆盖三种文档的思路,比分别接三个独立库更利于技术栈统一。
6.2 不建议的场景
如果你的需求只是在一个后台页面展示一张只读报表,完全不需要编辑公式,那我建议别用 Univer。纯展示场景用 Canvas 是杀鸡用牛刀,维护成本反而高。一个静态表格用 Ant Design Table 或普通 HTML Table 就能解决,还顺便解决了无障碍访问。
如果团队以业务开发为主,没有专门的算法或实时协同工程师,也要谨慎。Univer 的深度集成需要面对 Canvas 渲染、公式引擎、插件依赖、状态同步这一系列复杂问题。不是它不好,而是你们没有足够余力去驾驭它。
6.3 自我评估清单
我在项目启动前整理过一张评估表,分享出来供参考:
| 问题 | 如果答案是“是” |
|---|---|
| 用户需要在页面里直接编辑和公式计算吗? | 是,Univer 的优势才能发挥 |
| 你会大量定制单元格样式或行为吗? | 是,插件机制值得投入 |
| 你需要协同编辑吗? | 是,确认有人手搭建服务端 |
| 你的表格只是展示型报表吗? | 是,赶紧换轻量方案 |
| 你希望快速上线、不折腾底层吗? | 是,商业闭源库可能更合适 |
每个问题背后都是取舍。Univer 适合愿意接受学习成本、长期打磨产品的团队,不适合所有项目都硬上。
6.4 我的真实体会
最后说一点个人感受。Univer 把“擦地板”的活干完了——Canvas 渲染、命令注册、公式引擎、插件加载都是已经铺好的地基。但“怎么装修”还是要自己来:业务数据模型怎么映射到工作簿、协同协议怎么设计、自定义命令怎么和现有系统打通。这些事没有任何一个开源项目能替你做。
如果让我重新选型一次,我依然会用 Univer,但我会更早做一件事:先把项目里的文档数据抽象成“操作流”,而不是把界面状态和业务数据混在一起。这个习惯让我的团队在后来接入协同、做权限审计、加撤销重做时都轻松了很多。Univer 的价值不只在于它渲染出来的界面,更在于它逼着我们用更工程化的方式思考在线文档产品。