
1. Univer 到底是个什么东西为什么值得单独拿出来聊第一次听到 Univer 这个名字很多人会以为是某个新出的前端框架或者 UI 组件库。其实不是。Univer 是一套开源的在线电子表格与文档协作引擎核心定位是让开发者能把“类 Excel”“类 Google Sheets”的能力直接嵌进自己的产品里。它对外暴露的核心入口是Facade API底层渲染依赖Canvas服务端和构建链路则深度绑定Node.js生态。这几个关键词——univer、SDK、Node.js、Canvas、Facade API——基本就是它的技术骨架。我最早接触 Univer 是因为一个内部数据填报系统的需求。业务方想要一个能在浏览器里直接编辑、支持公式、支持多人同时改单元格的东西但又不想引入一整套笨重的商业表格控件。当时评估了几条路线自己用 Canvas 从零画表格、用现成的开源表格库、或者找一个带协作能力的引擎。自己画表格这件事做过的人都懂光是单元格合并、冻结行列、公式依赖链就能把人拖垮。现成的开源表格库大多只解决“展示”不解决“编辑 协作 公式”。Univer 恰好卡在这个位置上它把电子表格的渲染、公式计算、协同编辑、插件体系都做成了可复用的 SDK。所以这篇文章不是官方文档的搬运而是我作为一个实际把它接进项目里的人把 Univer 的核心设计、Facade API 的用法、Canvas 渲染的坑、Node.js 侧的配合、以及踩过的那些坑完整地摊开讲一遍。适合谁看如果你正在做在线表格、数据看板、低代码平台里的表格模块、或者任何需要“在网页里编辑结构化数据”的产品这篇内容能帮你少走至少两周弯路。如果你只是好奇 Canvas 怎么画一个高性能表格里面关于渲染分层和脏矩形的内容也值得一看。Univer 的能力边界要说清楚它不是 Excel 的完整替代品宏、VBA、复杂的透视表这些它不覆盖它也不是一个开箱即用的 SaaS 产品你需要自己写代码把它集成进去。它更像是一套“表格能力中间件”你给它一个容器它给你一个可编辑、可扩展、可协作的表格运行时。2. 整体架构拆解为什么是 Canvas Facade API Node.js 这套组合2.1 Canvas 渲染为什么不用 DOM 表格这是被问得最多的问题。HTML 里用table或者 div 网格不也能做表格吗为什么 Univer 要用 Canvas答案在“规模”和“交互”两个词上。DOM 方案在几百个单元格时没问题但一旦到几万、几十万个单元格浏览器要维护的 DOM 节点数量会直接压垮渲染性能。每个单元格是一个节点滚动时浏览器的重排重绘成本极高。Canvas 则是一块画布所有单元格都画在同一个位图上节点数量恒定为 1。滚动、缩放、选区高亮这些操作本质上只是重绘画布的一部分区域性能曲线要平缓得多。但 Canvas 也有代价它没有 DOM 的事件模型。你点一个单元格浏览器不会告诉你“你点了第 3 行第 5 列”你得自己根据鼠标坐标反算行列号。文本选择、复制粘贴、输入法、无障碍访问这些 DOM 天然具备的能力Canvas 全都要自己实现。Univer 的做法是在 Canvas 上层叠一个透明的 DOM 层专门处理输入和事件画布负责“画”DOM 负责“收事件”。这个设计思路很关键后面讲 Facade API 时会再提到。2.2 Facade API把复杂内核包成一层好用的壳Univer 的内核其实相当复杂有渲染引擎、公式引擎、协同层、插件系统。如果让业务开发者直接操作内核对象学习成本会高到劝退。Facade API 就是在这个背景下出现的——它是一层门面Facade 模式把常用的操作封装成直观的方法。举个最直接的对比。你要往 A1 单元格写一个值内核层面可能涉及命令的构造、命令的派发、撤销栈的记录、协同层的广播。而 Facade API 里就是一行const sheet univerAPI.getActiveWorkbook().getActiveSheet(); sheet.getRange(A1).setValue(hello);这层封装的价值在于它把“命令式”的内核操作变成了“声明式”的业务调用。你不需要知道命令怎么派发只需要告诉它“我要把 A1 设成 hello”。同时 Facade API 还统一了同步和异步的边界很多看起来是同步的调用内部其实是异步命令Facade 帮你处理了 Promise 的编排。2.3 Node.js 在链路里的角色很多人以为 Univer 是纯前端的东西Node.js 只是用来跑构建工具。这个理解只对了一半。Node.js 在 Univer 生态里至少有三个角色。第一是构建与开发环境。Univer 的源码是 TypeScript包管理、打包、本地开发服务器都跑在 Node.js 上。你npm install装依赖、npm run dev起本地服务这些都离不开 Node.js。第二是服务端协同。Univer 的协同编辑需要一个服务端来中转和持久化操作官方提供的协同服务就是 Node.js 写的。第三是服务端渲染与导出。如果你要在服务端把表格导出成图片或者 PDF需要在 Node.js 环境里跑 Canvas 的渲染逻辑。所以“Univer Node.js”不是随便凑的关键词而是真实存在的技术依赖。你如果 Node.js 版本太老装依赖时就会遇到各种奇怪的报错这个后面会专门讲。3. 环境搭建Node.js 版本选择和依赖安装的实操细节3.1 Node.js 版本到底选哪个这是新手最容易翻车的地方。Univer 的依赖树里有一些包对 Node.js 版本有硬性要求版本太低会直接编译失败。我实测下来Node.js 18.20.4 LTS 和 20.x LTS 都是稳的22.x 也能跑但个别依赖会有警告。不建议用 16.x 及以下也不建议用奇数版本比如 21.x因为奇数版本不是 LTS生态兼容性差。怎么确认自己装的是哪个版本node -v npm -v如果输出是v18.20.4这种就对了。如果你机器上有多个版本建议用 nvm 管理切换起来方便。Windows 用户如果不想折腾 nvm直接去官网下 LTS 版本的安装包一路下一步就行安装时记得勾选“Add to PATH”。提示安装完 Node.js 后一定要重开一个终端窗口否则 PATH 可能没刷新node -v会提示找不到命令。3.2 创建项目并安装 Univer 依赖我习惯用 Vite 起一个干净的 TypeScript 项目因为 Univer 本身是 TS 写的类型提示能省很多事。npm create vitelatest univer-demo -- --template vanilla-ts cd univer-demo npm install然后装 Univer 的核心包。Univer 是拆成多个包发布的核心包、预设包、协同包是分开的。最小可用集合是核心加一个预设npm install univerjs/core univerjs/presets univerjs/preset-sheets-core这里有个细节univerjs/presets是聚合包univerjs/preset-sheets-core是电子表格的核心预设。如果你还要公式、协同、导出得再装对应的 preset。不要一次性把所有包都装上按需装否则打包体积会很难看。3.3 一个最小可运行的表格实例装完依赖后写一个最简单的入口。核心逻辑是创建一个 Univer 实例挂到一个 div 上然后拿到 Facade API 往里面写数据。import { createUniver, LocaleType, merge } from univerjs/presets; import { UniverSheetsCorePreset } from univerjs/preset-sheets-core; import univerjs/preset-sheets-core/lib/index.css; const { univerAPI } createUniver({ locale: LocaleType.ZH_CN, presets: [ UniverSheetsCorePreset({ container: app, }), ], }); univerAPI.createWorkbook({ sheets: { sheet1: { id: sheet1, name: 第一个表, cellData: { 0: { 0: { v: 姓名 }, 1: { v: 分数 } }, 1: { 0: { v: 张三 }, 1: { v: 92 } }, 2: { 0: { v: 李四 }, 1: { v: 88 } }, }, }, }, });这段代码跑起来页面上就会出现一个可编辑的表格A1 是“姓名”B1 是“分数”下面两行是数据。cellData的结构是行号 - 列号 - 单元格对象行列号从 0 开始。这个结构看起来有点绕但它是 Univer 内部数据模型的原貌Facade API 的很多方法最终都会落到这个结构上。注意container对应的 div 必须提前在 HTML 里存在而且要有明确的宽高。如果 div 高度是 0表格会渲染不出来这个坑我踩过排查了半天以为是渲染引擎的问题结果是 CSS 没给高度。4. Facade API 深入从单元格操作到公式与选区4.1 单元格读写与批量操作Facade API 里最常用的对象是FRange它代表一个区域。你可以通过getRange拿到它然后做读写。const sheet univerAPI.getActiveWorkbook().getActiveSheet(); // 单个单元格 sheet.getRange(A1).setValue(标题); // 区域批量写 sheet.getRange(A2:B4).setValues([ [张三, 92], [李四, 88], [王五, 95], ]); // 读取 const values sheet.getRange(A2:B4).getValues(); console.log(values); // [[张三, 92], [李四, 88], [王五, 95]]setValues接收的是二维数组行优先。这里有个容易搞混的地方setValue是单数写一个值setValues是复数写一个二维数组。如果你把二维数组传给setValue它不会报错但结果可能不是你想要的。批量写比逐个写快很多因为每次写操作都会触发一次渲染调度。逐个写 1000 个单元格会触发 1000 次调度批量写只触发一次。这个差异在数据量大的时候非常明显。4.2 公式的写入与计算Univer 内置了公式引擎写入公式和写普通值的方式一样只是值以开头。sheet.getRange(C1).setValue(SUM(B2:B4)); sheet.getRange(C2).setValue(AVERAGE(B2:B4));公式写入后Univer 会自动计算并显示结果。但要注意公式的计算是异步的如果你在写入后立刻读取getValue()可能拿到的是公式字符串而不是计算结果。正确的做法是监听计算完成事件或者用getCell拿到单元格对象后读它的计算值。// 不推荐可能拿到公式字符串 const v sheet.getRange(C1).getValue(); // 推荐等公式计算完成 univerAPI.getActiveWorkbook().onCommandExecuted((command) { // 命令执行完后再读 });公式引擎支持大部分常用函数SUM、AVERAGE、COUNT、IF、VLOOKUP、INDEX、MATCH 这些都有。但一些 Excel 特有的、依赖外部数据的函数比如 WEBSERVICE不支持这个要有预期。4.3 选区与事件监听选区是表格交互的核心。Facade API 提供了获取当前选区、监听选区变化的能力。// 获取当前选区 const selection sheet.getSelection(); const range selection.getActiveRange(); console.log(range.getA1Notation()); // 比如 A1:B4 // 监听选区变化 univerAPI.getActiveWorkbook().onSelectionChanged((selection) { const range selection.getActiveRange(); console.log(当前选中, range.getA1Notation()); });选区事件在做什么用比如你想做一个“选中区域后显示统计信息”的功能或者“选中区域后弹出操作菜单”都依赖这个事件。我做过一个需求是选中一列数字后实时显示求和就是用onSelectionChanged拿到区域再用getValues读值求和。实操心得onSelectionChanged触发非常频繁用户拖动选区时会连续触发。如果你在回调里做重计算一定要加防抖否则会卡。我一般用 100ms 的防抖体验和性能都能兼顾。5. Canvas 渲染层性能优化的几个关键点5.1 渲染分层与脏矩形Univer 的 Canvas 渲染不是每次操作都全量重绘而是用了脏矩形dirty rectangle机制。只有发生变化的区域会被重绘其他区域保持不动。这个机制是表格能流畅滚动的关键。理解这一点对排查渲染问题很重要。如果你发现某个单元格更新后没刷新很可能是它没有被标记为“脏”。这种情况通常出现在你直接改了内部数据模型而没走命令派发的时候。永远通过 Facade API 或命令来改数据不要直接改内部对象否则渲染层不知道要重绘。5.2 大数据量下的滚动性能我做过一个压力测试10 万行、20 列的表格用 Univer 渲染滚动基本流畅。但如果单元格里有大量富文本或者复杂样式帧率会下降。优化方向有几个。第一减少样式数量。每个单元格的样式对象如果都不一样渲染时要处理的样式组合会爆炸。能复用样式就复用。第二避免在单元格里塞超长文本。Canvas 绘制长文本的成本比短文本高很多而且换行计算也耗时。第三如果不需要编辑考虑用只读模式只读模式下渲染层可以跳过很多交互相关的计算。5.3 导出图片时的 Canvas 处理Univer 支持把表格导出成图片这个功能在服务端跑的时候依赖 Node.js 的 Canvas 实现。这里有个坑浏览器里的 Canvas 和 Node.js 里的 Canvas 不是同一个东西字体渲染会有差异。如果你在浏览器里预览正常导出到服务端生成的图片字体不对大概率是服务端没有装对应的字体。解决办法是在服务端环境里安装表格里用到的字体或者导出时指定字体回退链。这个坑比较隐蔽因为本地开发时往往不会触发一上生产就暴露。6. 协同编辑与服务端配合的实操要点6.1 协同的基本原理Univer 的协同基于操作变换OT或者 CRDT 的思路把每个用户的编辑操作变成一个可合并、可排序的命令通过服务端广播给其他客户端。客户端收到远端命令后在本地重放从而保持状态一致。这个机制对使用者的影响是你不能假设“我改了数据数据立刻就是我改的值”。在协同场景下你的修改可能和别人的修改冲突最终结果由合并算法决定。所以协同场景下读数据要读“当前状态”而不是“我写入的值”。6.2 服务端需要做什么官方提供的协同服务是 Node.js 写的核心职责是接收客户端发来的操作命令、持久化、广播给同一文档的其他客户端。如果你要自己实现至少要处理三件事连接管理谁在编辑哪个文档、命令排序保证所有客户端看到的顺序一致、断线重连用户网络抖动后要能同步到最新状态。我建议初期直接用官方提供的协同服务等业务稳定了再考虑自研。自研协同的复杂度很高光是冲突处理就能吃掉大量时间。6.3 协同场景下的常见问题最常见的问题是“我改了但别人看不到”。排查顺序是先确认命令有没有发出去看网络面板再确认服务端有没有收到看服务端日志最后确认其他客户端有没有收到广播。这三步能定位 90% 的协同问题。另一个问题是“两个人同时改一个单元格结果不对”。这是正常的冲突最终结果取决于合并策略。如果你的业务对冲突敏感需要在 UI 上做提示比如“该单元格正在被他人编辑”。7. 常见问题与排查速查表问题现象可能原因排查方向表格渲染不出来白屏容器 div 没有宽高检查 CSS给容器明确的高度安装依赖时报编译错误Node.js 版本过低升级到 18.20.4 或 20.x LTS写入数据后不刷新直接改了内部模型没走命令改用 Facade API 写入公式读出来是字符串公式计算是异步的等计算完成事件后再读选区变化回调卡顿回调触发太频繁加防抖100ms 左右导出图片字体不对服务端缺字体服务端安装对应字体协同编辑不同步命令没发出去或没广播按网络、服务端、客户端三步排查大数据量滚动卡样式太多或文本太长复用样式精简单元格内容避坑技巧Univer 的包版本要统一。如果你装了univerjs/core的 0.1.x 和univerjs/presets的 0.2.x可能会出现类型不匹配或者运行时错误。安装时尽量用同一个版本号或者直接用univerjs/presets里带的依赖不要单独指定核心包版本。8. 我在实际项目里踩过的几个坑第一个坑是容器尺寸。前面提过但值得再强调。Univer 初始化时会读取容器的宽高来决定画布尺寸。如果容器在初始化时高度是 0比如放在一个还没展开的折叠面板里画布尺寸就是 0后面即使容器展开了画布也不会自动调整。解决办法是监听容器尺寸变化手动调用resize或者确保初始化时容器已经可见。第二个坑是样式污染。Univer 的 CSS 是全局注入的如果你的项目里也有表格相关的全局样式可能会互相影响。我遇到过一次项目里的td样式把 Univer 的某些 UI 元素搞乱了。解决办法是把 Univer 挂在一个独立的容器里用 CSS 作用域隔离或者检查全局样式里有没有过于宽泛的选择器。第三个坑是内存泄漏。Univer 实例如果反复创建销毁不调用销毁方法会残留事件监听和 Canvas 上下文。在单页应用里切换路由时尤其要注意离开页面时一定要调univer.dispose()。第四个坑是公式循环引用。如果 A1 的公式引用了 B1B1 又引用了 A1Univer 会检测到循环引用并报错。这个报错信息有时候不够直观排查时要顺着公式依赖链找。9. 后续可以扩展的方向Univer 的插件体系是它比较有想象力的地方。你可以写自定义插件往表格里加自定义的工具栏按钮、自定义的单元格渲染器、自定义的命令。比如我做过一个插件在单元格里渲染进度条就是通过自定义渲染器实现的。另一个方向是服务端能力。Univer 的公式引擎理论上可以在 Node.js 里独立跑这意味着你可以在服务端做批量计算、数据校验、报表生成。这个方向我还在探索目前的做法是把表格数据同步到服务端用 Node.js 跑一遍公式校验再把结果推回前端。如果你要做的是数据填报系统还可以考虑把 Univer 和表单校验结合在单元格级别做数据校验比如“这一列必须是数字”“这一列不能为空”。Univer 本身有数据验证的能力但需要自己配置规则。最后分享一个小技巧Univer 的 Facade API 文档虽然全但有些方法的参数说明不够细。遇到不确定的方法直接在浏览器控制台里把对象打印出来看它的原型链上有哪些方法比翻文档快。我很多用法都是这么试出来的。