☰
Univer开源在线表格引擎解析:架构、接入与性能优化实战
2026/9/30 8:38:43 网站建设 项目流程

1. 先看懂“univer”这三个字母到底在讨论什么

老实说,刚看到“univer”上热搜的时候,不少人第一反应是某个明星的英文名,或者是某款新游戏。但当你把这个词放到技术语境里,它指向的其实是一个很硬核的开源项目:Univer。简单点说,它是一套基于 TypeScript 构建的在线办公套件引擎,目前以电子表格(Spreadsheet)为核心,同时也在扩展文档、幻灯片等能力。放到普通用户视角,你可以把它理解成“用网页运行的一套 Excel 克隆工具”,但本质上它比 Excel 的网页版要轻得多,因为它的定位是“可嵌入的组件”,而不是一个封闭的产品。

我最初看到这个热搜词时,第一反应是去查它的 GitHub 仓库。Univer 的前身叫 DreamNum,曾经做过一个叫 Luckysheet 的开源表格项目,后来团队把整个架构推翻重做,才有了现在的 Univer。它解决的问题很直接:在线表格总是需要一款真正开源、可二次开发、性能足够好、且样式交互接近原生 Office 的产品。市面上的旧方案要么是简单的 DOM 表格模拟,要么是重量级的自研引擎,最终都很难在“轻量”和“强大”之间取得平衡。Univer 选择用 Canvas 2D 做渲染,把数据层、命令层、UI 层全部模块化拆分,等于给开发者准备了一套“乐高式”的表格内核。

如果你正在做 SaaS 报表系统、在线协作工具、数据分析平台,或者需要在网页里嵌入一个可交互的表格组件,Univer 会是一个值得认真研究的方向。这篇文章我就从我的实际使用经验出发,把 Univer 的架构、核心实现、接入流程、常见坑位一起拆开聊清楚。内容不保证 100% 覆盖源码细节,但一定能让你少走很多弯路。

1.1 为什么一个开源表格项目能上热搜

Univer 上热搜这件事,本身透露了一个行业信号:在线办公的基础设施正在经历一次“换引擎”的窗口期。过去我们想做一个在线表格,能选的东西很有限——要么直接用 Excel 的 Web 版,但它嵌入不了你自家应用;要么自己用 HTML 表格硬拼,但是性能、交互、公式能力全都跟不上。Univer 等于是在“开箱即用的桌面级体验”和“可编程的 SDK”之间开了一条新路。

此外,Univer 的视觉观感非常接近现代 Office,滚动、拖拽、区域选择、迷你图表的平滑度都做得不错。对于做 B 端产品的团队来说,默认 UI 足够体面,省去了一大批写交互的时间。再加上它是 MIT 或类似宽松协议开源(具体以仓库 LICENSE 为准),社区热度高,自然会被推到风口。

从技术圈热搜的特质来看,一个开源项目突然霸榜,往往不是因为代码写得好看,而是因为这个项目解决了一个“多数人都在头疼但没人做好”的通用问题。在线表格的组件化这件事,刚好踩中了这一波需求。

1.2 Univer 到底是什么,能干什么

Univer 不是一个复制粘贴的 Excel 皮肤,它是一套完整的电子表格引擎。核心能力大致包括:单元格数据模型、公式计算、条件格式、数据透视、图表、协同编辑预留机制、以及一套基于命令模式的撤销重做系统。开发者可以像使用一个普通 npm 包一样,把 Univer 装进自己的项目,然后通过 API 创建 Workbook、Sheet、Range、Formula,甚至自定义一套行业专用的单元格类型。

举个例子,我在一个内部数据管理项目里,需要让用户直接在浏览器里编辑一张包含员工信息、考勤记录、绩效评分的表格。如果用原生的<table>实现,光是处理单元格合并、缩放、冻结窗格就能写掉几万行代码。用 Univer 之后,核心操作变成了两件事:创建实例、注册数据。其他诸如滚动性能、选区绘制、粘贴 Excel 数据处理,都是框架内部已经解决的问题。

