ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

Univer深度解析:打造企业级Web表格组件的架构与实践

Univer深度解析:打造企业级Web表格组件的架构与实践

这些年做前端基建和低代码平台,我一直在找一个能真正嵌进业务系统里的表格组件。试过用iframe套在线文档,交互割裂、二开困难;试过用企业微信和钉钉的表格API,数据都进了别人家的系统;也试过各种纯前端表格库,要么太重,要么缺了协同和公式能力。直到我深入研究Univer后,思路被彻底打开了——它是一个开源的全能办公套件,能同时处理电子表格、文档和幻灯片,而且核心层是纯粹的API和数据模型,UI层完全可替换。简单说,Univer要解决的不是“做一个好用的Excel”,而是“让任何Web系统都拥有一整套办公基座”。

这篇文章我会从选型背景、底层架构、企业真实集成、性能调优、协同对接、二次开发到踩坑记录,全方位拆解Univer。适合正在做在线表格、无代码平台、数据分析工具、协同办公产品的技术人员,也适合想给自己的产品嵌入复杂表格能力的产品负责人。如果你正在犹豫选Luckysheet、OnlyOffice还是SpreadJS,这篇内容应该能帮你想清楚。

1. 先聊选型:为什么押注Univer

1.1 市面上的开源表格方案都有什么短板

做Web表格选型,业界基本绕不开这几类:

  • Luckysheet:当年最火的纯前端开源表格,渲染和交互做得不错,但工程化结构偏老,核心层没和UI充分分离,后续维护慢,二开体验一般。
  • OnlyOffice:功能全,协同做得深,但它本质是完整办公产品,服务端和存储逻辑比较重,深度嵌入Web应用时改造量大。
  • SpreadJS(商业收费):性能好、功能全,也是我做企业项目时常用的备选。它的问题是本质仍是控件级封装,自定义一套全新交互形态时会被框架本身限制。
  • Google Sheets / 飞书表格 / 腾讯文档:体验一流,但数据都在对方服务端,且无法私有化部署,对排他性和数据资产要求高的企业项目根本不行。

1.2 Univer的差异化定位

Univer最大的特点是把“数据模型”和“交互视图”分层设计。它不像Luckysheet那样把数据、DOM、事件绑在一起,而是先定义了一套统一文档数据模型(Universal Data Model),再通过渲染引擎和应用层把它呈现出来。这意味着:

  • 你可以用纯代码创建一个工作簿,给它塞数据、设样式、写公式,再决定用什么UI壳套起来。
  • 同一个数据模型可以同时服务Sheet、Doc、Slide三种文档类型。
  • 插件机制贯穿全生命周期,从工具栏按钮到公式计算引擎,几乎每个层级都能插自定义能力。

我最初只是在找表格,后来发现它其实是一个“可编程的办公SDK”。很多无代码平台做数据录入、审批流转、报表展示时,最头疼的动态行列结构、复杂自定义公式、条件格式联动,用Univer来做都不是拼凑,而是原生的能力。

2. 核心架构拆解:Univer的灵魂设计

2.1 渲染引擎:不依赖DOM性能瓶颈的自有绘制方案

Univer的渲染层值得单独拿出来说。它没有直接用Canvas库去包一层,而是自己维护了一套渲染引擎,底层基于Canvas,支持DOM和Canvas混用。这套引擎的核心是局部脏区重绘和虚拟列表分屏加载。

传统表格组件一旦遇到大范围选区拖动或频繁更新单元格样式,很容易掉帧,原因是浏览器要频繁重排和重绘整个DOM结构。Univer的Canvas渲染把每个单元格作为一个绘制单元,只在数据变化时重绘“被影响的区域”,整表不动。配合双缓冲机制,交互时先绘制到离屏Canvas,最终合成呈现,画面干净利落。

实际开发时,如果你要在Univer的单元格里嵌入视频复杂图表或自定义交互组件,它提供了Overlay层,可以直接叠加DOM节点,而且Overlay能跟随滚动、选区位置移动。这意味着企业里常见的“表格中带小按钮、进度条、缩略图”这类需求,都能用标准方式实现,Studio在项目落地时用得很顺手。

2.2 命令系统:让撤销重做和协同同步成为同一件事

Univer的数据操作不是直接改单元格对象,而是通过**命令(Command)**双向分发。每一次操作的描述就是一个标准对象,比如:

{ type: 'setRangeValues', range: { startRow: 0, startColumn: 0, endRow: 0, endColumn: 2 }, value: [ ['A1', 'B1', 'C1'] ] }

所有变更最终都归为命令,撤销、重做、协同同步、操作录制,统一走这一套机制。这个设计的价值在于:你在本地做的操作,能原样同步到远端;远端用户做的操作,又能在你本地完整回放,且状态一致。相当于所有编辑行为都变成可序列化、可回放的事件流,这正是协同编辑的基本前提。

