ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Univer在线表格架构解析:从Canvas渲染到协同编辑的选型实践

Univer在线表格架构解析:从Canvas渲染到协同编辑的选型实践

说实话,我第一次打开 Univer 的 GitHub 仓库时,第一反应是:又一个在线表格轮子。但把 Issues 和架构文档翻完之后,这个结论很快被推翻。Univer 不是简单的表格组件,而是一整套以 Sheet 为起点,目标覆盖 Doc、Slide、甚至 IM 能力的开源 Office 基座。对于每个想把 Excel 搬进浏览器,又不想被商业授权卡脖子的团队来说,Univer 在线表格是目前很值得认真看的一个选项。这篇文章不聊 PPT 式的展望,只讲我实际调研和接入时看到的东西:选型逻辑、架构设计、接入步骤、性能痛点,以及最容易踩的坑。

我有大概一周时间专门在评估在线表格方案,前后试过四五个开源项目,最后把主要精力放在 Univer 上,一部分原因是它的 GitHub 活跃度太显眼了,另一部分原因是它解决了我在其他表格库里遇到的最大痛点——扩展能力。如果你也是前端、全栈,或者是正在做办公协同类产品的技术选型者,这篇内容应该能帮你省下不少试错时间。

1. 在线表格选型复盘:为什么我最终盯上了 Univer

1.1 在线表格的“老三难”

做生产可用的在线表格,本质上是在挑战三座大山:性能、协同、文件兼容。

性能上的典型场景是十万行数据。如果每个单元格对应一个天然 DOM 节点,浏览器在滚动时基本会卡到“未响应”。我自己之前做过一个纯 DOM 的管理后台表格,只有 5000 行 8 列,滚动时帧率掉到个位数,鼠标滚轮就像在水里划船。换成 Canvas 之后,只需要把可视区域那几十个格子画出来,渲染压力小到可以忽略。所以但凡是认真做在线表格的产品,渲染层基本都会往 Canvas 或 WebGL 上走。

协同难在冲突处理。多人同时编辑 A1 单元格,一个改成“100”,另一个改成“200”,如果后写直接覆盖先写,用户就会打架。在线文档的协同至少要做到“字段级合并”或“操作顺序重放”,把每个用户的每次修改当成一条可排序的日志。这个能力对架构要求很高,很多表格库根本没有在底层预留,导致协同功能都是后期硬贴上去的。

文件兼容是最容易被低估的一项。Excel 看起来是一个普通文件,拆开之后有共享字符串表、样式表、公式缓存、数据透视表、图表等二十多种结构化碎片。很多表格库能把 xlsx 打开,但公式会失效、样式会错位。用“能打开 Excel”当卖点的库不少,真正能把条件格式和数据验证也还原出来的少之又少。

这三个难点叠加在一起,基本就决定了在线表格不是“给个表格控件随便改改”就能交付的东西。这也是为什么市面上成熟开源项目那么少,因为每一座山都要投入大量人力。

1.2 从 Luckysheet 到 Univer:一次自我推翻

Univer 这个项目和 Luckysheet 有直接血缘关系。Luckysheet 曾经是国产开源表格里的明星项目,很多网站都在用,我自己也研究过它的源码,优点是渲染思路很先进,Canvas 画格子这件事它在很多年前就做了。但它的整体架构偏组件化,核心逻辑和界面耦合比较重,导致协同、公式、扩展这些能力越往后越难加。

Univer 更像是把 Luckysheet 的实践经验推倒重来:底层用 TypeScript 重写,界面和逻辑彻底分离,所有业务模块以插件方式动态注册,渲染层继承并发扬了 Luckysheet 的 Canvas 路线。在社区讨论里经常能看到“Luckysheet 作者转去做 Univer”的说法,事实上核心团队也是同一批人。

简单做个选型对比,大家根据自己项目阶段对号入座:

项目开源协议核心渲染协同能力扩展方式适合人群
UniverApache-2.0 为主Canvas 2D / WebGL架构预留完整协同链路插件化 + IoC 依赖注入产品级在线表格、办公套件
LuckysheetMITCanvas偏原始,维护趋于停滞组件内扩展研究学习、老项目维护
SheetJSApache-2.0 / 商业双授权纯 JS 数据解析基本无编辑协同无界面层,适合服务端处理批量导入导出、解析场景
Handsontable商业授权(部分版本免费)DOM + 虚拟滚动需要商业组件支持插件机制但受限较多中后台数据网格、传统表格