Univer 还内置了多语言、主题皮肤、富文本单元格、批量格式刷这类细节能力,配置项非常丰富。如果说 Excel 是一间精装房,那么 Univer 就是一套可以随意拆改墙体的清水房,你拿到的是一套结构完整但允许你自由定制的基本盘。

1.3 哪些场景适合用 Univer

从我的经验看,Univer 最适合三类场景。

第一类是企业级 SaaS 中的嵌入式表格。比如客户关系管理、项目管理、采购审批系统,这些产品通常都有“列表 + 编辑”的需求,与其自己写一个半吊子编辑器,不如直接嵌入 Univer 作为数据录入和展示的载体。

第二类是数据处理与配置类后台。很多运维平台、财务审核系统需要批量修改配置项,Univer 的单元格编辑、公式联动、数据校验能把这部分体验拉高一个档次。

第三类是教育类或协作类工具。比如在线教学作业表、轻量级数据采集工具,利用 Univer 作为基础组件,可以快速做出一个“看起来像 Excel”的协同界面,配合 WebSocket 还能实现多人同时编辑。

如果你只是想在博客里展示一个表格,那么杀鸡用牛刀;但如果你需要的是一个能承载复杂业务逻辑、能深度嵌入、能活十年还不被淘汰的表格底层,Univer 确实值得押注。

2. Univer 的技术底牌:从渲染到数据的架构设计

2.1 渲染层:为什么用 Canvas 而不是 DOM

老牌开源表格项目 Luckysheet 早期是用 DOM 模拟单元格来渲染的,一旦数据量超过几千行,滚动和重绘就会明显卡顿。Univer 把渲染层全面切换到 Canvas 2D,所有单元格、边框、文字、选区都绘制在同一张画布上,大幅减少了浏览器布局与样式重算的开销。

但这里要澄清一个常见误解:Canvas 不是“无脑快”。真正的性能优势来自 Univer 在画布之上做的一套“脏区域重绘”机制。它会把可视区域拆分成多个图层,只有发生变化的格子才会触发局部重绘,而不是整个表格都刷新。也就是说,你在滚动的时候,Univer 只绘制新进入视口的那部分单元格,再配合虚拟滚动按需渲染,表格的显示层就变得非常顺滑。

在我自己的压力测试里,Univer 渲染一万行二十列的表格数据,初次滚动基本能保持在 40 到 60 帧的水平。相比之下,之前用 DOM 表格的方案在同样数据量下帧率直接掉到十几帧,肉眼可见的卡顿。所以 Canvas 渲染这个选择,对在线表格来说是“质变”级别的。

当然,Canvas 也带来一个开发上的难点:你没法直接用浏览器 DevTools 去检查某个单元格的 DOM 节点。调试时得借助 Univer 提供的内部事件和调试面板,这一步很多新手会卡住,后面我单独说。

2.2 数据与命令:可回溯的表格状态

Univer 的核心数据层是自研的一套模型结构。它会维护一个 Workbook 对象,内部包含 Sheet、CellData、Range 等实体。所有对数据的修改,都不是直接操作对象属性,而是通过派发“命令”完成。命令会被记录进历史栈,所以撤销和重做天然可用。

这套命令模式的思路有点像 Git:你每次操作等同于一次 Commit,Univer 负责把 Commit 应用到数据模型上,同时记录逆操作。这样做的好处是,表格的任何状态变化都有完整的轨迹,后续要接入协同编辑或者操作日志,都会变得非常轻松。

我在二次开发过程中,经常需要扩展自定义操作,比如“根据某列的颜色批量给另一列赋值”。我只需要写一个自定义 Command,分发到对应的业务模块,Univer 就能把它纳入撤销链。相比直接修改单元格 value,这种方式更安全,也更容易定位问题。

2.3 插件化生态与模块边界

