1. 从“univer”这个关键词说起:它到底解决什么问题
第一次看到“univer”这个词,很多人会以为是“universe”的缩写,或者某个新出的前端框架。实际上,它是一套面向表格场景的在线电子表格引擎,核心能力是让开发者把类似 Excel 的表格能力嵌入到自己的 Web 应用里。你可以把它理解成“把 Excel 的骨架和肌肉拆出来,做成一套可编程的 SDK”,而不是一个成品软件。
我在实际项目里接触 univer,起因是一个很具体的需求:客户希望在自己的后台系统里做一个“预算填报”模块,表格结构由管理员预先定义好,普通员工只能填写指定单元格,其他区域锁定不可改。这个需求听起来简单,但真做起来会发现,自己用原生 Canvas 画表格,光是单元格选中、公式计算、复制粘贴、撤销重做这些基础交互,就够写几个月。而 univer 恰好把这些都封装好了,还提供了 Facade API 让你用很短的代码控制表格行为。
所以这篇文章不是官方文档的复述,而是我从零跑通 univer、踩过若干坑之后,整理出来的一份实战笔记。适合两类人看:一是正在评估“要不要用 univer”的技术负责人,二是已经决定用但不知道从哪下手的开发者。我会把核心概念、环境搭建、锁定单元格的实现思路、以及几个容易翻车的地方都讲清楚。
关键词里出现了 Node.js、Canvas、SDK、Facade API,这几个词基本勾勒出了 univer 的技术轮廓:它跑在浏览器里,底层依赖 Canvas 做渲染,通过 npm 包的形式分发,对外暴露 Facade API 作为主要操作入口。理解了这四个词,就理解了 univer 的一半。
2. univer 的技术底座:Canvas 渲染与 Facade API 设计
2.1 为什么表格引擎偏爱 Canvas 而不是 DOM
很多人第一反应是:表格不就是 HTML 的<table>吗,为什么还要用 Canvas 重画一遍?这个问题我在第一次接触 univer 时也问过。答案在于性能边界。
当表格只有几十行几列时,DOM 完全够用。但电子表格的真实场景往往是几千行、上百列,还叠加了合并单元格、条件格式、冻结行列、公式高亮等特性。如果用 DOM,每个单元格都是一个节点,浏览器要维护的节点数量会爆炸,滚动和重绘都会卡顿。Canvas 的思路是:整个表格就是一张画布,所有单元格都是画上去的像素,浏览器只需要维护一个节点。滚动时重绘可视区域即可,性能曲线平缓得多。
univer 的渲染层正是基于 Canvas 构建的。它内部维护了一套自己的布局计算和绘制管线,把单元格、边框、文字、选区都当作绘制指令来处理。这也是为什么关键词里会出现“canvas 绘图引擎”这类词——univer 本质上就是一个跑在 Canvas 上的表格绘图引擎。
但 Canvas 带来的代价是:你没法用document.querySelector去拿某个单元格,也没法用 CSS 直接改样式。所有操作都必须走它提供的 API。这就是 Facade API 存在的意义。
2.2 Facade API 是什么,为什么它是主要入口
Facade 这个词在软件工程里是“外观模式”的意思,也就是用一个简化的接口把底层复杂的子系统包起来。univer 的 Facade API 就是这层外观:底层有渲染引擎、公式引擎、命令系统、数据模型等一堆模块,但你不需要直接碰它们,只需要通过univerAPI这个对象调用方法即可。
举个直观的例子。假设你要往 A1 单元格写一个值,底层可能涉及数据模型更新、命令派发、重绘调度等一连串动作,但用 Facade API 就是一行:
univerAPI.getActiveWorkbook().getActiveSheet().getRange('A1').setValue('hello');这种链式调用的设计,让不熟悉内部架构的人也能快速上手。我的经验是:先学 Facade API 的常用方法,遇到它覆盖不到的场景再去翻底层模块。绝大多数业务需求,Facade API 都能满足。
2.3 univer 的包结构:别被一堆 npm 包吓到
第一次npm installuniver 的时候,你会发现它不是一个包,而是一组包:@univerjs/core、@univerjs/sheets、@univerjs/sheets-ui、@univerjs/facade等等。这是典型的 monorepo 拆分策略,好处是按需引入,坏处是新手容易懵。
我的建议是:初期直接用官方提供的 preset 包,比如@univerjs/presets,它把常用能力打包好了,一行引入就能跑起来。等业务稳定了,再根据实际用到的功能做裁剪,减小打包体积。下面这张表是我整理的核心包职责对照,方便你建立整体认知:
| 包名 | 职责 | 是否必须 |
|---|---|---|
| @univerjs/core | 核心数据模型、命令系统、生命周期 | 必须 |
| @univerjs/sheets | 电子表格的数据逻辑 | 必须 |
| @univerjs/sheets-ui | 表格的界面与交互 | 必须 |
| @univerjs/facade | 对外简化 API | 强烈建议 |
| @univerjs/sheets-formula | 公式计算能力 | 按需 |
| @univerjs/sheets-conditional-formatting | 条件格式 | 按需 |
理解这张表之后,你在排查问题时就能快速定位:如果是数据不对,往 core 和 sheets 想;如果是界面不显示,往 sheets-ui 想;如果是 API 调用报错,往 facade 想。
3. 把 univer 跑起来:环境准备与最小可运行示例
3.1 Node.js 环境的版本选择与验证
univer 是前端库,但它的构建和依赖管理依赖 Node.js。关键词里反复出现“node.js 安装教程”“node.js 官网下载”“如何查看有没有安装 node.js”,说明这是很多人的第一道坎。我直接给结论:用 Node.js 18 LTS 或 20 LTS,不要用太老的版本,也不建议追最新的奇数版本。
原因很简单:univer 的依赖链里有一些包对 Node 版本有要求,太老的版本(比如 14)会在安装阶段就报错,而最新的实验性版本可能遇到依赖不兼容。LTS 版本是经过验证的稳定选择。
验证是否安装成功,打开终端执行:
node -v npm -v如果两条命令都能输出版本号,说明环境没问题。如果提示“command not found”,那就是没装好或者没配环境变量。Windows 用户特别注意:安装时勾选“Add to PATH”,否则装完了也用不了。
提示:如果你在公司内网环境,npm 安装可能很慢或失败,这时候配置一个国内镜像源会省很多时间。具体命令是
npm config set registry加上镜像地址,这里不展开。
3.2 用 Vite 搭一个最小 Demo
我不建议直接在老项目里集成 univer,而是先建一个干净的 Vite 项目验证。Vite 启动快、配置少,适合做技术验证。
npm create vite@latest univer-demo -- --template vanilla cd univer-demo npm install npm install @univerjs/presets @univerjs/preset-sheets-core安装完成后,在入口文件里写最小初始化代码:
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({});这段代码做了三件事:创建 univer 实例、加载表格核心预设、创建一个空白工作簿。跑起来之后,你应该能在页面上看到一个完整的电子表格界面,可以输入、选中、拖拽。
3.3 第一次跑通后必须确认的三件事
很多人跑出界面就以为成功了,其实还有三个隐藏检查点,不确认的话后面会踩坑。
第一,容器尺寸。univer 的容器必须有明确的高度,否则表格可能显示为一条线或者完全不显示。我一般给容器设width: 100%; height: 100vh;,确保它有足够的绘制空间。
第二,样式文件是否引入。univer 的 UI 依赖 CSS,如果忘了引入样式,界面会错位得很难看。上面代码里的import '@univerjs/preset-sheets-core/lib/index.css'就是干这个的,别漏。
第三,控制台有没有警告。univer 在初始化时会输出一些信息,如果有“missing locale”或“plugin not registered”之类的警告,说明预设没加载全,后面调用 API 可能失败。跑通后先看一眼控制台,把警告清掉再往下做。
4. 锁定单元格:从需求到 Facade API 落地
4.1 需求拆解:什么叫“用户只能填指定单元格”
回到我开头提到的预算填报场景。需求原文是“支持用户定义表格,然后让用户去填写一些单元格,其他的单元格用户无法修改”。这句话拆开来看,其实包含三层含义:
第一层是结构定义:管理员能决定哪些单元格是可编辑的,哪些是只读的。第二层是权限隔离:普通用户打开表格时,只读单元格不仅不能改,最好在视觉上也有区分。第三层是数据校验:用户填完之后,系统要能拿到填写结果,并且确保只读区域没有被篡改。
这三层里,第一层和第三层是业务逻辑,第二层是 univer 要提供的核心能力。univer 的权限控制是通过工作表保护和单元格锁定两个机制配合实现的,理解这一点很关键。
4.2 工作表保护与单元格锁定的配合逻辑
这里有个容易混淆的点:很多人以为“锁定单元格”就是直接给单元格设一个 locked 属性。实际上,在 univer(以及 Excel)的模型里,锁定是分两步的。
第一步,给单元格设置locked: true,但这只是标记,默认情况下所有单元格都是 locked 状态,但保护没开启时锁定不生效。第二步,开启工作表保护,这时候所有 locked 的单元格才会真正变成只读。
这个设计的好处是灵活:你可以先把一批单元格标记为 locked,然后决定什么时候开启保护。比如管理员编辑阶段不开保护,随便改;发布给用户填写时才开保护,锁定区域就生效了。
用 Facade API 实现大概是这个流程:
const sheet = univerAPI.getActiveWorkbook().getActiveSheet(); // 第一步:把整个表格先设为可编辑 sheet.getRange('A1:Z100').setLocked(false); // 第二步:把需要用户填写的区域设为可编辑 sheet.getRange('B2:B10').setLocked(false); // 第三步:把其他区域设为锁定 sheet.getRange('A1:Z100').setLocked(true); sheet.getRange('B2:B10').setLocked(false); // 第四步:开启工作表保护 sheet.setSheetProtection({ enabled: true, });注意第三步和第四步的顺序:先设锁定状态,再开保护。如果反过来,保护开启后再改锁定状态,可能不生效或者需要额外刷新。
4.3 视觉区分:让用户一眼看出哪里能填
功能上锁定之后,还有个体验问题:用户怎么知道哪些格子能填?如果只读和可编辑看起来一模一样,用户会反复尝试点击只读区域,体验很差。
我的做法是给可编辑区域加一个浅色背景,比如淡黄色。这样用户一眼就能看出“黄色区域是我要填的”。实现方式是用 Facade API 设置背景色:
sheet.getRange('B2:B10').setBackgroundColor('#FFF9C4');同时可以给只读区域设一个灰色背景,进一步强化对比。这个细节看起来小,但在实际交付时,客户对体验的评价往往就来自这种地方。
注意:设置背景色和设置锁定是两个独立操作,不要指望设了背景色就自动锁定,也不要指望锁定了就自动变色。两者要分别处理。
4.4 读取用户填写结果并做校验
用户填完之后,后端需要拿到数据。Facade API 提供了获取区域值的方法:
const values = sheet.getRange('B2:B10').getValues();返回的是一个二维数组,对应区域内的每个单元格。拿到之后就可以做业务校验,比如必填检查、数值范围检查等。
这里有个坑我要特别提醒:如果用户通过某些方式绕过了前端保护(比如直接调 API),后端必须再做一次校验。前端的锁定只是体验层面的约束,不是安全边界。真正的数据校验一定要放在服务端。我在项目里就遇到过测试同学用控制台直接改数据的情况,所以后端校验这一层绝对不能省。
5. 集成过程中最容易翻车的几个地方
5.1 版本不一致导致的 API 找不到
univer 迭代比较快,不同版本之间 API 可能有变化。我遇到过一次:按照某篇教程写了setLocked,结果运行时报“方法不存在”。排查半天发现是教程用的是旧版本,而我装的是新版本,方法名或调用方式变了。
解决办法有两个:一是锁定版本号,在package.json里写死具体版本,不要用^或~;二是以官方文档为准,教程只作参考。我现在做技术验证时,会先把所有 univer 相关包的版本统一成同一个版本号,避免包之间版本错配。
5.2 容器销毁与内存泄漏
在单页应用里,如果页面切换时没有正确销毁 univer 实例,会造成内存泄漏。表现是切换几次页面后,浏览器越来越卡。
正确的做法是在组件卸载时调用univer.dispose():
// 组件卸载时 univer.dispose();这个调用会释放 Canvas、事件监听、内部数据模型等资源。我建议把它放在框架的生命周期钩子里,比如 Vue 的onUnmounted或 React 的useEffect清理函数中。
5.3 大数据量下的性能调优
univer 虽然基于 Canvas,性能比 DOM 好很多,但也不是没有上限。当表格数据量特别大(比如几万行)时,初始化会变慢。
我的调优经验是:不要一次性把所有数据都塞进去。如果只是展示,可以用分页或者虚拟滚动;如果是编辑场景,尽量限制用户操作的范围。另外,关闭一些用不到的特性(比如公式计算、条件格式)也能明显提升性能。univer 的预设是模块化的,按需引入本身就是一种优化。
5.4 中文输入法的兼容问题
这个坑比较隐蔽。在 Canvas 里做文本输入,中文输入法的候选词处理是个难点。univer 在这方面做了处理,但在某些浏览器或输入法组合下,仍可能出现候选词位置偏移、输入重复等问题。
我的应对策略是:测试阶段一定要用真实的中文输入法测一遍,不要只用英文测。如果发现问题,先确认 univer 版本是否最新,很多输入法兼容问题在新版本里已经修复了。
6. 关于 univer 选型与落地的一些个人判断
6.1 什么场景适合用 univer,什么场景不适合
用了几个月之后,我对 univer 的适用边界有了比较清晰的认识。
适合的场景:需要在线表格编辑能力、有权限控制需求、希望快速集成而不是从零造轮子、团队对 Canvas 渲染没有深度定制需求。比如报表填报、数据采集、在线协作表格等。
不太适合的场景:只需要展示静态表格(用普通 HTML 表格更轻)、对表格外观有极度定制需求(Canvas 定制成本高)、需要处理超大规模数据且对性能极其敏感(可能需要专门的表格方案)。
选型这件事没有绝对的对错,关键是看你的核心需求是否落在 univer 的能力圈内。
6.2 从 Demo 到生产环境还差什么
跑通 Demo 只是第一步,真正上生产还要考虑很多。我列几个我认为最重要的:
- 错误处理:API 调用失败时要有兜底,不能让页面白屏。
- 数据持久化:用户填的数据要能保存到后端,并且支持重新加载。
- 权限体系对接:univer 的锁定是前端层面的,要和后端的权限系统打通。
- 多语言:如果面向多地区用户,locale 配置要提前规划。
- 打包体积:按需引入,避免把整个 univer 都打进去。
这些点看起来琐碎,但每一个没处理好,都可能在生产环境变成事故。
6.3 我对 Facade API 使用的一点心得
最后分享一个使用 Facade API 的小技巧。Facade API 的链式调用很方便,但不要在一行里写太长的链。比如:
univerAPI.getActiveWorkbook().getActiveSheet().getRange('A1').setValue('x').setBackgroundColor('#fff');这种写法虽然能跑,但一旦中间某步返回 null,报错信息会很难定位。我的习惯是拆成几步,每步加一个判空:
const workbook = univerAPI.getActiveWorkbook(); if (!workbook) return; const sheet = workbook.getActiveSheet(); if (!sheet) return; const range = sheet.getRange('A1'); range.setValue('x'); range.setBackgroundColor('#fff');多写几行,换来的是排查问题时的时间节省。这个习惯在复杂业务里尤其值得。
另外,Facade API 的文档虽然覆盖了大部分方法,但有些边界行为(比如对空区域调用 getValues 返回什么)文档没写清楚。遇到这种情况,我的做法是写一个最小复现,直接在控制台打印结果,比猜要快得多。技术这东西,动手验证永远比读文档可靠。