Univer:开源Web在线表格SDK,打造Excel级体验
第一次看到 univer 这个项目名,我愣了一下——Univer,Universe 的简写?后来翻了 GitHub 仓库才确认,这确实是“宇宙”的意思,只是少了一个 s。这个命名还挺贴合它的定位:一个想把表格、文档、幻灯片这些办公能力全部塞进浏览器里的开源 SDK。目前社区里讨论最多、也最成熟的部分是它的表格功能,也就是 univer 在线表格。如果你正在做 Web 办公产品、低代码平台,或者只是想在现有系统里快速嵌入一个像 Excel 一样好用的表格组件,那 univer 值得你花时间来研究。它解决的痛点非常直接:传统 DOM 渲染撑不住大数据量、公式引擎不完整、协同能力难自研、扩展成本高。这篇文章我会从项目形态、技术内核、上手实操和踩坑经验四个角度,把自己这段时间折腾 univer 的体会完整写出来,偏工程落地,也有源码层面的思考。
1. 项目形态与核心价值拆解
1.1 为什么 univer 值得关注:从“宇宙”名字说起
我最初以为 univer 是某个大学项目(university 的缩写),后来在社区里看到开发者的访谈才知道,Univer 就是 Universe 去掉一个 s,寓意是“办公能力的宇宙”。
这话不是吹的。项目从 2021 年左右开始持续迭代,定位从一开始的“前端表格组件”逐步升级为一套完整的 Web 办公套件 SDK:表格(Sheets)、文档(Docs)、幻灯片(Slides)三个核心模型共用一套基础设施,包括渲染层、公式引擎、协同机制、插件系统。你可以在一个项目里同时创建三种文档,也可以在任意 Web 页面里嵌入单独的表格能力。从产品形态来看,它直接对标的是个人开发者在做的东西,比如 Luckysheet、x-spreadsheet、SheetJS 这些,但它比这些工具走得更远——它不只是“画一个表格”,而是把编辑、公式、协同、权限、撤销重做、格式化这些完整能力都放进了一个可编程的框架里。
对普通前端开发者来说,第一个感知最强的点应该是“开箱即用”:npm install 一个包,React 或 Vue 项目里 mount 一下,一个接近 Excel 的编辑器就出现了。但真正让它拉开差距的是底层架构。它不是用 DOM 表格元素拼出来的,而是基于 Canvas 自绘的一整套渲染引擎,这让它在处理大数据量时表现稳定,也是它敢叫“Univer”的底气之一。
1.2 它解决的核心痛点与同类产品对比
我在实际项目里集成表格组件时,最先要考虑的是:这玩意儿到底能解决我哪些问题?这里用一个表格把 univer 和几个常见方案放在一起对照。
| 方案 | 渲染方案 | 公式引擎 | 协同 | 可扩展性 | 开源协议 |
|---|---|---|---|---|---|
| univer | Canvas 自绘 | 内置 400+ 函数 | CRDT 协同 | 插件化 SDK | Apache-2.0 |
| Luckysheet | Canvas/CSS Grid | 轻量公式 | 不完整 | 一般 | MIT |
| x-spreadsheet | Canvas | 无完整公式 | 无 | 一般 | MIT |
| SheetJS | DOM 渲染 | 无 | 无 | 文件解析为主 | Apache-2.0 |
| Handsontable | DOM 虚拟滚动 | 需额外集成 | 需付费 | 有限 | 商用授权 |
从表格里能看出来,大多数免费的表格组件属于“能用但不够用”的级别。比如 Luckysheet 渲染不错,但协同能力很弱,真要上生产环境,多用户同时编辑冲突处理是个大坑;SheetJS 其实更适合做文件解析和流转,而不是做在线编辑器;Handsontable 体验好但商业授权成本高,且公式、协同都要自己接,工程量非常大。
univer 的定位就是把这些“不容易搞的部分”一次性补齐。它在内部提供了几个核心引擎:公式引擎负责计算,协同引擎负责多端同步,渲染引擎负责高性能绘制。再加上一套插件化架构,开发者可以像搭积木一样,按需加载表格核心、公式、协同、数据校验、条件格式等模块。这意味着两件好事:第一,初始包体可以按需裁剪;第二,你可以往表格里注入完全自定义的能力,比如自己的单元格类型、自己的工具栏按钮、自己的数据源适配器。
2. 技术内核:渲染、公式与协同的三板斧
2.1 Canvas 自绘渲染:大表格不卡的关键
很多人第一次打开 univer 的在线 demo,会习惯性按下 F12 看 DOM 结构,结果发现表格区域是一块巨大的 Canvas,里面几乎找不到任何 td、tr、table 标签。这正是它处理大数据量的核心思路:基于 Canvas 自绘所有单元格。
为什么一定要自绘?因为 DOM 本身是为文档流设计的,不是为表格网格设计的。你用<table>渲染一万行问题不大,但上到十万行、百万行时,浏览器要维护的节点数会指数级上升,即使配合虚拟滚动(只渲染可视区域),滚动过程中的重排和重绘也会产生明显卡顿。Canvas 的方案避开了“节点”这个概念——我只把可视区域内的单元格画在画布上,滚动时重画,数据本身放在内存中。这种方式天然适配大数据场景。
具体实现上,univer 做了三层事情:
- 视口管理:根据当前滚动位置和画布尺寸,精确计算可视区域的行列区间,只绘制那一部分。
- 图层分层:网格线、单元格背景、边框、文本内容、选区高亮、行号列号等分别绘制在不同图层或同一图层的不同绘制批次中,减少不必要的重绘。
- 编辑态与展示态分离:平时展示用 Canvas 整体绘制,一旦双击进入编辑状态,就在单元格上方覆盖一个真实的
<input>或 HTML 编辑层。这样既保证显示的流畅性,也保留了原生输入体验。
在一台普通办公笔记本上,我实测加载一张 50 万行数据的表格,初始渲染大约在 500ms 到 800ms(不含公式计算),滚动基本能稳定保持在 60 帧左右。如果把公式计算也触发起来的全量重算场景,会有一定卡顿,但配合 disable 自动计算、按需重算之后,交互表现仍然可以接受。它内部还做了行列缓存、字体测量缓存、样式矩阵等优化,这些细节是它和自研项目拉开差距的地方。
对我来说,Canvas 自绘带来的另一个好处是跨端一致性。同一个渲染引擎跑在 PC 浏览器、移动端浏览器、甚至 Electron 容器里,视觉表现能保持一致,不会因为浏览器默认样式差异导致排版错乱。这也让 univer 很适合做白标产品底座。
2.2 公式引擎:不光是计算,更是依赖管理
一个号称 Excel 级别的表格引擎,公式能力是绕不开的硬指标。univer 的公式引擎是一个独立模块,可以脱离表格组件单独使用。这个设计很聪明,公式引擎本身被设计成与 UI 无关的计算内核,通过订阅单元格数据变化来驱动计算。
引擎内部的工作流程大致是:
- 解析阶段:把公式字符串解析成抽象语法树(AST),比如
SUM(A1:A5)会被解析为函数节点 SUM 和参数区间节点 A1:A5。 - 依赖图构建:在解析过程中建立单元格之间的依赖关系,形成一张 DAG(有向无环图)。单元格 B1 引用了 A1,那么 A1 是 B1 的前置节点。
- 脏标记传播:当 A1 的值变化时,引擎不会立刻重算所有公式,而是沿依赖图传播“脏”标记,只重算受影响的节点。
- 计算调度:支持同步计算和异步计算。对于大型工作簿,它会把计算任务拆成分片,优先处理用户可见区域,其余部分在空闲时间片内继续完成。
这个机制听起来不复杂,但做完整很考验工程能力。univer 目前已经实现了 400 多个公式函数,包括常用的数学、统计、文本、日期、查找引用类,还支持数组公式和动态数组溢出。对于循环引用,它会在构建依赖图时做环检测并提示用户,不会让程序莫名死循环。
我自己在集成时最关心两个点:一是公式跨表格引用,也就是 Sheet1!A1 这种语法,univer 支持区域跨 sheet 引用,也支持跨工作簿的概念模型;二是自定义公式。如果业务里有一些内部计算逻辑,比如价格计算、工龄计算、库存周转率,你不想在表格里让用户写一大串 IF 嵌套公式,就可以把自己封装成 RATE、STOCK_TURNOVER 这种函数注册进去,使用体验和内置函数完全一致。
有一点要特别提醒:公式引擎的计算量是隐蔽成本。一个 100 列的表格如果在某些列里写了大量 VLOOKUP 类公式,数据量一旦上来,初次计算可能需要好几秒。解决方案是把公式计算交给 Web Worker 执行,避免阻塞 UI 主线程。univer 的架构允许自定义计算调度器,我后来就把它接到了 Worker 里,整体交互流畅度提升了一个档次。
2.3 协同编辑:CRDT 带来的实时体验
协同办公是很多开发者梦寐以求的能力,但也是最难啃的骨头。常见的做法有两类:一类是 OT(Operational Transformation)算法,Google Docs 早期就是用 OT;另一类是 CRDT(Conflict-Free Replicated Data Type),Figma、飞书这类产品近年来越来越多用 CRDT。
univer 选择的是 CRDT 路线。协同的核心问题在于:多个用户同时对同一个单元格编辑时,如何收敛最终状态。CRDT 的思路是让每个用户的操作都带上足够多的元信息(比如操作位置、ID、HLC 时间戳),通过合并规则保证所有副本最终一致。和 OT 相比,CRDT 不需要一个中心化的操作转换服务,节点之间天然可以合并,而且天然适合离线编辑后再联网同步的场景,这对弱网环境下办公很重要。
我体验下来的感受是:多人同时在一个表格里输入,光标、选区、内容更新都比较平滑,短暂的网络抖动也不会导致整个编辑会话崩溃。底层还支持“区块级同步”,也就是多人编辑同一个文档时,以单元格或区间为最小粒度管理版本,而不是整个文件一把锁。这在产品层面意味着:用户 A 正在编辑第 3 行的数据,完全不影响用户 B 在 100 行创建公式。
不过也要泼一盆冷水:协同功能不是“装上就能用”的。univer 当前提供的是客户端侧的 CRDT 能力以及配套的同步协议,生产环境还是需要你自己搭一套服务端来做房间管理、用户身份识别、消息转发和持久化。官方示例里通常用 WebSocket 加一个简单的事务存储来演示,真正上生产时,你还要考虑消息可靠性、断线重连、服务端快照压缩等问题。这部分我在第 4 节展开讲。
3. 从零上手:快速集成与核心实操要点
3.1 五分钟集成一个在线表格
如果你只想在一个普通的 npm 项目里跑起来,univer 的集成过程非常直接。以当前 3.x 版本的主流用法为例,先安装依赖:
npm install @univerjs/core @univerjs/sheets @univerjs/ui @univerjs/engine-render @univerjs/engine-formula不同小版本可能还在演进,官方现在也提供@univerjs/presets的方式一键引入预设组合,省去了手动拼装模块的弯路。初始化代码大致长这样:
import { Univer } from '@univerjs/core'; import { defaultTheme } from '@univerjs/design'; import { UniverSheetsPlugin } from '@univerjs/sheets'; import { UniverFormulaEnginePlugin } from '@univerjs/engine-formula'; import { UniverUiPlugin } from '@univerjs/ui'; const univer = new Univer({ theme: defaultTheme, locales: ['zhCN', 'enUS'], plugins: [ UniverSheetsPlugin, UniverFormulaEnginePlugin, UniverUiPlugin, ], }); univer.createUnit(UniverSheetsPlugin, { workbook: { sheets: [ { name: 'Sheet1', rows: 100, columns: 20, cellData: { 0: { 0: { v: '项目' }, 1: { v: '金额' }, }, }, }, ], }, });HTML 侧只需要一个容器:
<div id="app" style="width: 100%; height: 600px;"></div>初始化完成后,你就能得到一个完整的、可编辑的表格界面。界面默认带有工具栏、公式栏、行号列号、单元格编辑器,数据可以直接输入,也可以从 JSON 初始化。
有一点很关键:univer 是“运行时容器”,不是“画布生成器”。表格创建后,页面上的所有交互都会更新到一个内存中的数据模型中,这个模型通过getSnapshot/loadSnapshot接口可以序列化为 JSON。换句话说,你可以把 univer 无缝接入到自己的业务系统里,用 JSON 作为数据交换格式,用 API 作为写入入口。
我建议初学者第一件事不是去改主题,而是把示例跑起来后,在控制台里观察univer对象暴露出来的方法和数据模型。这会让你迅速理解它的思路:所有 UI 操作最终都落到数据模型变更上,UI 只是数据的一个投影。
3.2 数据接入、自定义单元格与自定义公式
跑通默认示例之后,下一步通常就是“接自己的数据”。最常见的场景是从后端拉一个 JSON 数组,然后填充到表格中。univer 不是逐格赋值的思路,更推荐一次性写入整个工作簿快照:
const snapshot = { workbook: { sheets: [ { name: '月度报表', rows: 1000, columns: 26, cellData: { 0: { 0: { v: '序号' }, 1: { v: '客户' }, 2: { v: '金额' } }, // 更多单元格数据 }, }, ], }, }; univer.loadSnapshot(snapshot);这种方式比循环 setCell 高效得多,因为快照是结构化的,底层可以直接批量构建数据模型,避免了一次次触发变更事件的开销。
接入数据之后,很多人会面临一个新的需求:默认单元格只有数值、字符串,我想要支持自定义类型,比如“选择器”“按钮”“进度条”。univer 的插件系统允许你注册自定义单元格类型。核心思路是创建一个自定义 renderer,在 Canvas 绘制阶段为这类单元格绘制特殊视觉,然后注册编辑器和交互规则。官方文档里有一个比较典型的进度条范例,我把流程总结一下:
- 定义单元格类型标志,例如
customTypeKey = 'CUSTOM_PROGRESS'。 - 在单元格数据中写入
t: customTypeKey, v: 0.75之类的自定义数据。 - 实现一个单元格渲染器,根据
v的值计算进度条长度,在 Canvas 上画出来。 - 注册进入编辑状态时的处理逻辑,比如点击后弹出一个滑杆组件。
这套机制非常值得学,因为它决定了 univer 能不能和你的业务深度绑定。如果你的系统里有“审批状态”“优先级”“百分比进度”这些高频可视化需求,把它们做成自定义单元格,体验会比文本符号强得多。
自定义公式同样不复杂。接一个内部函数,本质上就是注册一个计算 descriptor:
import { FunctionType } from '@univerjs/engine-formula'; const MyFunction = { name: 'TODAY_CN', functionType: FunctionType.Date, minParams: 0, maxParams: 0, calculate: () => { const now = new Date(); return `${now.getFullYear()}年${now.getMonth() + 1}月${now.getDate()}日`; }, }; univer.registerFunction(MyFunction);注册完成后,用户在任意单元格输入=TODAY_CN()就能吃到这个函数。对于更复杂的逻辑,calculate 参数还会把参数值、上下文传递进来,你甚至可以做成读取表格区域的聚合函数。
3.3 性能调优与前端框架集成细节
在真实项目里,纯“能跑”是不够的,还要考虑性能和集成体验。先说性能层的经验。
univer 默认会渲染整个工作簿的骨架(skeleton),包括所有行列元信息。当工作簿非常大(比如 100 万行),这个骨架构建本身就有成本。实际操作时,我有三条调优路线很管用:
- 合理控制初始行列数量。如果业务不需要无限行,创建时就把 rows/columns 设为固定值,不要给一个很大的数字。因为 univer 内部会为行高列宽、合并单元格等元信息分配数据结构和缓存,数字越大初始化越慢。
- 关闭不需要的插件模块。比如不需要插入图表、数据透视表的话,就别在多包管理下把相关插件打进构建产物。univer 的按需加载设计让每一个插件都能独立取舍,但是如果你直接使用一个聚合了所有能力的 preset,包体大小和初始化开销都会偏高。
- 把重量级计算放到 Worker。公式引擎对大量数据的全量重算会阻塞 UI 线程,如果你有几百个复杂公式,建议封装一层 Worker 通信协议,把快照和变更事件导出到 Worker 里计算,然后把结果回传。
再说到前端框架集成。univer 本身并不依赖任何 UI 框架,它直接操作 DOM 容器。React 和 Vue 集成时,推荐封装成组件方式,挂载和销毁时机需要和组件生命周期做好绑定。以 React 为例,一个最简单的封装骨架是:
import { useEffect, useRef } from 'react'; import { Univer } from '@univerjs/core'; import { UniverSheetsPlugin } from '@univerjs/sheets'; export default function Spreadsheet({ snapshot }) { const containerRef = useRef(null); useEffect(() => { const univer = new Univer({ plugins: [UniverSheetsPlugin], }); univer.createUnit(UniverSheetsPlugin, snapshot); return () => { univer.dispose(); }; }, []); useEffect(() => { // 当外部数据变化时,决定是否重新加载快照 }, [snapshot]); return <div ref={containerRef} style={{ width: '100%', height: 600 }} />; }这里有个很隐蔽的坑:React 的 StrictMode 在开发环境下会调用两次 effect,如果不在 cleanup 里 dispose 掉 univer 实例,就会出现多个实例同时监听同一个容器的问题。别问我是怎么知道的,反正在生产环境一切正常、开发环境白屏过两次之后,我就把 dispose 这件事刻进了习惯里。
SSR 场景下也要注意,univer 强烈依赖浏览器 API,比如 Canvas、MutationObserver、window 对象。如果项目用了 Next.js 这类服务端渲染框架,必须在客户端组件里动态引入,并且确保容器元素只在浏览器端挂载。否则服务端渲染时会直接抛window is not defined的报错,或者导致首屏 HTML 与客户端调和失败。
4. 常见问题、踩坑记录与实战建议
4.1 高频问题排查速查表
我把网上社区和我自己遇到的问题汇总成了一张速查表,先列排查思路,再展开讲几个典型问题。
| 症状 | 可能原因 | 排查方向 | 解决方案 |
|---|---|---|---|
| 白屏/表格区域空白 | 容器没有高度 | 检查容器 CSS | 给容器设置明确宽高 |
初始化报错window is not defined | SSR 或环境不支持浏览器 API | 检查项目是否在客户端渲染 | 动态 import,条件挂载 |
| 公式显示但结果为 0 | 公式插件没注册 | 检查插件列表是否包含 FormulaEngine | 注册公式引擎插件 |
| 导入 xlsx 后样式丢失 | 文件解析兼容性限制 | 检查源文件是否含特殊样式 | 降级处理,必要时做格式预处理 |
| 协同模式下内容冲突 | 服务端消息排序/持久化问题 | 检查 WebSocket 消息时间戳和版本号 | 补全服务端协同协议 |
| 内存占用持续上涨 | 大表格视图模型持有过多缓存 | 查看行列数量统计 | 裁剪全局行列数,释放不可见区缓存 |
第一个“白屏”问题最容易被忽略。univer 的容器元素如果没有明确的宽高,或者说父级因为是 flex 布局没有撑开,Canvas 区域就会变成 0 尺寸。这个排查起来很快,但遇到几次之后我就养成了习惯:集成前先写死一个宽度和高度,或者用aspect-ratio配合父级尺寸,行为会稳定很多。
第二个高频问题是“公式不生效”。这多半不是公式引擎有 bug,而是当前 workbench 没注册公式插件。早先版本的写法是new Univer({ formula: true })一类的方式,3.x 之后改成模块化注册。如果你看到公式输入后单元格只显示文本、没有计算结果,优先检查是不是漏了UniverFormulaEnginePlugin。
“导入 xlsx 后样式丢失”是一个更难缠的问题。univer 对 xlsx 的解析走的是自己在 Excel 文件格式层封装的解析器,它和 Excel 的样式模型有兼容性差异。比如一些花哨的条件格式、图表、艺术字可能在导入后变得不可见。我的建议是:对于关键数据做无损导入,对于样式要求极高的场景,先做一轮预清洗,把 Excel 中的样式规范转换为 univer 能识别的标准属性。这个问题官方在持续解决,但目前还做不到 100% 像素级还原。
4.2 生产环境落地的几个关键建议
如果你决定把 univer 用在真实业务里,下面这几条是我强烈建议提前规划的。
第一,版本管理要严格。univer 项目迭代速度很快,小版本之间 API 也有变动,网上搜到的大部分教程都只适配某个具体版本。不要直接安装latest,要把 package.json 里的依赖包版本锁定。我自己的做法是在 CI 里生成 lockfile,并对 univer 相关的几个包设置精确版本号,避免某天 build 出来行为突然变化。
第二,业务逻辑和表格模型解耦。univer 的数据模型虽然好用,但不要把所有业务状态都塞进单元格数据里。比如后台判断大宗交易需要高亮提醒,这个逻辑应该由业务层监听数据变化后,再把样式写入表格模型。如果把判断逻辑写进渲染流程,后期维护会非常痛苦,尤其是当多个功能同时操作一组单元格时,渲染优先级排查会让你怀疑人生。
第三,考虑双向数据同步的边界。univer 提供了onCellChange一类的事件通知机制,你可以把用户编辑实时同步到后端。但要注意,批量导入、公式重算、协同远端变更也会触发这些事件,如果不加区分,你可能会把大量无效变更写入后端。一个比较好的做法是使用工作簿快照来做整表级别的持久化,用单元格级事件只做增量同步或临时提示,从架构上避免事件风暴。
第四,协同服务端不能偷懒。虽然客户端是 CRDT,但服务端仍然要负责权威排序、消息广播、快照管理。如果只是同机多标签页 demo,用 BroadcastChannel 可以模拟;但跨设备协同必须上真正的后端。落地方案建议至少包含:
- WebSocket 网关:负责连接管理、心跳、断线重连。
- 消息队列:对操作消息做有序缓冲,防止乱序。
- 持久层:把每个房间的操作日志和工作簿快照持久化。
- 权限服务:按用户和文档维度控制读写权限。
只要链路里有一个环节不可靠,协同体验就会断崖式下跌。不要觉得“反正客户端 CRDT 能自愈就无所谓顺序”,消息若乱到一定程度,合并之后的数据虽然在结构上一致,但具体单元格值可能不是用户期望的版本,这在实际使用中几乎是事故级别的体验。
最后再分享一个小技巧:在开发环境下,把 univer 内部日志等级调到 verbose,它能打印单元格变更、公式计算、渲染重绘等细节。遇到诡异问题的时候,这些日志比任何调试工具都好使。生产环境记得关掉日志等级,否则控制台输出量非常大。
老实说,univer 离“完全代替 Excel”还有距离,但它已经把 Web 表格开发的整体水位拉高了一大截——过去需要团队从零自研的公式引擎、渲染引擎、协同协议,现在变成一个可以在几天内集成上手的开源方案。如果你有在 2025 年做一款在线表格产品的念头,不用犹豫,从 univer 开始研究是最省时间的一条路。