前段时间打开技术社区,突然发现univer这个开源项目被反复挂在嘴边,甚至“univer在线”都成了搜索热词。很多人第一反应以为它只是个在线表格组件,实际上它是一个从底层渲染到协同逻辑全部开源的 Web 办公套件,表格是当前最完善的部分,文档和演示文稿也在持续迭代。它能解决什么问题?简单说,就是让你不用再被商业表格 SDK 的高额授权卡脖子,也不用在 Excel 和 Web 之间来回搬运,直接在浏览器里做出一个带公式、带图表、带多人协作的电子表格产品。适合的人群也明确:想自建低代码平台里的表格模块、需要做在线 Excel 类产品的团队,以及想研究开源办公软件底层实现的技术爱好者。
1. Univer是什么:先搞懂这个项目在解决什么问题
1.1 在线表格这个赛道的老大难
在线表格不像前端展示一个列表那么简单。早期方案要么用js-xlsx/SheetJS 这类库只做文件解析,界面还得自己用 DOM 表格拼;要么接入商业控件,功能倒是全,但授权费和定制空间往往让人头疼。我之前做过一个内部报表系统,里面最重的模块就是“仿 Excel 编辑区”,前端写了将近一万行代码,仍然绕不开滚动、选区、编辑框定位这些脏活。
Univer 的做法是直接把这些底层问题打包收走,把“表格能力”做成一个可以独立启动的 Web 应用核心。它不依赖 Excel 桌面环境,也不绑定某个框架——你可以在 React、Vue,甚至原生<div>容器里初始化。对于团队来说,核心收益有两个:一是省掉了从零手写网格渲染和选区交互的工程量;二是它把公式、格式、协同这些高级模块都拆成了可插拔的组件,你不需要一次性全上,按业务需要裁切就行。
这个定位在开源领域其实是个空白。国外有类似产品,但部署层面对国内用户并不友好;国内大多数开源项目停留在“解析 Excel 文件”阶段,离“在线编辑”还很远。Univer 算是一条鲇鱼,把整个赛道的技术门槛拉低了一截。
1.2 技术架构:为什么选择自研 Canvas 渲染
我第一次看 Univer 源码时,最惊讶的是它没有用常规的 DOM 表格来渲染单元格,而是自己实现了基于 Canvas 的渲染引擎。为什么要费劲去画?你去操作一张上万行、几十列的表就能感受到:DOM 节点数量一旦上来,滚动、选区、编辑框都会触发大量重排,页面会卡得让人怀疑人生。
Canvas 的思路更像游戏引擎:把表格画在京布线制区域上,鼠标键盘事件通过坐标换算命中单元格,滚动和缩放只触发局部重绘,不涉及 DOM 增删。这样在大数据量场景下,性能模型稳定得多。代价也很明显:文本绘制、选区框、自动换行、合并单元格都要自己实现,工作量成倍增加。Univer 愿意在这部分重投入,说明它瞄着的不是“能看文件”的弱需求,而是“能办公”的强需求。
从架构分层看,Univer 把 UI 层和核心逻辑层拆得很开。命令系统负责所有操作,包括输入、删除、格式调整、筛选等;公式引擎独立计算依赖关系;协同模块通过 CRDT 同步状态。这种分层让多人协作、插件扩展、二次开发都变得可预测,而不是一锅粥里什么都往main.ts塞。
2. 核心功能拆解:Univer到底能做什么
2.1 表格能力快览:半个 Excel 不一定夸张
先别被“开源”俩字吓住,觉得功能一定很简陋。我实际体验下来,Univer 的表格已经覆盖了绝大多数日常办公场景:
- 单元格格式:字体、对齐、边框、背景、数字格式
- 行列操作:插入、删除、隐藏、冻结窗格、调整宽高
- 数据处理:排序、筛选、文本分列、数据验证
- 公式函数:内置大量常用函数,支持跨工作表引用和数组公式
- 条件格式:按规则高亮单元格
- 图表:基于表格数据生成常见图表类型
- 数据透视表:基础分组、汇总能力
- 撤销重做、复制粘贴、查找替换、批注
其中公式引擎是值得单独说说的部分。它不是简单调用 JS 的eval,而是对公式做了词法解析、依赖管理和循环引用检测。像SUMIFS、VLOOKUP这类复杂函数也能稳定计算。对于前端团队来说,最直接的好处是——不需要再拿纯前端公式库去补齐 Excel 90% 的函数了,Univer 已经帮你做了一个底子。
2.2 协同编辑:从“能看到”到“能实时改”
在线表格最难的场景不是展示,而是多人同时编辑。如果只是显示结果,轮询接口就够;但要实现多人同时改同一个单元格、互相看到光标,就必须解决状态冲突。
Univer 的协同方案基于 CRDT 思路,我用大白话解释一下:每个人编辑时,改的不是“最终值”,而是一串可以被合并的操作记录。这些操作通过 WebSocket 广播给协作端,各端用自己的规则合并,最终收敛到一致状态。对比传统 OT 锁机制,CRDT 的好处是天然支持网状同步,不需要一个中心节点串行处理所有操作,扩展性和离线编辑能力都更好。
你在界面上能直接感受到的就是:对方的实时选中区块、正在编辑的单元格内容、以及多人并发时数据不会互相覆盖。不过要注意,官方提供的协同模块偏向“能力开放”,消息通道需要你们自己搭。对于有小团队协作需求的产品,可以先把文件级同步做好,再做单元格级协同,避免早期复杂度失控。
2.3 插件体系:改菜单、加命令、换外观
Univer 的另一个我不是很舍得分享的优点是插件化,因为它真的是“按需加载”。默认你启动一个最小表格,装的就是核心、渲染、表格编辑这几个包。公式、协同、图表、打印、粘贴增强,都是单独插件。这意味着你的首屏体积可以很干净,不用的功能不进产物。
插件能做的事情也相当多。比如你想在工具栏加一个“导出 JSON”按钮,不需要去 Hack Univer 内部代码,只需要实现一个插件注册菜单项,点击时调用命令服务拿到当前工作表数据,再自行序列化即可。我举个例子:
class ExportJsonPlugin { onMounted(univer: Univer): void { univer.getRegistry().registerPlugin({ name: 'export-json', toolbar: { label: '导出JSON', onClick: () => { const sheet = univer.getActiveSheet(); const data = sheet.getData(); console.log(JSON.stringify(data)); }, }, }); } }这属于最常见的扩展范式。更复杂的场景,比如自定义单元格渲染、想做批注系统的样式换皮,也都能通过插件钩子切入。对我来说,插件化最大的价值是团队可以并行开发:“表格内核”保持稳定,业务定制各自独立,避免合并代码冲突。
3. 从零接入Univer:一个能跑起来的最小环境
3.1 环境准备与依赖安装
先说明一下,Univer 还在快速迭代期,包名和 API 偶尔会有调整,我在下面写的是目前主流的接入方式,实际以官方文档为准。整体思路是:用 Vite 搭一个前端工程,通过官方预设(Preset)快速启动表格。
Node 版本建议 18 以上,因为新版工具链和若干依赖对 Node 版本有要求。老项目也别慌,Univer 有 UMD 版本可以直接通过<script>引入,但带构建工具的工程化方案更利于做二次开发。
初始化项目的步骤:
# 1. 用 Vite 创建一个 TypeScript 模板 npm create vite@latest univer-demo -- --template vanilla-ts # 2. 进入目录并安装基础依赖 cd univer-demo npm install # 3. 安装 Univer 相关依赖 npm install @univerjs/presets @univerjs/presets-sheets第一遍跑不起来很正常,多数问题出在源码模板的“空页面”上。你先执行npm run dev,确认 Vite 能启动,再去填 Univer 的代码。
3.2 最小可运行的表格实例
在index.html里留一个容器:
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8" /> <title>Univer Demo</title> </head> <body> <div id="app" style="height: 100vh; width: 100vw;"></div> <script type="module" src="/src/main.ts"></script> </body> </html>然后在src/main.ts写入:
import { Univer } from '@univerjs/presets'; import { UniverPresetSheets } from '@univerjs/presets-sheets'; const univer = new Univer({ theme: { primaryColor: '#3574f0', }, locales: ['zhCN'], }); const sheet = univer.createUniverSheet( new UniverPresetSheets({ container: document.getElementById('app')!, toolbar: true, statusbar: true, }) ); sheet.createSheet({ name: '示例', rows: 100, columns: 20, defaultRowHeight: 28, defaultColumnWidth: 90, });这段代码里,Univer是根实例,负责初始化核心服务和插件注册;UniverPresetSheets是表格预设,包含了表格编辑所需的基础插件;createSheet会创建第一个工作表,同时指定行列数。这里的关键是container必须指向一个有明确宽高的元素,否则 Canvas 渲染引擎算不出可视区域,页面大概率会白屏。
3.3 关键配置参数与工作簿初始项
Univer 的配置项不算少,我整理几个最常用也最容易影响初始体验的:
| 配置项 | 作用 | 推荐设置 |
|---|---|---|
container | 挂载点 DOM 元素 | 必须有非零宽高 |
locales | 界面语言 | 中文环境填['zhCN'] |
theme | 主题色/UI 样式 | 按品牌色覆盖primaryColor |
toolbar | 是否渲染工具栏 | 内部工具可先开,后期按需关闭 |
statusbar | 状态栏显示缩放/行列信息 | 默认开也可 |
sheet行列数 | 初始工作表尺寸 | 按数据规模估,不需要一次给太大 |
frozen | 默认冻结区域 | 表头行较多时很好用 |
我一开始把rows设成了 100000,想着“反正 Canvas 能扛”,结果首屏加载明显变慢。原因很简单:虽然渲染是 Canvas,但初始化时仍要建立单元格数据模型,过大的初始行列数会带来不必要的内存开销。经验是先给一个适中尺寸,运行过程中再通过 API 动态增加行列,效果最稳。
4. 接入 Univer 的避坑记录:常见问题与排查技巧
4.1 页面空白、Canvas 没画出来怎么办
这是接入时遇到最多的问题。我刚接入时页面就是一片空白,一度以为包装错了。后来总结出三个排查点:
- 容器高度是否有效。
height: 100%在 React 或 Vue 应用里经常因为父级塌陷变成 0,尽量在根节点上用100vh或固定像素。 - CSS 是否被引入。Univer 的 UI 层依赖样式文件,如果只装了 JS 包没引样式,工具栏和画布区域都不会正常显示。检查构建产物里有没有对应的
.css文件被加载。 - 包版本是否匹配。Univer 是一个 Monorepo 多包项目,不同小版本之间可能有 API 差异。混装
@univerjs/presets的 0.4 版本和@univerjs/sheets的 0.3 版本,很容易在运行时悄悄报错。
在浏览器控制台里看到类似Cannot read properties of undefined的错误时,先别急着看业务代码,优先把 timeline 切到 Network 面板,看看 JS 和 CSS 资源是不是有 404,资源加载失败会直接导致初始化中断。
4.2 同版本号强迫症:Monorepo 带来的依赖雷区
Univer 拆了很多子包,比如@univerjs/core、@univerjs/sheets、@univerjs/ui、@univerjs/engine-render。这些包在发布时会同步打版本号,理论上应该严格对齐。
但实际项目中很常见的情况是:手动装了一个新包,比如@univerjs/charts,它默认带的是最新版,而你之前已经装的@univerjs/sheets还是老版本。一旦内部模块引用了新增类型或渲染协议,运行时的错误就会像抽风一样随机出现,而且报错栈根本不指向你的业务代码。
我的建议是:所有@univerjs/*相关的依赖,要么全部用最新版,要么全部锁同一个版本,不要只锁某一个。升级时优先运行官方提供的升级脚本或参考迁移日志,很多破坏性变更只在文档里提了一句,但不升级就会掉坑。
4.3 协同开启之后,鉴权和服务端状态管理
开启协同编辑不等于开箱即用。Univer 的协同模块更多是客户端协议支持,真正负责消息分发的服务端还是要自己维护。如果你们团队是第一次做在线编辑,我建议先想清楚三件事:
- 连接鉴权:WebSocket 握手时怎么验证用户身份?推荐在建立连接时携带 token,服务端完成校验后再允许订阅工作表变更。
- 文档权限:不同用户可能对同一张表拥有读、写、管理权限,Univer 本身不会帮你判断“这个用户能不能改这一列”,这些规则要在业务层做。
- 状态持久化:CRDT 同步的是“实时状态”,一旦所有人都离线,数据要不要落库?建议在服务端周期性保存一份快照,同时记录增量操作,避免整个文档重放消耗过大。
我见过最典型的翻车是:协同服务端只做了广播,没有做持久化,结果用户一刷新,所有操作全没。虽然 Univer 客户端支持重放操作恢复状态,但那要求服务端把历史操作都保留下来,不能简单“转发一次就丢”。
4.4 大数据量下的性能调优经验
如果你要处理几万行甚至几十万行的数据,Univer 虽然比 DOM 表格强很多,但也不是无脑堆数量就行。我的实际调优经验有三条。
第一,能用冻结窗格就用冻结窗格。冻结后滚动时只重绘可见区域,数据量越大收益越明显。第二,避免频繁“整体写”公式或样式,把多次修改合并成一次批量命令提交。比如用setRangeValues一次性写入多列,比循环调用单格赋值快很多。第三,如果只是做数据展示,不要求编辑,可以考虑关闭工具栏和编辑链,只保留渲染和 Excel 导入导出,能省掉相当一部分开销。
数据导入也是一块。大文件解析会阻塞主线程,最好放到 Web Worker 里做,解析完再一次性把数据塞给 Univer。官方 Demo 里有相关参考,你直接照搬比重新实现稳妥。
最后留一个我习惯的做法:任何一次 Univer 升级之后,先跑一遍官方仓库里的 Playground 用例,再接入业务。它的渲染层和命令 API 改动频繁,直接把旧页面和最新包混装最容易翻车。等确认基础用例无误,再小范围灰度你的真实业务,比盲目升到生产环境少踩很多坑。