我做权限控制时,按命令类型做拦截也特别方便。普通用户只能执行部分命令,超过权限就拦截;管理员能执行全部。比在UI层做按钮隐藏靠谱得多,因为即使绕过前端直接调API,命令层也会校验。

2.3 三种文档模型:数据定义才是真正的核心竞争力

Univer同时支持Sheet、Doc、Slide,但这三种形态在底层不是三套代码,而是同一套“文档模型”在不同展示层的具象化。一个工作簿有Workbook、Worksheet、CellRange等对象;一个文档有段落、块、行内元素;一个幻灯片有页面、组件、图层。它们共享资源管理、事件机制、插件体系、协同框架。

这种设计的好处可以从一个真实场景看出来:在无代码平台里,用户经常想把一段业务说明文字和数据表格放在同一个页面预览。传统方案是富文本编辑器嵌一个表格组件,两者各管各的,滚动互相独立,样式不统一。在Univer的架构里,由于文档结构统一,可以基于Doc类型做一个“图文混排”视图,内部直接嵌入Sheet数据区间,甚至把表格渲染成一个块级对象,整体保持同一套交互逻辑。

3. 集成实战:把Univer嵌进企业项目

3.1 最小可用集成:先跑起来再说

第一件事是通过官方脚手架把最基础的工作簿跑起来。用Vite + Vue3 + TypeScript做示例:

npm create vite@latest my-univer-app -- --template vue-ts cd my-univer-app npm install @univerjs/preset-sheets @univerjs/preset-docs @univerjs/ui

创建一个初始化文件:

import { Univer } from '@univerjs/core'; import { UniverSheetsPlugin } from '@univerjs/sheets-plugin'; import { UniverSheetsUIPlugin } from '@univerjs/sheets-ui-plugin'; const univer = new Univer(); const workbook = univer.createUniverSheet({ sheetList: [ { name: '示例表', rowCount: 100, columnCount: 30, cellData: { 0: { 0: { v: '项目', s: { b: true } }, 1: { v: '进度', s: { b: true } } } } } ] }); univer.registerPlugin(UniverSheetsPlugin); univer.registerPlugin(UniverSheetsUIPlugin, { container: document.getElementById('toolbar'), layout: 'horizontal' });

这段代码的核心是两步:先创建数据模型,再注册UI插件。数据没有UI也能独立存在,所以如果只做后端数据计算服务或导出服务,连UI都可以不加载,直接用Node环境跑数据层。这是Univer区别于其他前端表格最大的工程优势。

3.2 按需加载:别把三个文档引擎都打进来

很多团队第一次接入时,习惯性把preset全部引入,包体积直接超标。Univer的模块化粒度做得比较细,完全可以按业务需求拆。

  • 如果业务只需要表格,不处理文档和幻灯片,只安装@univerjs/sheets相关包。
  • 如果不需要公式引擎,可以不注册Formula插件;需要时再动态import。
  • 如果企业是用CDN离线部署,可以自己打一个精简bundle,只保留核心和表格UI。

我在项目里的做法是把Univer分成两个bundle:核心数据包和UI交互包。首屏只加载核心数据包,用于渲染静态报表数据;用户点击“编辑”按钮后再异步加载UI交互包,这样首屏体积能砍掉近一半。

3.3 主题适配与本地化:让它看起来像自家产品

产品经理肯定不希望界面上出现一个“外来”的工具条。Univer的UI基于自研控件库,支持通过主题变量覆盖颜色、字体、间距。以深浅色适配为例:

import { defaultTheme } from '@univerjs/ui'; const customTheme = { color: { primary: '#1677ff', background: '#f5f7fa', text: '#1f2329', border: '#e2e5e9' }, fontFamily: 'PingFang SC, Microsoft YaHei, sans-serif', fontSize: { base: 13, large: 15 } }; univer.registerPlugin(UniverSheetsUIPlugin, { container: containerRef.value, theme: customTheme });

自定义语言包也支持,把菜单文案、右键菜单、快捷键提示全部替换成业务需要的措辞。发布给银行类客户时,我们把工具属性叫“台账编辑”,把“删除”叫“作废”,全是按业务语义透传,用户根本感知不到底层是个开源套件,集成度判断一下子就上来了。

4. 性能调优:让表格一直保持流畅

4.1 虚拟滚动的边界情况

Univer默认开启虚拟滚动,按可视区域渲染单元格,但工程上“看似简单”的虚拟滚动其实受几个因素影响严重:行高不定、合并单元格跨区域跨度大、列宽不同。

实测下来,以1万行、30列的表格为例,每个单元格都有基础数据样式,未做优化时滚动帧率约40fps,局部卡顿可以看出。调整方案是给表格设置默认行高和列宽,减小滚动时不定尺寸测量的开销:

worksheet.getConfig().defaultRowHeight = 28; worksheet.getConfig().defaultColumnWidth = 96;

同时开启allowAutoMerge严格控制合并单元格数量,一张工作表合并区域超过500个时,交互明显变重。业务设计上要尽量约束用户不要大面积合并,或者提供“解除所有合并”的一次性操作。

4.2 公式计算的懒加载与可控调度

公式是表格的核心能力,但复杂公式在数据变化时重算极其消耗性能。Univer的公式引擎默认按依赖链做脏值传播,本质上是Optimized top-down dependency resolution,改一个值只会通知受影响的公式单元进行重算。

实际使用中有两个技巧:

  • 把高成本公式改成手动重算模式,比如自定义按钮触发全表重算。
  • 用calculateOnDemand插件配置,使公式只在指定时机执行。

我做过一个指标测算系统,用户经常修改几千个业务参数,公式链跨越多个工作簿。如果默认自动重算,修改一次要等待近3秒才能继续交互。改成手动重算后,改为实时只重算当前可见区域的公式,完整重算则通过用户点“测算”按钮触发,体验和性能都兼顾到了。

4.3 大表格数据导入:用流式加载而非一次塞爆

直接调用setCellData传几万行数据,不仅慢而且容易白屏。最优做法是把数据拆成分块流式灌入。Univer提供了可修改配置的set表格数据API,可以多次局部赋值,也可以先设置数据再统一刷一次样式。

我的标准做法是:

for (let i = 0; i < totalRows; i += step) { const range = { startRow: i, endRow: Math.min(i + step - 1, totalRows - 1), startColumn: 0, endColumn: 29 }; univer.setCommandExecution({ type: 'setRangeValues', range, value: chunkData }); // 用 requestIdleCallback 分片,避免阻塞主线程 await new Promise((resolve) => requestIdleCallback(resolve)); }

分块大小按数据复杂度取1000行到2000行,插入过程中界面可以慢慢显示数据,用户感知是一种“逐步加载完”的顺畅体验,而不是长时间的空白等待。

5. 协同编辑与数据安全:从单机到多人实时

5.1 协同协议的选型思路

Univer的协同层抽象做得非常干净,它不强制绑定某个协同协议,官方提供的协同插件基于WebSocket实现,但具体同步逻辑可以根据业务选择Yjs或者CRDT思想。协同插件本质上是把本地命令生成的操作结构(Operation)同步给服务端,服务端确认后广播分发,本质上可以理解为“操作变换 + 版本管理”。

团队没有自研协同算法的情况下,建议直接用Yjs来做协同后端的数据层,把Univer的命令转发包装成Yjs的更新事件。Univer的命令结构天然适合做增量同步,因为每个操作都描述具体影响范围。

5.2 服务端对接的关键点

服务端不需要运行Univer框架本身,只需要两个能力:鉴权协议和消息广播。我们用的是一套轻量方式:

  • 客户端通过JWT握手到协同服务。
  • 服务者把消息编码成二进制操作类型推送到房间。
  • 用Redis pub/sub做多节点横向扩展。

首次连接时,客户端拉取文档最新快照(可以用Univer的序列化格式),快照之后应用增量操作。需要注意,快照的生成要用统一版本号,防止服务端分布式部署时不同节点快照不一致。协同编辑有一个避不开的问题:样式和选区冲突。由于单元格值是操作对象,而样式的属性也是同一对象的属性,所以协同过程中如果两个人同时修改同一个单元格的不同字段,需要做属性级合并。我建议在前端基础上加一层字段细分,把value、style、comment分开同步,降低冲突概率。

5.3 权限控制:从单元格级别落地

Univer支持单元格级别的读写权限校验。接入企业后台时,我们按用户角色给数据范围打标。流程是:

  • 登录后拉取该用户可编辑的单元格范围列表。
  • 在前端拦截命令,判断命令对应的range是否在权限范围内,否则直接拒绝。
  • 在服务端再次校验命令的range,防绕过。

这个逻辑最关键的一点是“双端校验”,纯前端做拦截只防君子,防不了程序员。命令结构在这里又帮了忙,因为命令自带range信息,服务端直接解析并按规则校验就行了。

6. 从“用”到“造”:二开实战要掌握的能力

6.1 插件开发的正确姿势

Univer的个人开发者生态还很早期,不少能力要靠自定义插件去补。官方插件机制遵循生命周期,注册、挂载、卸载都绑定在Univer实例上。比如做一个“导出PDF”的按钮:

class ExportPdfPlugin extends UniverPlugin { initialize() { this.registerCommand({ id: 'export-pdf', handler: async () => { const snapshot = this.univer.getSnapshot(); // 调用服务端接口生成PDF } }); } onMounted() { this.getPlugin(UniverSheetsUIPlugin).addToolbarButton({ id: 'export-pdf', icon: 'download', tooltip: '导出PDF' }); } }

插件里能访问getSnapshot和getCommand,本质上就是一个全套件SDK入口。设计插件时,建议把“业务逻辑”和“UI按钮”分开:UI按钮是可选的,命令逻辑必须独立可复用。这样后续如果把插件能力暴露成API给外部系统调用,不用重新改代码。

6.2 业务命令封装:把Excel操作翻译成领域语言

纯前端表格操作对业务用户来说太底层了。二开时要多一层“业务命令”,把Univer原生命令封装成领域语义。比如财务场景的“调拨入库”,底层是“把某列某单元格值加N、把另一个减N”,但前端代码应该写成:

univer.executeCommand('adjust-inventory', { productId: 'SKU-1001', fromWarehouse: '华东仓', toWarehouse: '华南仓', amount: 5 });

然后在这个命令内部去执行多个Univer命令。这样做的好处是业务规则集中、审计日志可读、权限控制粒度统一,不会因某一次前端优化把底层操作绕开。

6.3 构建自定义数据绑定:打通表单和表格

无代码平台最需要的其实是双向绑定。我在表格里的单元格和后台业务对象之间建了一个映射层:

const bindings = { 'A1': { field: 'customer.name' }, 'B2': { field: 'order.status', formatter: 'statusText' } }; worksheet.onChange((range, value) => { Object.entries(bindings).forEach(([key, config]) => { if (rangeContainsCell(range, key)) { setBusinessValue(config.field, value); } }); });

这样用户不是直接编辑原始台账,而是在一个“业务表单”视图里操作,底层才会拉开相应的单元格区域。这个体验比单纯给一个表格填数据更容易被业务接受。

7. 踩坑实录:那些官方文档没写明白的事

7.1 快照序列化与文档版本不兼容

Univer的迭代速度很快,我遇到过跨版本快照读取失败的坑,是数据结构里字段变动导致的。解决办法是别存原始Univer快照,而是导出一份扁平JSON,再在读取时映射成当前版本结构,相当于业务层做一层“数据防腐”。

7.2 移动端适配:工具栏需要单独设计

Univer默认UI是面向桌面端的,移动端触摸交互虽然有基础支持,但工具栏挤在一起几乎没法用。真要在移动端使用,建议隐藏默认工具栏,自定义一个底部操作条,只保留“撤销、加粗、颜色、公式”四个核心动作。表格缩放和滚动在触摸屏上表现还行,主要瓶颈还是多级弹窗。

7.3 公式引擎的精度与Excel差异

Univer公式引擎的精度在部分场景下能对齐Excel的IEEE754,但个别财务函数如IRR、XIRR,细微差异还是存在。做财务系统时必须做一轮“对照测试”:用同一批Excel样本和Univer计算结果比对,误差超过阈值就手动绕过,换成服务端精确计算。

7.4 服务端渲染(SSR)环境下的坑

如果项目用了Nuxt或Next做SSR,直接import Univer会报window未定义。解决思路是只在mount后动态引入,且SSR阶段只负责输出容器的placeholder。另一个方案是用动态组件配合ClientOnly,能把首屏时间压到可接受范围。

7.5 大协同房间的性能拐点

实测一个工作簿有20人以上同时高频编辑时,单文档节点消息量会指数增长,操作延迟会显著上升。规避策略是“协作单位最小化”:一个sheet页最多同时容纳10个编辑者,更多的写操作用队列削峰。或者拆成多个sheet页协作,服务端按sheet分房间扩散。

8. 总结一点个人经验

用Univer做生产项目快一年半,最大的感受是它的设计哲学值得团队深入学习。它不是又一个“国产Excel”,而是真正站在数据、渲染、协同、扩展四个层面进行过系统设计的办公基座。工程侧它还有很多不成熟的地方,文档没有全面覆盖,API存在断代,社区例子偏少,但它的架构上限很高。遇到它当前做不了的能力,在插件层做“临时的、局部的替代”,远比在传统表格里嵌套iframe,或者为了某一个业务点去重新造一个表格要快得多。

如果你正打算把一个表格能力嵌进产品,又纠结选型,我建议先别急着写代码,花一周时间把Univer的示例代码跑一遍,用它的命令和插件机制模拟三个你业务里最真实的操作场景。能不能支撑,比看任何对比分析都直观。

如果你已经在用Univer了,我觉得最值得投入的方向是两个:一是把协同服务做好,这是企业付费的核心理由;二是沉淀一套业务命令库,这是产品壁垒最好的护城河。后面有时间我再单独写一篇关于协同服务端落地的具体方案。

返回列表