我的个人判断是:如果你只是想“解析 Excel 并展示在页面上”,SheetJS 就够了;但如果你要做一个“让用户在浏览器里真正像用 Excel 一样编辑数据”的产品,Univer 的架构余量明显更大。选型不能只看今天能不能跑通 Demo,更要看半年后加需求时会不会想骂人。

2. Univer 核心架构拆解:一张表格是怎么跑起来的

2.1 分层设计:Core、渲染引擎与插件

Univer 给人的感觉很像一个小型游戏引擎,而不是传统组件库。它把核心数据、界面渲染、交互控制拆成了独立模块,再通过依赖注入容器(IoC)把它们装配起来。

我在接入时最直观的感受是:你不一定要先理解所有模块才能用起来,就像你不需要知道游戏引擎的骨骼动画系统怎么实现,也能先摆个场景进去。但理解分层对后续排错至关重要。

大概可以这么分层:

  • Core 层:不依赖 DOM,负责 Workbook、Sheet、Cell 的数据模型和命令分发,纯 TypeScript 实现,理论上可以在 Node.js 环境跑。
  • 渲染引擎层:基于 Canvas 2D / WebGL 的即时绘制,类似游戏引擎按帧绘制,单元格、选区、滚动条都走这一层。
  • 业务模块层:Sheets、Docs、Slides 等具体产品模块,各自有独立模型和命令,通过插件注入。
  • UI 层:封装工具栏、右键菜单、弹窗等交互组件,UI 组件和渲染引擎分离,方便替换成自己的壳。

这种分层带来的直接好处是:你可以在任意框架里接入 Univer。Vue、React,还是原生 DOM 容器,都能跑。Univer 不绑定框架,官方文档里也反复强调“框架无关”。

依赖注入容器这个点,对没接触过的同学可能会觉得抽象。打个比方:就像装修房子时,你把插座、水管、网线接口都预留好了,后面接什么家电都行;没有预留的房子,想装个空调要砸墙。Univer 的插件系统就是那套标准化接口,你只要按接口接入,就能往里塞任何业务能力。

2.2 Command 机制:为什么每次修改都是“有迹可循”的

Univer 里几乎每个用户操作都会被包装成一条 Command,比如“设置某个单元格的值为 100”“给某一行加背景色”。命令不是直接写在监听函数里的,而是统一发送到 Command Service,由它分发、校验、执行、记录。

我在做二次开发时发现,这个设计有三个非常实际的好处。

第一,撤销重做好做。每个 Command 都有对应的逆向操作,只要把命令日志存起来,Ctrl+Z 就天然支持。第二,操作日志清晰。用户的每一步行为都能留存,出问题可以回溯。第三,协同回放有底子。一个远端用户的操作,本质上就是在你这台机器上“重放一遍 Command”。

如果你准备在 Univer 上做权限控制,这个机制也很有用。比如你想限制某些用户只能修改自己负责的列,可以在命令执行前拦截它,做一次权限校验,通不过就丢弃,完全不用去改底层源码。我自己接“指定列只读”功能时,就是在这个环节拦截的,改动量很小。

2.3 协同编辑的底层思路

关于协同,Univer 的架构给了非常开放的接口,但这里要特别提醒一句:Univer 提供了协同所需的数据模型、操作隔离和执行框架,但真正“多人同时编辑”还需要一个后端同步服务。它不像某些商业产品那样开箱即用自带服务器。

通常的做法是:把用户产生的 Command 序列化后,通过 WebSocket 推到后端,后端再用 OT(操作转换)或 CRDT 这类算法合并不同用户的并发修改,合并完再广播回所有客户端。

我在调研时看到很多团队有一个误解:以为引用了 Univer 就等于支持了实时协同。其实在默认情况下,它就是单机编辑器。想要生产级的多人在线表格,后端协同服务是必须独立设计和实现的。开发初期建议直接把 Command 日志打到控制台,确认多端各自生成了命令,再进行合并逻辑的开发,这样排查问题会快很多。

3. 从零接入:5 分钟做一个 Univer 在线表格

3.1 环境准备与安装依赖

实操部分,我们用 Vite 搭一个最简前端项目,React 或 Vue 都行。Univer 是框架无关的,这里我用原生 TS 示例,保证大家怎么接都顺。

先安装依赖:

npm install @univerjs/core @univerjs/sheets @univerjs/ui @univerjs/sheets-ui

注意包版本要拉齐,尽量用最新稳定版。我最早踩过的坑就是 core 装了一版、ui 装了一版,结果工具栏按钮全部点了没反应,排查了半天才发现是版本错位。建议安装时直接统一版本号,比如全部带@latest或用同一个精确版本。

