1. Univer 到底是个什么东西
第一次听到 Univer 这个名字,很多人会以为是某个新出的前端框架或者 UI 库。其实不是。Univer 是一套开源的在线电子表格与文档协作引擎,核心定位是让开发者能在自己的产品里嵌入一套类似在线表格的能力——单元格编辑、公式计算、多 Sheet 管理、协同编辑、导入导出,这些原本需要从零造轮子的事情,Univer 帮你把底座搭好了。
我最早接触它是因为一个内部数据填报系统的需求。业务方想要一个“能在线编辑、能算公式、能多人同时改”的表格页面,但又不愿意直接嵌第三方商业产品。找了一圈方案,最后落在 Univer 上。用下来的感受是:它的架构设计比想象中要清晰,Facade API 这层封装做得很聪明,但上手门槛也确实存在,尤其是对不熟悉 Canvas 渲染和 Node.js 工具链的同学来说,前期配置阶段容易卡住。
这篇文章我打算把 Univer 从“是什么”到“怎么跑起来”再到“怎么避坑”完整讲一遍。适合三类人看:一是想给产品加在线表格能力的前端开发者;二是需要做数据协作平台的技术负责人;三是对 Canvas 渲染引擎和 SDK 设计感兴趣、想研究一下别人怎么造轮子的同学。不管你是刚听说 Univer,还是已经跑过 demo 但卡在某个环节,下面这些内容应该都能帮到你。
2. 核心架构拆解:为什么 Univer 要这么设计
2.1 从“表格”到“引擎”的思维转变
很多人第一次用 Univer 会不习惯,因为它不是一个“组件库”,而是一个“引擎”。组件库的思路是:我给你一个<Table />,你传数据进去,它渲染出来。引擎的思路是:我给你一套运行时,你告诉它数据长什么样、要渲染成什么样、用户操作后怎么响应,它来驱动整个流程。
这个区别很关键。组件库适合做展示,引擎适合做编辑。在线表格的本质是什么?是用户在一个巨大的二维网格上不断做增删改查,每次操作都可能触发公式重算、依赖更新、视图重绘、协同同步。这些事情如果全塞在一个 React 组件里,状态管理会爆炸。Univer 的做法是把这些职责拆开,用一套插件化的架构来组织。
它的核心分层大致是这样的:
- 底层渲染层:基于 Canvas 做绘制,不依赖 DOM 节点来承载每个单元格。这是它能扛住大表格的关键。
- 数据模型层:管理 Workbook、Worksheet、Cell、Range 等数据结构,负责公式计算和依赖追踪。
- 服务层:包括命令系统、撤销重做、协同、导入导出等能力。
- Facade API 层:面向使用者的统一入口,把底层复杂度封装起来。
你作为使用者,日常打交道最多的就是 Facade API。但理解下面这几层的关系,能帮你在出问题时知道该往哪个方向排查。
2.2 Canvas 渲染:为什么不用 DOM
这是被问得最多的一个问题。用 DOM 渲染表格不是不行,但有几个硬伤。
第一是节点数量。一个 1000 行 × 50 列的表格就是 5 万个单元格,如果每个单元格是一个 DOM 节点,浏览器的布局和重绘压力会非常大。Canvas 是一张画布,所有单元格画上去,节点数量始终是一,性能曲线完全不一样。
第二是渲染控制粒度。Canvas 让你可以精确控制每一帧画什么、怎么画。比如滚动时只重绘可视区域,比如单元格选中态用叠加层绘制而不是改 DOM 样式。这些优化在 DOM 方案里做起来很别扭。
第三是一致性。DOM 渲染在不同浏览器、不同缩放比例下的表现会有差异,Canvas 绘制出来的结果更可控。
当然 Canvas 也有代价。最直接的就是:你没法用浏览器的开发者工具直接选中某个单元格看它的 DOM 结构,调试方式完全不同。另外无障碍访问、文本选择、输入法这些能力都需要额外处理。Univer 在这些方面做了不少工作,但你心里要有数,Canvas 方案不是银弹。
2.3 Facade API:一层聪明的封装
Facade 这个词在软件设计里指的是“外观模式”——用一个统一接口把一堆复杂子系统包起来。Univer 的 Facade API 就是这个角色。
它聪明在哪?它没有把底层 API 直接暴露给你,而是重新组织了一套更符合使用者直觉的调用方式。比如你要创建一个工作簿、拿到某个工作表、往某个单元格写值,用 Facade API 写出来大概是这样:
const univer = new Univer(); const workbook = univer.createUniverSheet({}); const worksheet = workbook.getActiveSheet(); const range = worksheet.getRange(0, 0); range.setValue('hello');这套 API 的好处是语义清晰,不需要你了解底层命令系统怎么发、事件怎么订阅。但它也有个特点:很多操作是异步的或者有副作用的,如果你按同步思维去写,容易踩坑。这个后面会细说。
2.4 插件化架构带来的灵活性
Univer 的功能不是全塞在核心里,而是拆成一个个插件。公式计算是一个插件,协同是一个插件,导入导出是一个插件,甚至条件格式、数据验证这些也是插件。
这种设计的好处是你可以按需引入。如果你的场景只需要基础编辑,不需要协同,那协同相关的代码就不会进你的包。坏处是配置项变多了,你得知道哪些插件要开、哪些不用开。新手容易在这一步懵。
3. 环境搭建:从零把 Univer 跑起来
3.1 Node.js 环境准备
Univer 的开发环境依赖 Node.js。这里有个坑要先说:版本选择很重要。太老的版本跑不了新的构建工具,太新的版本又可能和某些依赖不兼容。我实测下来,Node.js 18 LTS 和 20 LTS 都比较稳,22 版本也能跑但偶尔会遇到依赖警告。
安装 Node.js 的步骤不复杂,但有几个细节值得注意:
- 去官网下载对应系统的 LTS 版本安装包。Windows 用户选
.msi,macOS 用户选.pkg,Linux 用户可以用包管理器或者二进制包。 - 安装时勾选“添加到 PATH”,这样命令行里能直接调用
node和npm。 - 装完后打开终端,运行
node -v和npm -v确认版本号能正常输出。
如果你之前装过旧版本,建议先卸载干净再装新的,避免 PATH 里残留旧版本导致命令冲突。Windows 上可以用where node查看当前用的是哪个路径下的 node,macOS 和 Linux 用which node。
提示:国内网络环境下 npm 安装依赖可能较慢,可以配置镜像源加速。但具体用哪个源、怎么配,建议根据你所在网络环境自行判断,这里不展开。
3.2 创建项目与安装依赖
环境好了之后,建一个空项目目录,初始化 npm:
mkdir univer-demo cd univer-demo npm init -y然后安装 Univer 的核心包。Univer 的包名是@univerjs/开头的,核心包包括:
npm install @univerjs/core @univerjs/design @univerjs/engine-formula @univerjs/engine-render @univerjs/sheets @univerjs/sheets-ui @univerjs/ui这里解释一下每个包的作用,方便你按需取舍:
| 包名 | 作用 | 是否必需 |
|---|---|---|
| @univerjs/core | 核心运行时,包含 Facade API | 必需 |
| @univerjs/design | 设计系统,样式变量和基础组件 | 建议 |
| @univerjs/engine-formula | 公式计算引擎 | 需要公式则装 |
| @univerjs/engine-render | Canvas 渲染引擎 | 必需 |
| @univerjs/sheets | 表格数据模型 | 必需 |
| @univerjs/sheets-ui | 表格 UI 交互 | 必需 |
| @univerjs/ui | 通用 UI 框架 | 必需 |
如果你还要导入导出 Excel,需要额外装@univerjs/sheets-import和@univerjs/sheets-export。要协同的话还有协同相关的包。建议一开始只装核心的,跑通了再逐步加。
3.3 最小可运行示例
装完依赖后,写一个最简单的入口文件。假设你用 Vite 做构建,先装 Vite:
npm install -D vite然后创建index.html和main.js:
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Univer Demo</title> <style> html, body { margin: 0; padding: 0; height: 100%; } #app { height: 100%; } </style> </head> <body> <div id="app"></div> <script type="module" src="/main.js"></script> </body> </html>import { Univer } from '@univerjs/core'; import { UniverSheetsPlugin } from '@univerjs/sheets'; import { UniverSheetsUIPlugin } from '@univerjs/sheets-ui'; import { UniverUIPlugin } from '@univerjs/ui'; import { UniverRenderEnginePlugin } from '@univerjs/engine-render'; import { UniverFormulaEnginePlugin } from '@univerjs/engine-formula'; const univer = new Univer(); univer.registerPlugin(UniverRenderEnginePlugin); univer.registerPlugin(UniverFormulaEnginePlugin); univer.registerPlugin(UniverUIPlugin, { container: 'app', }); univer.registerPlugin(UniverSheetsPlugin); univer.registerPlugin(UniverSheetsUIPlugin); univer.createUniverSheet({});跑npx vite启动开发服务器,打开浏览器应该能看到一个空表格。如果能看到,恭喜你,环境通了。如果白屏,先看控制台报错,大概率是插件注册顺序或者容器 ID 对不上。
3.4 插件注册顺序的坑
这里要单独说一下插件注册顺序。Univer 的插件之间有依赖关系,比如 UI 插件依赖渲染引擎,表格 UI 依赖表格核心。如果你注册顺序不对,可能不报错但功能异常。
我踩过的坑是:先注册了UniverSheetsUIPlugin再注册UniverUIPlugin,结果表格渲染出来了但工具栏没出来。后来调整顺序,把 UI 和渲染引擎放前面,表格相关放后面,就正常了。
一个比较稳的顺序是:渲染引擎 → 公式引擎 → UI → 表格核心 → 表格 UI。当然具体还要看你装了哪些插件,原则是“被依赖的放前面”。
4. Facade API 实操:常用能力与代码示例
4.1 工作簿与工作表操作
Facade API 里,univer.createUniverSheet()返回的是一个工作簿对象。你可以通过它拿到当前活动的工作表,也可以按 ID 或索引获取指定工作表。
const workbook = univer.createUniverSheet({}); // 获取活动工作表 const activeSheet = workbook.getActiveSheet(); // 获取所有工作表 const sheets = workbook.getSheets(); // 按名称获取 const sheet = workbook.getSheetByName('Sheet1'); // 新建工作表 const newSheet = workbook.createSheet('MySheet'); // 删除工作表 workbook.deleteSheet(newSheet.getSheetId());工作表对象上可以拿到行数、列数、名称等元信息。注意行号和列号都是从 0 开始的,这点和 Excel 的 1 开始不一样,写代码时容易搞混。
4.2 单元格读写与区域操作
单元格操作是最高频的。Facade API 提供了getRange方法来定位区域,然后调用getValue/setValue。
const worksheet = workbook.getActiveSheet(); // 单个单元格 const cell = worksheet.getRange(0, 0); cell.setValue('姓名'); // 批量写入一个区域 const range = worksheet.getRange(0, 0, 2, 3); // 起始行, 起始列, 行数, 列数 range.setValues([ ['姓名', '年龄', '城市'], ['张三', 28, '北京'], ]); // 读取 const values = range.getValues(); console.log(values);getRange的参数顺序是(startRow, startColumn, numRows, numColumns)。如果你只传两个参数,就是单个单元格。这个重载设计还算直观,但参数多了容易记混,建议写的时候加注释。
4.3 公式与计算
公式引擎插件注册后,你可以在单元格里写公式,Univer 会自动计算。
worksheet.getRange(0, 0).setValue(10); worksheet.getRange(1, 0).setValue(20); worksheet.getRange(2, 0).setFormula('=SUM(A1:A2)');注意公式里的单元格引用用的是 Excel 风格的 A1、B2,不是行列索引。这个转换由引擎内部处理。
公式计算是异步的,如果你 setFormula 之后立刻 getValue,可能拿到的是旧值或者空值。正确做法是监听计算完成事件,或者用await等待。这一点官方文档里写得不算特别显眼,但实际开发中很容易踩。
4.4 样式与格式设置
Facade API 也支持设置单元格样式,比如字体、颜色、对齐、边框等。
const range = worksheet.getRange(0, 0, 1, 3); range.setFontWeight('bold'); range.setBackgroundColor('#f0f0f0'); range.setHorizontalAlignment('center');样式设置是批量生效的,对一个区域设置样式比逐个单元格设置效率高很多。如果你要做条件格式,Univer 有单独的条件格式插件,不要用样式 API 硬做。
4.5 事件监听与响应
Univer 有一套事件系统,你可以监听用户操作、数据变化等。
univer.onCommandExecuted((command) => { console.log('命令执行:', command.id); });事件系统是协同功能的基础,也是你做自定义扩展的入口。但要注意,事件回调里不要做太重的事情,否则会阻塞主线程影响编辑体验。重逻辑建议放到防抖或异步队列里。
5. 常见问题与排查技巧实录
5.1 白屏或表格不显示
这是最高频的问题。排查顺序建议如下:
- 打开浏览器控制台,看有没有报错。常见的是模块找不到、插件未注册、容器 ID 不对。
- 确认
container参数传的是 DOM 元素的 id 字符串,不是元素本身。 - 确认容器元素有明确的宽高。如果父元素高度是 0,Canvas 画出来也看不见。
- 确认插件注册顺序正确,被依赖的插件先注册。
- 如果用了框架(React/Vue),确认 Univer 初始化是在 DOM 挂载之后执行的。
5.2 公式不计算或计算结果不对
先确认公式引擎插件注册了。然后检查公式字符串格式,比如=SUM(A1:A2)前面的等号不能少,区域引用要用冒号。如果公式引用了空单元格,结果可能是 0 而不是报错,这是正常行为。
还有一个容易忽略的点:如果你是通过setValue写入的数字,但写成了字符串'10',公式计算时可能按文本处理导致结果异常。确保数值类型正确。
5.3 大表格卡顿
Univer 本身对大数据量做了优化,但如果你一次性写入几万行数据,还是可能卡。建议:
- 分批写入,每批几百行,用
requestAnimationFrame或setTimeout错开。 - 关闭不必要的插件,减少运行时开销。
- 如果只是展示不需要编辑,考虑用只读模式。
5.4 导入导出 Excel 失败
导入导出依赖额外的插件包,确认装了@univerjs/sheets-import和@univerjs/sheets-export并注册。另外文件格式要匹配,.xlsx和.xls处理方式不同。如果文件里有复杂公式或图表,导入后可能有偏差,这是正常现象,不要期望 100% 还原。
5.5 常见问题速查表
| 问题现象 | 可能原因 | 排查方向 |
|---|---|---|
| 白屏 | 插件未注册/容器问题 | 看控制台报错,检查容器宽高 |
| 公式不计算 | 引擎未注册/格式错误 | 确认插件,检查公式字符串 |
| 样式不生效 | 区域参数错误 | 确认 getRange 参数顺序 |
| 大表格卡顿 | 一次性写入过多 | 分批写入,关闭多余插件 |
| 导入失败 | 插件缺失/格式不支持 | 确认插件注册,检查文件格式 |
| 事件不触发 | 监听时机太晚 | 在初始化后立即注册监听 |
6. 一些实操心得与扩展思路
用 Univer 做项目,有几个经验我觉得值得分享。
第一,不要一上来就追求功能全。先把最小可运行版本跑通,确认渲染、编辑、公式这三块没问题,再逐步加协同、导入导出、条件格式。每加一个能力就验证一次,出问题容易定位。
第二,Facade API 虽然好用,但遇到复杂需求时可能需要下沉到更底层的 API。这时候要理解 Univer 的命令系统和事件机制,不然会卡住。建议花点时间看看它的架构文档,哪怕不写底层代码,理解设计思路对排查问题也有帮助。
第三,Canvas 渲染的调试方式和 DOM 完全不同。你没法用元素审查工具看单元格,但可以用 Univer 提供的调试工具或者自己在渲染层打日志。另外性能分析要用 Canvas 相关的工具,不要用 DOM 的那套。
第四,协同功能不是开箱即用的。Univer 提供了协同的底层能力,但你需要自己接后端服务、处理冲突、管理用户状态。如果你的场景只是单人编辑,完全不用引入协同插件,能省不少事。
这个内容后续还可以往几个方向扩展:一是自定义插件开发,把业务特有的单元格类型或操作封装成插件;二是和现有后端系统对接,做数据持久化和权限控制;三是性能优化专题,针对超大数据量的场景做专项调优。每个方向都够单独写一篇了,后面有机会再展开。