1. Univer 到底是个什么东西
第一次听到 Univer 这个名字,很多人会以为是某个新出的前端框架或者 UI 库。其实不是。Univer 是一套开源的在线电子表格与文档协作引擎,核心定位是让开发者能够把“类 Excel”“类 Google Sheets”的能力嵌入到自己的产品里。它不是一个成品应用,而是一套 SDK 加 Facade API 的组合,底层用 Canvas 做渲染,上层用 Facade API 暴露简洁的调用接口,运行时依赖 Node.js 做构建和本地服务。
我最初接触 Univer 是因为一个内部数据看板项目,业务方要求“表格能像 Excel 一样编辑,还要支持多人同时改”。当时评估过几条路线:一是直接嵌第三方在线表格,二是自己用 Canvas 从零画,三是找一套现成的表格引擎。第一条路线数据不在自己手里,第二条路线工作量太大,最后落到 Univer 上。实测下来,它在“可嵌入”和“可扩展”这两点上确实做得比较扎实。
这篇文章适合三类人看:第一类是想在自家产品里加表格能力的前端工程师;第二类是对Canvas 绘图引擎感兴趣、想了解大规模表格怎么渲染的技术爱好者;第三类是做Node.js 全栈、需要一套本地可跑可改的协作表格方案的独立开发者。不管你之前有没有用过类似的 SDK,只要跟着思路走,都能把 Univer 跑起来并理解它为什么这么设计。
2. 整体架构与设计思路拆解
2.1 为什么是 SDK 而不是成品应用
Univer 选择以 SDK 形态交付,这个决策背后有很现实的考量。在线表格这个赛道,成品应用已经很多了,但真正能把表格内核抽出来、让开发者自由嵌入的并不多。SDK 形态意味着它不绑定你的业务逻辑,你可以把它塞进 CRM、塞进报表系统、塞进低代码平台,甚至塞进一个笔记应用里当数据录入组件。
从工程角度看,SDK 形态对架构的要求反而更高。成品应用可以为了某个功能写死逻辑,SDK 不行,它必须把“可配置”做到骨子里。Univer 的做法是把整个表格拆成若干插件(Plugin),每个插件负责一块能力,比如公式计算、条件格式、协同编辑。你用到哪个就注册哪个,不用的一律不加载。这种设计在打包体积和运行时性能上都更友好。
提示:SDK 形态的项目,文档里往往不会把所有插件都列全,实际使用时建议先看官方示例仓库里注册了哪些插件,再按需裁剪。
2.2 Canvas 渲染为什么是必选项
表格看起来简单,但一旦数据量上去,DOM 渲染就会崩。你可以做个实验:用原生 table 标签渲染一万行十列的数据,滚动一下就能感受到明显的卡顿。原因在于 DOM 节点数量爆炸,浏览器布局和重绘的开销随节点数线性增长。
Canvas 的思路完全不同。它把整个表格画在一张画布上,无论多少行多少列,DOM 里始终只有一个 canvas 元素。滚动、选中、编辑这些交互,靠的是坐标计算和局部重绘,而不是操作 DOM。Univer 选择 Canvas 作为渲染层,本质上是为了支撑大数据量 + 高交互频率这个场景。
当然 Canvas 也有代价。它没有 DOM 那样的可访问性支持,屏幕阅读器读不了;文本选择和复制粘贴需要自己实现;调试起来也不像 DOM 那么直观。Univer 在这些地方做了不少补丁,比如自己维护选区模型、自己处理剪贴板事件。这些细节在后面实操部分会展开。
2.3 Facade API 的设计哲学
Facade 这个词在软件工程里指“门面模式”,意思是把复杂的内部结构包一层简单的接口。Univer 的 Facade API 就是这个角色。底层有工作簿、工作表、单元格、样式、公式等一堆对象,如果直接暴露给使用者,学习成本会很高。Facade API 把它们收拢成几个核心入口,比如univerAPI.getActiveWorkbook()、worksheet.getRange()这种链式调用。
我比较欣赏的一点是,Facade API 的命名和 Excel 的 VBA 对象模型有相似之处,用过 Excel 宏的人上手会快很多。比如取一个区域、设值、设样式,几乎能一一对应。这种“借力已有心智模型”的设计,比自创一套命名体系要聪明。
2.4 Node.js 在整条链路里的位置
Univer 本身是跑在浏览器里的,但它的开发、构建、本地调试都离不开 Node.js。官方推荐用 Node.js 18 LTS 或更高版本,我实测 18.20.4 和 20.x 都能正常跑。Node.js 在这里承担三个角色:一是包管理,用 npm 或 pnpm 装依赖;二是构建工具链,Vite 或 Webpack 都行;三是本地开发服务器,起一个带热更新的环境方便调试。
如果你之前没装过 Node.js,建议直接去官网下 LTS 版本,不要用系统包管理器里那个可能过时的版本。装完之后用node -v和npm -v确认一下,版本对不上后面会出各种奇怪的错。
3. 核心细节解析与实操要点
3.1 环境准备:Node.js 与包管理器
先把地基打好。Node.js 安装本身没什么难度,但有几个坑我踩过。第一,Windows 上如果之前装过旧版本,最好先卸载干净再装新的,否则可能出现node命令指向旧路径的情况。第二,macOS 上用 Homebrew 装的 Node.js 有时候会和 nvm 冲突,建议二选一。第三,公司网络如果有代理,npm 装包可能会卡住,这时候需要配 registry。
装好之后,包管理器我推荐 pnpm。Univer 的依赖树比较深,pnpm 的硬链接机制能省不少磁盘空间,安装速度也快。如果你习惯 npm 也没问题,只是 node_modules 会大一些。
node -v # 期望输出 v18.20.4 或更高 npm install -g pnpm pnpm -v注意:不要用
sudo npm install -g在 macOS 或 Linux 上全局装包,权限问题后面会很麻烦。用 nvm 管理 Node.js 版本是更稳妥的做法。
3.2 项目初始化与依赖安装
新建一个目录,初始化项目,然后装 Univer 的核心包。Univer 的包是按功能拆分的,核心包是@univerjs/core,渲染相关的有@univerjs/engine-render,UI 相关的有@univerjs/design、@univerjs/ui等。第一次跑建议直接照抄官方示例的依赖列表,跑通之后再按需删减。
mkdir univer-demo && cd univer-demo pnpm init pnpm add @univerjs/core @univerjs/engine-render @univerjs/ui @univerjs/sheets @univerjs/sheets-ui pnpm add -D vite这里解释一下为什么这么选。core是内核,必须要有;engine-render提供 Canvas 渲染能力;sheets是电子表格的业务逻辑;sheets-ui是表格的界面组件;ui是通用 UI 组件库。这套组合能跑出一个最简可用的表格。
3.3 最小可运行示例的搭建
依赖装好后,建一个index.html和一个main.js。HTML 里放一个容器 div,JS 里初始化 Univer 实例并挂载到容器上。核心代码大概长这样:
import { Univer } from '@univerjs/core'; import { defaultTheme } from '@univerjs/design'; import { UniverRenderEnginePlugin } from '@univerjs/engine-render'; import { UniverUIPlugin } from '@univerjs/ui'; import { UniverSheetsPlugin } from '@univerjs/sheets'; import { UniverSheetsUIPlugin } from '@univerjs/sheets-ui'; const univer = new Univer({ theme: defaultTheme, }); univer.registerPlugin(UniverRenderEnginePlugin); univer.registerPlugin(UniverUIPlugin, { container: 'app', }); univer.registerPlugin(UniverSheetsPlugin); univer.registerPlugin(UniverSheetsUIPlugin); univer.createUnit('workbook', { id: 'demo-workbook', sheetOrder: ['sheet-01'], sheets: { 'sheet-01': { id: 'sheet-01', name: 'Sheet1', cellData: { 0: { 0: { v: 'Hello' }, 1: { v: 'Univer' } }, }, }, }, });这段代码做了几件事:创建 Univer 实例、注册渲染引擎、注册 UI、注册表格逻辑和表格界面、最后创建一个工作簿并塞了两个单元格。跑起来之后你应该能看到一个带工具栏的表格界面。
3.4 Facade API 的常用操作
界面跑起来只是第一步,真正干活要靠 Facade API。我整理了几个高频操作,基本覆盖日常需求。
取当前工作簿和当前工作表:
const workbook = univerAPI.getActiveWorkbook(); const worksheet = workbook.getActiveSheet();读写单元格:
const range = worksheet.getRange('A1:B2'); range.setValue('测试'); const value = range.getValue();设置样式:
range.setBackgroundColor('#f0f0f0'); range.setFontWeight('bold');批量写入数据,这个在导入场景里很常用:
const data = [ ['姓名', '年龄', '城市'], ['张三', 28, '北京'], ['李四', 32, '上海'], ]; worksheet.getRange('A1:C3').setValues(data);提示:Facade API 的很多方法返回的是 Promise 或者链式对象,写的时候注意 await 和链式调用的顺序,顺序错了可能不生效。
3.5 插件注册的取舍逻辑
Univer 的插件体系是它的核心优势,但也是新手最容易懵的地方。官方示例里往往注册了十几个插件,你如果全抄过来,打包体积会很大。我的建议是分三步走:第一步,先跑通最小集合(core + render + ui + sheets + sheets-ui);第二步,按功能需求逐个加,比如要公式就加@univerjs/sheets-formula,要条件格式就加@univerjs/sheets-conditional-formatting;第三步,跑一遍打包分析,看看哪个插件占体积大,再决定要不要换实现方式。
这里有个经验:协同编辑相关的插件(@univerjs/sheets-collaboration之类)依赖会比较重,如果只是单机使用,完全可以不加。我见过有人为了“以后可能要用”把所有插件都装上,结果首屏加载慢得离谱,这就本末倒置了。
4. 实操过程与核心环节实现
4.1 从零跑通一个可编辑表格
我把完整流程拆成六步,照着做基本不会出问题。
第一步,确认 Node.js 版本。打开终端输入node -v,低于 18 的先升级。这一步别偷懒,版本不对后面报错会很难查。
第二步,创建项目目录并初始化。用pnpm init生成 package.json,然后手动改一下type字段为module,方便用 ES Module 语法。
第三步,安装依赖。除了前面说的核心包,还要装 Vite 作为构建工具。Vite 的配置很简单,一个vite.config.js加上默认配置就能跑。
第四步,写 HTML 和入口 JS。HTML 里放一个 id 为 app 的 div,宽高设成 100%。JS 里按前面的示例初始化 Univer。
第五步,起开发服务器。pnpm vite或者配一个dev脚本都行。浏览器打开 localhost 对应端口,应该能看到表格。
第六步,验证功能。试着在单元格里输入文字、改改样式、拖拽选区,确认交互正常。
4.2 数据导入导出的实现路径
实际项目里,表格很少是空的,通常要从后端拉数据或者从 Excel 文件导入。Univer 本身提供了数据模型,你可以直接把二维数组塞进去。如果是 Excel 文件,需要借助 SheetJS 之类的库先解析成二维数组,再通过 Facade API 写入。
导出的话,Univer 有@univerjs/sheets-export这类插件,能把当前工作簿导出成 xlsx。不过要注意,导出功能对样式的支持程度取决于插件版本,复杂样式可能会丢。我的做法是导出前先做一次样式简化,把不支持的样式降级处理,避免用户拿到文件后发现格式乱了。
// 导入示例:把二维数组写入指定区域 const rows = await fetchDataFromBackend(); worksheet.getRange(`A1:${colLetter(rows[0].length)}${rows.length}`).setValues(rows);4.3 大数据量下的性能调优
前面说过 Canvas 的优势是大数据量,但也不是无脑快。我实测过,一万行十列的数据,如果每个单元格都设独立样式,首次渲染还是会卡。优化思路有几个:一是减少样式对象数量,相同样式的单元格共用样式定义;二是开启虚拟滚动,只渲染可视区域;三是把不必要的历史记录关掉,撤销栈太深也吃内存。
Univer 的虚拟滚动是内置的,但需要确认你用的版本里相关配置有没有开。另外,cellData的数据结构尽量用稀疏对象,不要给空单元格也塞一个空对象,那样内存占用会翻倍。
4.4 与后端协作的数据同步方案
如果要做多人协作,数据同步是绕不开的。Univer 的协同能力基于操作变换(OT)或者 CRDT 这类算法,具体用哪种取决于你选的插件。我的建议是,如果团队里没有专门研究协同算法的人,优先用官方提供的协同插件,不要自己从零实现。自己实现 OT 的坑非常多,光是冲突处理就能耗掉几周时间。
同步的粒度也要想清楚。是按单元格同步,还是按操作同步?按单元格同步实现简单,但网络请求频繁;按操作同步效率高,但需要处理操作合并和冲突。我一般推荐按操作同步,配合节流和批量提交。
5. 常见问题与排查技巧实录
5.1 启动就报错的几种典型情况
第一种,Cannot find module '@univerjs/core'。这通常是依赖没装全,或者 pnpm 的 node_modules 结构有问题。解决办法是删掉 node_modules 和 lock 文件重新装。
第二种,页面一片空白,控制台没有明显报错。这种情况多半是容器 div 的宽高没设,Canvas 画布尺寸为 0,自然什么都看不到。检查 CSS 里容器有没有明确的高度。
第三种,Univer is not defined。这是模块导入方式不对,确认你用的是 ES Module 的 import 语法,而不是 script 标签直接引。
第四种,样式错乱。Univer 的 UI 依赖它自己的样式文件,如果用的是按需引入,可能漏了某个 CSS。检查一下有没有引入@univerjs/design/lib/index.css之类的样式入口。
5.2 渲染异常与白屏排查
白屏是 Canvas 项目的高频问题。排查顺序建议这样:先看控制台有没有报错;再看容器尺寸;然后看 Canvas 元素有没有被创建出来;最后看数据有没有正确传入。我遇到过一次白屏,查了半天发现是createUnit的 id 和sheetOrder里的 id 对不上,数据没挂上去,画布自然是空的。
还有一个隐蔽的坑:如果容器父级有overflow: hidden且高度是 0,Canvas 也会被裁掉。这种问题用开发者工具的元素检查一看便知,但如果不熟悉 Canvas 渲染流程,容易往数据方向查,浪费时间。
5.3 版本兼容性速查
Univer 迭代比较快,不同版本之间 API 可能有变化。我整理了一个简单的对照表,方便排查版本问题。
| 问题现象 | 可能原因 | 排查方向 |
|---|---|---|
| 插件注册报错 | 插件版本与 core 不匹配 | 检查 package.json 里各包版本是否一致 |
| API 方法不存在 | 版本升级后方法改名 | 查对应版本的 changelog |
| 样式丢失 | 样式文件未引入或版本不符 | 确认 CSS 入口和包版本 |
| 构建失败 | Node.js 版本过低 | 升级到 18 LTS 以上 |
| 热更新失效 | Vite 配置问题 | 检查 vite.config.js 的插件配置 |
提示:升级 Univer 版本时,建议一次性把所有
@univerjs/*包升到同一版本,混用版本是很多诡异问题的根源。
5.4 我踩过的三个坑
第一个坑是插件注册顺序。有些插件之间有依赖关系,比如 UI 插件要在渲染引擎之后注册,顺序反了会报错。官方文档里没写得很清楚,我是看源码里的依赖声明才搞明白的。
第二个坑是 Facade API 的异步特性。有些方法看起来是同步的,实际返回 Promise,不 await 的话拿到的可能是旧值。这个在批量操作时特别容易出问题,数据写进去了但读出来还是空的。
第三个坑是内存泄漏。如果频繁创建和销毁 Univer 实例,一定要记得调用 dispose 方法释放资源,否则 Canvas 和事件监听会一直挂着,页面越用越卡。
6. 扩展方向与个人经验
Univer 跑通之后,能做的事情其实很多。往浅了说,可以把它当成一个可定制的数据录入组件,嵌到后台管理系统里替代传统的表单。往深了说,可以基于它的插件体系做行业专用的表格,比如财务用的报表工具、教育用的成绩分析表、物流用的运单跟踪表。
我自己后续打算试两个方向。一个是把 Univer 和低代码平台结合,让用户拖拽生成带表格的页面。另一个是研究它的公式引擎,看能不能接入自定义函数,做一些业务专用的计算。这两个方向都需要对 Facade API 和插件机制有比较深的理解,不是一两天能搞定的,但值得投入。
最后分享一个小技巧:调试 Univer 的时候,善用univerAPI这个全局对象。在控制台里直接敲univerAPI.getActiveWorkbook()能看到当前工作簿的完整结构,比翻文档快得多。另外,官方示例仓库里的代码质量参差不齐,有些是早期版本写的,跑之前先看 package.json 里的版本号,对不上就手动改一下依赖版本再跑。