3.2 基础初始化与创建 Workbook

初始化代码很简单:

import { Univer } from '@univerjs/core'; import { UniverSheetsPlugin } from '@univerjs/sheets'; import { UniverUIPlugin } from '@univerjs/ui'; import { UniverSheetsUIPlugin } from '@univerjs/sheets-ui'; const univer = new Univer(); univer.registerPlugin(UniverSheetsPlugin); univer.registerPlugin(UniverUIPlugin, { container: '#app', header: true, toolbar: true, footer: false, }); univer.registerPlugin(UniverSheetsUIPlugin); univer.createWorkbook('My first workbook');

这里有一个值得深入理解的设计:为什么 UIPlugin 要单独注册?因为 Univer 把“有没有表格能力”和“有没有界面”拆成了两个维度。如果你是在无头浏览器或 Node 环境里做数据处理,只需要UniverSheetsPlugin,不需要 UI 插件。这个设计对自动化脚本来说非常友好,数据模型和执行引擎可以完全脱离界面运行。

3.3 往单元格写入数据与样式

初始化之后,拿到 Sheet 实例,用 Range 方式操作单元格:

const sheet = univer.getActiveSheet(); const range = sheet.getRange(0, 0, 1, 1); // 第 1 行第 1 列,1x1 区域 range.setValue({ v: 'Hello Univer' }); range.setFormat({ bold: 1, color: '#FF0000' });

如果要批量写入二维数组,建议用批量接口,而不是循环 setValue。我实测过,用循环写 1 万行数据,浏览器明显卡顿;走批量写入后,耗时基本可以忽略。

这里需要提醒一个容易踩的细节:Univer 的单元格值对象通常是{ v: 实际值 }的结构,v既可以是字符串、数字,也可以是带格式的对象。写公式时,要注意公式字符串是否带等号,不同版本的解析规则略有差异。如果你发现getActiveSheet这个方法在新版本里不存在了,多半是官方改了获取方式,打开 examples 目录同步更新一下即可。

3.4 绑定数据源与导入导出

“univer 在线”这个热词背后,大部分人的真实诉求就是“像 Excel 一样在线编辑,还要能导入导出”。这块拆成两方面来说。

导入导出方面:在 Univer 里是“Import / Export”插件能力。你注册对应的 xlsx 导入导出插件后,把文件流交给导入方法,Excel 文件就能被渲染成可交互的 Sheet;反过来,把当前 workbook 内容序列化,就能导出为 xlsx 或 CSV。但我的实战经验是:不要用全量导出接口去导超大文件,Excel 到了 50MB 以上很容易卡死浏览器主线程。更稳的做法是先序列化业务数据,后端用服务端库合并生成 xlsx,再走下载接口。

数据源绑定方面:监听单元格变化事件,把增量改动推到自己的后端。

univer.onCellChange((params) => { const { sheetId, row, column, value } = params; api.post('/sheet/update', { sheetId, row, column, value }); });

这样 Univer 就变成了一个前端编辑视图,数据最终落在你自己的业务库里。我见过不少团队把整张表 JSON 全量回传,短时间没问题,但数据量一大带宽和数据库压力都会上来,增量更新是更健康的方式。

4. 实测中的坑位与性能调优(附排查清单)

4.1 大数据量渲染卡顿怎么办

先承认一个事实:Canvas 渲染并不是万能的。Univer 确实能轻松画出 10 万行,但当你把这 10 万行的数据一次性通过 API 写入时,渲染只是其中之一,数据模型构建、样式计算、公式求值才是开销大头。

我实测下来建议按这个顺序排查:

  • 是否注册了不必要的插件,比如不需要图表却加载了图表插件;
  • 是否设置了超出可视区域的大量条件格式规则;
  • 是否在写入数据后没有做区域去重和合并刷新;
  • 公式单元格是否形成链式依赖,改了 A 列导致几百个公式级联重算。

排查工具直接用浏览器 Performance 面板。录一段操作,如果 Main Thread 上 Function call 占了大头,多半是公式求值;如果 Paint 占大头,才是渲染层的问题。这样能快速定位瓶颈,不用瞎猜。

4.2 Canvas 渲染与主题样式的“爱恨情仇”

在线表格项目最烦的一件事就是“按公司 VI 换肤”。DOM 组件换个 CSS 变量就完了,Canvas 不行,单元格里的所有颜色都是渲染代码画出来的。

