1. 从“univer”这个标题说起:它到底是什么,能解决什么问题
第一次看到“univer”这个词,很多人会以为是“universe”的缩写,或者某个新出的前端框架。其实它是一套开源的表格与文档协作引擎,核心定位是让开发者能在自己的产品里嵌入类似在线电子表格、文档编辑的能力。你可以把它理解成“把在线表格的底层能力做成了一套可复用的 SDK”,而不是让你从零去写一个 Canvas 渲染引擎。
我最早接触它是因为一个内部管理系统的需求:业务方希望能在网页里直接编辑一份带公式、带多 Sheet 的表格,还要支持多人同时看到彼此的修改。当时评估过几条路,要么直接用现成的在线文档产品做嵌入,要么自己基于 Canvas 从零画表格。前者受限于别人的产品边界,后者工作量巨大。univer 正好卡在中间——它提供了一套 Facade API,让你用命令式的方式去操作表格模型,底层渲染交给它自己处理。
这套东西适合谁?如果你是中高级前端,做过 Canvas 或者富文本编辑器相关的东西,那上手会很快;如果你是刚入门前端不久,想拿它做个玩具项目,也能跑起来,但遇到渲染性能、协同冲突这类问题时会比较吃力。它主要解决三个问题:一是表格/文档的渲染与交互,二是数据模型与公式计算,三是多人协同的底层同步机制。这三个问题任何一个单独拎出来都够写一个库,univer 把它们打包在一起,用 SDK 的形式交付。
热搜词里出现了 Node.js、Canvas、Facade API、SDK 这些词,说明大家关注的点集中在“怎么装环境”“底层怎么画的”“API 怎么调”。我下面会按这个顺序,把我在实际项目里踩过的坑和验证过的方案拆开讲。
2. 环境准备与依赖安装:Node.js 版本选择和常见报错处理
2.1 Node.js 版本到底选哪个
univer 的官方示例和构建工具链对 Node.js 版本有要求。我实测下来,Node.js 18.20.4 LTS 和 20.x LTS 都能正常跑,但 22.x 在部分依赖的 postinstall 阶段会有警告。热搜里有人搜“node.js 18.20.4 lts版本下载”和“node.js 22.12+”,说明版本选择确实是个高频问题。
我的建议是:如果你只是跑官方 demo,用 18.20.4 LTS 最稳;如果你要在现有项目里集成,先看你项目本身的 Node 版本,不要为了 univer 单独降级整个项目。可以用 nvm 做版本隔离:
nvm install 18.20.4 nvm use 18.20.4 node -v安装完 Node.js 后,验证 npm 是否正常:
npm -v如果 npm 版本低于 9,建议升级,因为 univer 的 monorepo 依赖里有一些 workspace 协议,老版本 npm 解析会出问题。
2.2 安装 univer 核心包
univer 是拆成多个包发布的,核心包包括@univerjs/core、@univerjs/ui、@univerjs/sheets等。不要想着只装一个包就完事,它的架构是分层的。我一般这样装:
npm install @univerjs/core @univerjs/ui @univerjs/sheets @univerjs/sheets-ui如果你要用公式,再加@univerjs/sheets-formula;要协同,加@univerjs/rpc和对应的协同包。这里有个坑:不同包之间的版本号必须一致,否则运行时会报“Facade API 找不到”或者“依赖注入失败”。我习惯在 package.json 里用同一个版本号锁定:
{ "dependencies": { "@univerjs/core": "0.1.0", "@univerjs/ui": "0.1.0", "@univerjs/sheets": "0.1.0" } }2.3 常见安装报错与排查
热搜里有人搜“安装node.js”“如何查看有没有安装node.js”,这类基础问题我就不展开了。重点说 univer 相关的报错。
第一个高频报错是Cannot find module '@univerjs/core'。这通常是因为你装了包但没在入口文件里正确初始化。univer 不是装完就能用的,它需要你手动创建 Univer 实例并注册插件。
第二个是Univer is not defined。如果你用 script 标签直接引入 UMD 包,全局变量名是Univer,不是univer。大小写敏感。
第三个是构建时的Module not found: Can't resolve 'canvas'。这是因为 univer 在某些环境下会尝试加载 Node.js 的 canvas 包做服务端渲染,但浏览器端不需要。解决办法是在构建配置里把 canvas 设为 external:
// webpack.config.js module.exports = { externals: { canvas: 'commonjs canvas' } };注意:不要为了省事直接
npm install canvas,那个包在 Windows 上编译经常失败,而且浏览器端根本用不到。
3. Canvas 渲染引擎与 Facade API 的配合逻辑
3.1 为什么 univer 选择 Canvas 而不是 DOM
这是很多人问的问题。用 DOM 做表格,每个单元格一个 div,几千行下来 DOM 节点数量爆炸,滚动和编辑都会卡。Canvas 把整个表格画在一张画布上,节点数量恒定,滚动时只需要重绘可视区域。univer 的渲染层就是基于 Canvas 做的,这也是热搜里“canvas绘图引擎”“canvas绘图”这些词出现的原因。
但 Canvas 有个天然劣势:它没有 DOM 的事件冒泡,所有交互都要自己算坐标。univer 的做法是在 Canvas 上层盖一层透明的 DOM 层,用来接收鼠标和键盘事件,再把事件坐标转换成单元格坐标。这个设计在 Facade API 里体现得很明显——你调 API 操作的是数据模型,不是直接操作 Canvas。
3.2 Facade API 是什么,怎么用
Facade API 是 univer 对外暴露的一套高层接口,目的是让你不用关心底层是 Canvas 还是别的渲染方式,只用命令式的方式操作表格。比如你要设置 A1 单元格的值:
import { Univer, LocaleType, merge } from '@univerjs/core'; import { defaultTheme } from '@univerjs/ui'; import { UniverSheetsPlugin } from '@univerjs/sheets'; import { UniverSheetsUIPlugin } from '@univerjs/sheets-ui'; const univer = new Univer({ theme: defaultTheme, locale: LocaleType.ZH_CN, }); univer.registerPlugin(UniverSheetsPlugin); univer.registerPlugin(UniverSheetsUIPlugin); const workbook = univer.createUniverSheet({}); const worksheet = workbook.getActiveSheet(); const range = worksheet.getRange(0, 0); range.setValue('Hello Univer');这段代码里,getRange(0, 0)拿到的就是 Facade API 的一个封装对象,setValue会触发数据模型更新,然后渲染层自动重绘。你不需要手动调canvas.draw()。
3.3 渲染流程拆解
univer 的渲染流程大致分四步:数据变更 -> 命令执行 -> 模型更新 -> 视图重绘。Facade API 的调用会生成一个命令,命令被 CommandService 执行后修改数据模型,模型变更触发渲染引擎的脏矩形标记,最后在下一帧统一重绘。
这个流程的好处是,如果你要做协同,只需要把命令同步给其他客户端,其他客户端执行同样的命令就能得到一致的状态。这也是为什么 univer 的协同方案是基于命令而不是基于状态快照的。
实操心得:调试渲染问题时,不要盯着 Canvas 看,先看数据模型对不对。我遇到过单元格显示空白,最后发现是
setValue传了undefined,模型里存了个空值,渲染层直接跳过了。
4. 从零搭建一个可运行的 univer 表格页面
4.1 项目初始化与目录结构
我用 Vite 做构建工具,因为它的冷启动快,配置也简单。先创建项目:
npm create vite@latest univer-demo -- --template vanilla cd univer-demo npm install然后安装 univer 相关包。目录结构我习惯这样组织:
univer-demo/ ├── index.html ├── src/ │ ├── main.js │ ├── univer/ │ │ ├── index.js │ │ └── plugins.js │ └── styles/ │ └── univer.css └── vite.config.jsindex.html里只需要一个容器:
<!DOCTYPE html> <html> <head> <title>Univer Demo</title> </head> <body> <div id="app" style="width: 100vw; height: 100vh;"></div> <script type="module" src="/src/main.js"></script> </body> </html>4.2 初始化 Univer 实例
src/univer/index.js里做初始化:
import { Univer, LocaleType } from '@univerjs/core'; import { defaultTheme } from '@univerjs/ui'; import { UniverSheetsPlugin } from '@univerjs/sheets'; import { UniverSheetsUIPlugin } from '@univerjs/sheets-ui'; import { UniverSheetsFormulaPlugin } from '@univerjs/sheets-formula'; export function createUniver(container) { const univer = new Univer({ theme: defaultTheme, locale: LocaleType.ZH_CN, }); univer.registerPlugin(UniverSheetsPlugin); univer.registerPlugin(UniverSheetsUIPlugin); univer.registerPlugin(UniverSheetsFormulaPlugin); const workbook = univer.createUniverSheet({ id: 'demo-workbook', sheetId: 'sheet-01', name: 'Sheet1', }); univer.mount(container); return { univer, workbook }; }这里有几个关键点。locale设成ZH_CN后,右键菜单和工具栏会显示中文。createUniverSheet的参数里,id和sheetId必须唯一,如果你要创建多个工作簿,重复的 id 会导致命令路由混乱。
4.3 挂载与样式处理
main.js里调用:
import { createUniver } from './univer'; import './styles/univer.css'; const container = document.getElementById('app'); const { univer, workbook } = createUniver(container); // 写入一些初始数据 const sheet = workbook.getActiveSheet(); sheet.getRange(0, 0, 3, 3).setValues([ ['姓名', '部门', '工时'], ['张三', '研发', 160], ['李四', '设计', 152], ]);univer.css里至少要保证容器有明确的高度:
#app { width: 100%; height: 100vh; overflow: hidden; }注意:如果容器高度是 0,Canvas 会画不出来,页面一片空白。我踩过这个坑,排查了半天以为是渲染引擎的问题,最后发现是 CSS 没给高度。
4.4 验证运行结果
跑npm run dev,打开浏览器,你应该能看到一个带工具栏的表格,里面有刚才写入的三行数据。点击单元格可以编辑,输入=SUM(C2:C3)能算出 312。如果公式没生效,检查UniverSheetsFormulaPlugin有没有注册。
5. 常见问题与排查技巧实录
5.1 表格不显示或显示空白
这是最高频的问题。排查顺序如下:
| 现象 | 可能原因 | 解决办法 |
|---|---|---|
| 页面完全空白 | 容器高度为 0 | 给容器设置明确高度 |
| 有工具栏无表格 | 插件未注册 | 检查 SheetsPlugin 和 SheetsUIPlugin |
| 表格显示但无数据 | 数据写入时机不对 | 在 mount 之后写入 |
| 控制台报 Facade 错误 | 包版本不一致 | 统一所有 @univerjs 包版本 |
5.2 公式计算不生效
公式插件注册后,还需要确保单元格的值以=开头。另外,公式的计算是异步的,如果你在setValue之后立刻getValue,可能拿到的是旧值。可以用univer.getCommandService().executeCommand的回调来监听计算完成。
5.3 协同场景下的冲突
如果你在做多人协同,两个用户同时改同一个单元格,后到的命令会覆盖先到的。univer 的 RPC 层提供了冲突解决机制,但需要你自己实现 OT 或 CRDT 算法。官方示例里有一个基于 WebSocket 的简单实现,但生产环境建议用成熟的协同后端。
实操心得:不要试图自己从零写协同算法,除非你专门研究过 OT。我试过一个简单的“最后写入胜出”策略,结果在并发编辑时数据丢失严重。后来改用命令队列加版本号校验,才稳定下来。
5.4 性能问题排查
当表格数据超过一万行时,滚动可能会卡。这时候要检查是否开启了虚拟滚动。univer 默认是开启的,但如果你自定义了渲染逻辑,可能会破坏它。另外,避免在setValues里一次性写入十万行,分批写入并配合requestAnimationFrame会流畅很多。
6. 进阶方向:从单机表格到协同应用
6.1 数据持久化方案
univer 的数据模型可以序列化成 JSON,你可以把它存到后端。workbook.save()返回一个快照对象,univer.createUniverSheet(snapshot)可以恢复。我一般用 IndexedDB 做本地缓存,用后端 API 做云端同步。
6.2 自定义插件开发
univer 的插件机制很灵活,你可以注册自己的命令和 UI 组件。比如加一个“一键导出 CSV”的按钮:
import { CommandType, ICommandService } from '@univerjs/core'; const ExportCSVCommand = { id: 'demo.command.export-csv', type: CommandType.OPERATION, handler: (accessor) => { const sheet = accessor.get(ICommandService); // 导出逻辑 return true; }, };然后在插件里注册这个命令,并在工具栏加一个按钮触发它。
6.3 与现有系统集成
如果你公司已经有权限系统,可以在 univer 的命令执行前加一层拦截,判断当前用户有没有编辑权限。Facade API 的onBeforeCommandExecute钩子可以做这件事。
7. 我在实际项目里总结的几条经验
第一条,不要一上来就追求功能全。univer 的包很多,全装进来会让打包体积暴涨。先装核心的 core、ui、sheets,跑通之后再按需加公式、协同、图表。
第二条,版本锁定比什么都重要。univer 还在快速迭代,不同小版本之间的 API 可能有 breaking change。我建议在 package.json 里用精确版本号,不要用^。
第三条,Canvas 的调试要用对工具。Chrome DevTools 的 Layers 面板可以看 Canvas 的重绘区域,Performance 面板可以录渲染帧。如果发现某个操作导致全量重绘,大概率是脏矩形计算出了问题。
第四条,协同不是刚需就别做。单机表格已经能覆盖大部分场景,协同的复杂度是指数级上升的。我见过太多项目在协同上翻车,最后回退到单机加手动保存。
第五条,多看官方示例的源码。univer 的文档还在完善中,很多用法在示例代码里比文档更清楚。特别是examples目录下的那几个 demo,基本覆盖了常见场景。
最后再分享一个小技巧:如果你在本地开发时遇到奇怪的渲染问题,先清空浏览器缓存再试。univer 的 Canvas 渲染有时会受缓存影响,尤其是你改了主题或样式之后。