Univer 从代码结构上就做了严格的分层:@univerjs/core负责核心对象与命令;@univerjs/sheets负责表格领域逻辑;@univerjs/ui负责 UI 渲染;@univerjs/sheets-ui负责表格 UI 与核心的联动;还单独有公式引擎、协同协议等模块包。这种拆分方式决定了 Univer 天生适合二次开发。

你可以只引入 core 和 sheets,做一个无头表格引擎,完全自己控制界面;也可以引入全套 UI 包,几分钟内得到一个类似 Excel 的完整页面。模块之间的依赖通过依赖注入容器来管理,所以业务系统里可以按需注册模块,不会因为引入了表格功能就把整个包撑到离谱的体积。

这种模块化设计对团队协作同样很有意义:前端团队只需要关心 UI 层的封装,业务逻辑layer可以集中在 Core 之上,算法团队甚至可以单独编写公式插件而不影响渲染。模块边界清晰之后,项目的可维护性上限被拉高了一大截。

3. 从零开始:在项目里接入 Univer 的完整过程

3.1 安装依赖与初始化项目

先说一个经验之谈:接入 Univer 前,一定要确认你的包管理器能访问最新版本。建议用 pnpm,因为 Univer 的依赖树比较大,pnpm 的硬链接机制会让磁盘占用更友好,也不会出现 npm 常见的幽灵依赖问题。

我以一个基于 Vite + TypeScript 的空项目为例:

pnpm create vite my-univer-demo --template vanilla-ts cd my-univer-demo pnpm install @univerjs/core @univerjs/sheets @univerjs/ui @univerjs/sheets-ui

这里要特别说明,Univer 的版本迭代速度很快,早期版本里插件名是univer-sheets,现在基本都改成@univerjs/sheets这样带作用域的命名空间。如果你打开官方文档发现命令和我的略有出入,别慌,以你装的那个版本的 package.json 为准。当前文档以较新的模块化 API 为例。

3.2 核心代码:创建 Univer 实例

初始化 Univer 的代码并不复杂,但容易踩坑的点在于“模块注册顺序”,UI 模块应该在领域模块之前注册,因为领域模块的交互需要基于 UI 已经初始化完成。

来看核心代码:

import { Univer } from '@univerjs/core'; import { UniverUIModule } from '@univerjs/ui'; import { SheetsModule } from '@univerjs/sheets'; import { SheetsUIModule } from '@univerjs/sheets-ui'; import { UniverUIPlugin } from '@univerjs/ui-plugin'; // 视版本而定 const univer = new Univer(); univer.registerModule(new UniverUIModule({ container: document.getElementById('root')! })); univer.registerModule(new SheetsModule()); univer.registerModule(new SheetsUIModule()); const workbook = univer.createUniverSheet({ name: '我的第一张表格', sheetOrder: ['Sheet1'], sheets: [ { id: 'sheet-1', name: 'Sheet1', cellData: { '0': { '0': { v: '欢迎使用 Univer' } } } } ] });

这段代码创建了一个名为“我的第一张表格”的工作簿,并往第一个 Sheet 的 A1 单元格写入了内容。实际运行时,你会在页面上看到一个带工具栏、公式栏、行列表头的完整表格界面。

这里我特意提到“视版本而定”,因为 Univer 在 core 与 UI 的命名上改动过几次,我初学时就因为照抄旧文档导致模块类型不匹配,花了不少时间排查。最稳妥的做法是,初始化时以官方仓库examples目录里的最新示例为基准,而不是记忆我的代码。

3.3 接入公式、样式与自定义编辑

Univer 内置了一套公式引擎,支持 SUM、AVERAGE、IF、VLOOKUP 等常见函数。单元格里输入以=开头的公式就能直接计算,并且支持跨 Sheet 引用。更让我惊喜的是它的智能补全,输入函数名时会弹出参数提示,这个交互细节对 B 端用户非常友好。

给单元格设置样式,可以通过 API 实时操作。比如把 A1 的背景色设为黄色、字体设为加粗:

const workbook = univer.getUniverSheetInstance('univer-sheet'); const activeSheet = workbook?.getActiveSheet(); const range = activeSheet?.getRange(0, 0, 1, 1); range?.setStyle({ fill: { bgColor: '#FFFF00' }, font: { bold: true } });

这种基于 Range 的 API 风格,和 Excel 的对象模型非常接近,做过后台表格开发的人基本能无缝切换。

如果你需要自定义编辑器,比如在单元格里放一个下拉选择器、日期选择器,Univer 也预留了 editor 扩展接口。我做过一个生产计划表,要在“状态”列通过下拉框选择“排产中/已完成/延迟”,实现方式很简单:封装一个自定义编辑器插件,注册到对应列的编辑环境即可。由于涉及插件协议,代码量稍大,但官方示例都有模板。

3.4 本地化与主题定制

Univer 的默认界面语言是英文,切换中文语言需要先预置语言包。我习惯把初始化配置放到一个统一入口:

import { LocaleType } from '@univerjs/core'; import enUS from '@univerjs/ui/assets/locale/en-US'; import zhCN from '@univerjs/ui/assets/locale/zh-CN'; const univer = new Univer({ locale: LocaleType.ZH_CN, locales: { [LocaleType.ZH_CN]: zhCN, [LocaleType.EN_US]: enUS } });

如果项目里的设计系统与默认主题差异较大,Univer 支持通过 CSS 变量覆盖大部分视觉样式。你可以把表格的边框、表头背景、选区颜色统一成公司品牌色。这些变量名集中在 UI 包的样式变量文件里,改起来很顺手。

不过有一点需要注意:Univer 的自定义不完全等于 CSS 换肤。深层交互,比如右键菜单的项、工具栏按钮的显隐,都需要通过配置面板或插件 API 修改。如果只靠 CSS 强行隐藏某个按钮,虽然外观上达到了效果,但内部快捷键可能仍然触发,会在测试阶段暴露问题。

4. 进阶实战:把普通表格变成真正的“在线表格”

4.1 数据导入导出:如何处理 Excel 文件

Univer 对 xlsx 文件的支持依赖第三方解析/生成库,目前比较成熟的方式是配合xlsx-js-style或ExcelJS。我习惯在后端做上传解析,把解析后的二维数组通过 Univer 的 API 批量写入,这样更可控。

在前端单纯实现“导出当前表格为 xlsx”,可以这么做:

import * as XLSX from 'xlsx'; function exportWorkbook() { const workbook = univer.getUniverSheetInstance('univer-sheet'); const activeSheet = workbook?.getActiveSheet(); const data = activeSheet?.getRawData(); // 拿到二维数据 const ws = XLSX.utils.aoa_to_sheet(data); const wb = XLSX.utils.book_new(); XLSX.utils.book_append_sheet(wb, ws, 'Sheet1'); XLSX.writeFile(wb, 'export.xlsx'); }

需要注意,Univer 的getRawData()返回的是纯值,不会带上样式。如果你需要保留合并单元格、颜色、行高列宽等格式,就得遍历工作簿模型里的样表数据,再映射到 ExcelJS 的单元格对象。这块工作量不小,但也不是不能做。基础导出纯数据已经完全够用。

4.2 自动保存与本地持久化

在线表格一项不可或缺的能力是不丢失数据。最简单的持久化方案是监听工作簿的变更事件,防抖几秒后把数据序列化存储到本地数据库或后端接口。

Univer 的监听方式很优雅:

workbook.onCommandExecuted((command) => { // 不需要精确到每一次操作,防抖保存即可 debounce(saveSnapshot, 2000); }); function saveSnapshot() { const snapshot = workbook.getSnapshot(); // 完整的 JSON 快照 localStorage.setItem('univer-snapshot', JSON.stringify(snapshot)); // 或者 POST 到后端 }

getSnapshot()返回的是 Univer 工作簿的完整 JSON 表示,请求重载页面时,把这份 JSON 传给createUniverSheet作为初始化数据即可恢复。我做过一个离线台账小工具,用这个方案实现了浏览器端自动存档,测试了一个多月没有丢过数据。

这里提醒一个容易被忽略的问题:大表格频繁保存会浪费性能。不要直接监听每个命令后立刻序列化整个快照,建议用防抖 + 单例标记位,确保同时只有一个保存请求在运行。否则用户连续输入时,网络请求会堆积,最终拖死页面。

4.3 协同编辑的实现思路

说一句实在话,Univer 的开源版本并不像商业版那样“开箱即协同”。开源核心更多是提供了数据模型和命令体系,这些是协同编辑的基础。要实现多人同时编辑,你需要自己实现或集成一套同步协议。

目前社区里比较能落地的方案是:用 Yjs 作为 CRDT 底层库,自定义一个 Provider 将 Univer 的命令转换为 Yjs 的更新事件。大致的流程是:A 用户执行一个命令,Univer 先把命令应用到本地数据模型,同时把命令发送给协作后端;后端广播给其他用户;其他用户通过独立的应用接口解析命令,再应用到自己的本地模型。

Univer 的命令模式在这里充当了“可传输操作”的角色。因为每个操作都有确定的描述结构,即使没有 CRDT 那样高深的合并能力,只要后端做好编号和顺序控制,一个基于 OT 的简单协同也是可行的。

我的建议是:如果协同需求的并发冲突只出现在不同单元格区域,那么先做一个“单操作锁”就够了——同一时刻只允许一个用户的写操作。这种方案简单可靠,虽然无法做到像腾讯文档那样细粒度协同,但对绝大多数内部系统来说,已经明显区别于“只能排队编辑”的旧模式。

4.4 权限与用户管理规划

在线表格通常还需要配合权限体系。Univer 本身不强绑权限系统,但你可以在操作入口做控制。

我通常的做法是:通过一个状态字段控制表格是否为“只读模式”。只读模式下,关闭掉所有修改类命令的调用入口,比如右键菜单、快捷键、工具栏按钮,同时拦截命令执行事件,禁止任何修改类命令落库。

Univer 的命令系统也支持监听筛选:

univer.onBeforeCommandExecute((command) => { if (isReadOnly && command.type === 'edit') { return false; // 阻止命令执行 } });

配合后端在每次保存时校验文档版本号,可以防止用户绕过前端直接改数据。权限这块没有银弹,核心原则是“前端控制体验,后端控制安全”。

5. 性能优化与踩坑实录

5.1 大数据量表头与冻结窗格的表现

Univer 在数据量达到四五万行时,滚动性能依然不错,但有两个性能瓶颈不能忽视:一是初始数据加载耗时,二是首次主动渲染耗时。

如果你一次性把五万行数据写进 cellData,JSON 解析和模型构建都可能阻塞页面好几秒。解决办法是按需加载:先只加载可视区域的数据,滚动时再向后端请求后续数据。或者把大数据分割到多个 Sheet,用户可以自己切换。

另一个高频需求是冻结首行首列。Univer 的 API 支持:

const activeSheet = workbook.getActiveSheet(); activeSheet.setFreeze({ row: 1, column: 1 });

冻结窗格会让绘制层增加一层独立渲染,数据量大的情况下,务必测试低端设备的滚动表现。我自己在 i3 处理器的老笔记本上测试过,五万行数据配合冻结窗格,依然能保持在可操作的流畅范围内,但快速滚动时会出现轻微白屏闪烁,可以通过降低脏区域绘制频率缓解。

5.2 公式重算的异步化与计算链优化

公式引擎是 Univer 最容易出严重性能问题的地方。假设一个表格里有几千个 VLOOKUP,用户每次修改一个单元格,Univer 默认会同步重算所有依赖它的单元格,如果依赖链很长,界面会直接卡死几秒。

官方提供的方案是开启异步计算或延迟计算模式。我在项目中设置的是:

const config = { calculation: { calcOnDemand: true, maxAsyncJobs: 50 } };

这样用户输入时,公式会进入任务队列,视图优先响应用户操作,计算完成后异步更新单元格。这个体验优化立竿见影。

另外要注意,不要在公式中大量使用易变函数,即每次重算结果都不同的函数,比如RAND()、NOW()。一旦存在这类函数,Univer 整个计算链的脏标记范围会扩大,导致任意单元格变化都可能触发全局重算。能避免就避免。

5.3 常见报错与排查方法

我在接 Univer 的过程中遇到过几个经典问题,这里整理成速查表。

问题现象可能原因解决办法
页面空白但控制台无报错container 容器高度为 0给挂载节点显式设置宽度和高度,比如#root { height: 100vh; }
引入模块后 TS 类型不匹配版本之间 API 编号不一致锁定版本,参考同版本官方示例
表格能显示但工具栏缺失UI 模块注册顺序错误先注册UniverUIModule,再注册 sheets UI 模块
滚动时图形残影未在窗口 resize 时触发重新渲染监听window.resize并调用univer.getActiveWorkbook().getRenderer().refresh()
中文输入法下公式不更新IME 组合过程中触发了计算锁关闭“即时计算”,改成编辑完成后统一计算

这些坑都是我逐个踩过来的。前两个问题尤其隐蔽,因为浏览器控制台往往干干净净,但表格就是不出来。后来我才意识到是 CSS 根节点没有设定高度导致的 Canvas 渲染尺寸为 0。这种不算 bug 的问题,确实需要一点经验才能避免。

5.4 资源分包与首屏加速

Univer 全量打包体积不小,直接 import 全套 UI 模块会让首屏 JS 超过 1MB,在弱网环境下体验很差。建议采用 Vite 的动态导入,把 Univer 相关模块单独拆成 chunk,只有进入表格页面时才加载。

我通常在路由层这样处理:

const TableEditor = () => import('./components/TableEditor');

同时,把 Excel 解析库放在异步函数里执行,用不到时不引入。经过分包后,主包体积能降不少,表格页懒加载也能接受几百毫秒的白屏。如果能结合 CDN 缓存静态资源,效果更好。

6. 选型思考:Univer 到底适不适合你

6.1 同类方案横向对比

最近几年我也关注过 SpreadJS、Handsontable 和 Luckysheet。SpreadJS 商业成熟度最高,但授权费不低,适合有预算且不愿意折腾的企业。Handsontable 在数据网格领域很强,但它的定位更偏“可编辑表格”而不是“完整表格应用”。Luckysheet 虽然社区知名度高,但停维护的风险较大,而且底层架构相对旧。Univer 的优势在于:新架构、完善的数据模型、活跃的社区迭代。劣势是版本变更太快,文档有时赶不上代码。

拿一个我在报价场景中常用的例子来说明:如果客户要求必须支持桌面级体验和无限行列,且要做深度定制,我会推荐 Univer。如果客户只是需要一个展示数据的网格,那 Handsontable 反而更轻更快。

6.2 基于实际场景的建议

如果你的团队已经有了稳定可维护的前端工程体系,接入 Univer 的核心成本在“学习架构”和“处理版本问题”两块,没有想象中那么可怕。但如果你是个人开发者,只想做一个简单表格页面,我不建议直接引入 Univer。它更像是一台精密机床,而不是一把螺丝刀。用它之前,你得先确认自己确实有“制造零件”的需求。

最后我给一个实操建议:不管最终选型定在哪个方案,都要先在自己的业务场景里跑一个概念验证项目,核心验证三件事——大数据量下的滚动性能、常用公式的计算准确度、以及与自家后端接口的数据对接复杂度。把这三个点测完,再上生产决策会踏实很多。我自己的经验是,Univer 在处理这三件事时得分都很高,这也是我最终选择在核心项目里长期使用它的原因。后续如果你也在接入 Univer,遇到具体问题,欢迎在评论区一起交流,有些坑确实只有踩过的人才能一眼看出来。

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

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

立即咨询