Univer 用一套主题 token 机制管理颜色和字体,想换肤必须通过覆盖主题配置实现,不是简单的样式覆盖。我实操中遇到的高频问题是:自定义了品牌色,工具栏按钮变蓝了,但单元格边框还是灰色。这种多半是只覆盖了 UI 组件主题,没有覆盖渲染引擎主题。排查时要把整套 Theme 配置从头过一遍,UI 主题和 Render 主题两边都要改。

还有一个 Retina 屏问题。Univer 默认会考虑 devicePixelRatio,但如果你在 iframe 里加载,或者手动调整过容器缩放,文字就会发虚。临时方案是在初始化容器上先固定一个像素尺寸,渲染完成后再切到自适应,体验会好不少。

4.3 协同冲突和数据刷新问题

不少团队试水阶段第一步不是做协同后端,而是先让人在同一个表格里各写各的,测试时发现 A 改的内容 B 完全看不见。这是很正常的,因为没有后端同步服务,Univer 就是一个单机编辑器。前面已经强调过,真的要多端实时同步,需要自己搭服务。

另外,外部数据源更新后表格不刷新,也是我见的比较多的问题。比如后台把库存数据推送过来,前端拿着新值往 Sheet 上 setValue,但 UI 纹丝不动。遇到这种情况先检查两个点:第一,值是否真的写到了正确的工作表;第二,对应的 Sheet 是否已经被切到非激活页签。Univer 对非激活 Sheet 的视图可能会做“离屏”处理,必要时手动触发一次区域重绘。

4.4 导入导出 xlsx 的兼容性陷阱

用 Univer 导入一个花里胡哨的 Excel,最常见的几类问题:

  • 带宏的 xlsm 文件会丢失宏能力;
  • 复杂数据透视表可能只保留结果、丢失部分配置;
  • 隐藏工作表导入后的显示策略和原 Excel 不一致;
  • 自定义字体导出时可能退化成默认字体。

我把这类问题归类为“Excel 宇宙无限,解析能力有限”。这不单是 Univer 的问题,任何开源表格库都一样。因此涉及金融级、对细节要求极高的 Excel 文件流转时,我的做法是:保留一份原文件做存档,Univer 负责编辑与预览,最终导出时做一轮服务端校验与样式补齐。这样用户下载到的文件不会“看起来不太对”。

5. Univer 还能怎么玩:从表格组件到业务基座

5.1 把 Univer 嵌进自己的业务系统

如果只是做个“能填表格的表单”,其实用不到 Univer,一个 textarea 加分隔符就够了。真正需要 Univer 的典型场景是:

  • CRM 里的报价单,需要销售人员像操作 Excel 一样调整价格和折扣;
  • 项目管理里的工时表,按项目拆行、按人员拆列;
  • 低代码平台的自定义数据表格,需要用户自己定义字段、筛选和公式。

在这些场景里,Univer 的价值不是“看得见的网格”,而是“让用户愿意在系统里做数据编辑”。它的编辑体验越接近 Excel,业务人员的培训成本就越低。

我比较推荐的接入套路是:页面加载后先拉取 JSON 配置,初始化 Univer 时填充表头和首行数据;运行时监听onCellChange做增量保存;再在工具栏里加一个“导出 Excel”按钮,方便客户把在线数据带走。这个套路可以在两天内跑通一个可用版本。

5.2 用插件体系做自己的“周边功能”

Univer 的插件化做得比较彻底,自定义一个侧边栏面板并不困难。我举个实际例子:做一个“自动校验库存”的按钮,可以注册一个插件,在工具栏增加入口,用户点击后遍历当前 Sheet 的 SKU 列,再调用后端接口校验库存,最后用高亮颜色把库存不足的行标红。

这个模式的好处是:你不用改 Univer 上游代码,业务逻辑都封装在自己的插件里,升级 Univer 时只需要同步接口签名。将来团队里有人基于 Univer 做二次开发,交付物也更容易沉淀成内部组件库。

社区里已经有不少围绕 Univer 的扩展包,比如公式插入面板、数字格式模板、图表快捷生成等。从 GitHub 的 Issue 和 PR 活跃度来看,这个项目已经不是“一个人在战斗”,而是越来越多团队在参与共建。

最后再聊一点个人工程师视角的感受。做在线表格是个典型的“看起来不难,做起来全是窟窿”的领域。选型选中 Univer,只是把地基打好了,后期的主要工作量依然在协同、兼容和性能调优上。我的建议是:别把它当成现成的 Excel,当成一个可以深度改造的“文档引擎”来用。先用小模块跑通集成,再逐步啃协同和导出链路。这套思路如果你也认同,Univer 很值得长期跟下去。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进