如果你最近在调研“怎么给自家产品塞一个靠谱的在线表格”,大概率绕不开univer这个名字。我最初接触它,是因为手头一个后台项目需要“让运营自己定义一张表,然后分发给业务人员填写,除指定单元格外其余地方都不可编辑”——听起来不难,真做起来却是个无底洞。自研选区、公式、撤销重做、保护逻辑,随便拎一个出来都能耗掉两周。
后来我把目光转到开源方案,试过好几个,最终选择 Univer 作为基础。它是目前少数能同时覆盖表格、文档、幻灯片三种场景的开源办公基础设施,基于 Canvas 渲染,插件化架构,还在持续更新。如果你也在评估“在线表格/可编辑表单”这类需求,这篇文章会把我踩过的坑、梳理清楚的架构逻辑、以及“只允许用户填指定单元格”的完整实现思路一次性讲透。
1. 为什么我放弃了自研表格方案,开始认真看 Univer
1.1 在线表格开发的三大拦路虎:渲染性能、编辑态复杂度、公式与计算
先别急着选型,我们得承认一个问题:“像 Excel 一样”的需求,自研的成本远比想象中高。
第一关是渲染性能。普通表格用<table>或div去堆,几千行可能还能扛,一旦到几万行、几十万行,DOM 节点数量直接拖垮浏览器。要做按需渲染、虚拟滚动、单元格复用,等于从零写一套网格渲染器。
第二关是编辑态。用户点击某个单元格,要出现光标、输入框、选区边框,还要处理方向键移动、Tab 建跳转、复制粘贴、撤销重做。这些交互在 Excel 里你觉得自己天经地义,换成 Web 实现每一个都是细节魔鬼。尤其是撤销重做,不只是记个数组,你得把单元格值、样式、合并信息、行列结构全部纳入快照或命令流。
第三关是公式与计算。一个在线表格如果连SUM(A1:A10)都算不了,前面做的渲染和编辑全是白费。公式引擎需要解析表达式、维护依赖树、处理循环引用、在单元格变化后自动重算。这一层复杂度,比前两关加起来都高。
所以我在调研一周后就放弃了“从零造轮子”的念头。市面上可选的商业组件确实成熟,但价格不低,而且在私有化部署和二次开发上总有各种限制。这时候 Univer 进入视野:代码开源、支持表格/文档/幻灯片、Canvas 渲染、插件机制,看起来是一个可以长期跟进的底座。
1.2 Univer 的定位:不是“一个表格组件”,而是“办公基础设施”
我见过不少人对 Univer 的第一印象是“又一个 SpreadJS 替代品”,这个理解不算错,但太小看它了。Univer 的定位更像是一个带渲染引擎、命令系统、插件体系的办公套件基础设施。它的核心仓库不只包含sheets(表格),还有doc(文档)、slide(幻灯片),你完全可以在同一个应用里把它们编排在一起。
这个定位带来一个直接好处:底层能力是统一的。比如渲染引擎、撤销重做、协同通信这些能力,表格和文档共享同一套基础;你在表格里积累的插件开发经验,以后做文档功能时也能复用。
我选择它的一个重要原因是插件化。大多数表格库的核心功能是打开的,但想加自定义能力只能靠 fork 修改源码,升级就痛苦。Univer 从架构上就是插件集合:公式、UI、数据验证、协同,都是可插拔模块。你不需要的功能可以不引入,不影响包体积;官方没覆盖的功能,也能顺着插件接口自己写。
顺带一提,Univer 官方有一个在线体验站,你可以直接导入 Excel、新建表格、测试公式和协同效果。我的建议是:先别急着看源码,把官网的 playgroud 玩一遍。大部分功能它已经具备,剩下的才是你要补的定制部分。
2. Univer 核心概念拆解:渲染引擎、命令系统和插件机制是怎么协作的
2.1 别被“前端组件”迷惑,Univer 是一整套分层架构
很多人第一次看 Univer 的初始化代码,容易发懵:为什么不是new Spreadsheet()这种一步到位的 API?因为 Univer 把功能拆成了多层。
从文档和源码里梳理下来,大致是这几层:
- core:数据模型、命令分发、协同状态管理,是整个应用的地基。
- engine-render:渲染引擎,负责把表格的“数据状态”画到 Canvas 上。
- engine-formula:公式引擎,解析表达式、维护计算依赖、处理重算。
- ui:通用 UI 框架,管理工具栏、弹窗、菜单这些 DOM 交互组件。
- sheets:表格业务逻辑,包括行列管理、单元格样式、选区模型、保护逻辑。
- sheets-ui:把表格业务逻辑与 UI/渲染串起来的地方,比如编辑器输入框、右键菜单、上下文工具条。
你用new Univer()创建的是一个宿主环境,然后通过addPlugin把表格能力装进去。这种设计的好处是:每一项能力都有自己的生命周期和依赖关系,你可以按需组合。
我在刚开始接触时有一个很深的感受:这不是一个“组件库”,而是一个“小型的办公应用开发框架”。用传统组件库的思维去套它,会觉得别扭;意识到它是框架后,思路就顺了。
2.2 命令系统与工作簿状态:为什么它敢管那么多场景
Univer 里几乎所有用户可感知的操作,最终都对应一条条Command(命令)。比如“修改单元格内容”是个命令,“合并单元格”是个命令,“设置工作表保护”也是个命令。
这套命令系统有两个关键用途:
第一,撤销重做变得简单且可靠。因为所有修改都经过命令封装,撤销时可以按命令逐个倒放。你不必自己记录一堆“修改前/修改后”的状态,只要命令列表还在,整个工作簿的历史栈就是完整的。
第二,协同计算的通信模型统一。多人同时编辑时,客户端A做了一次修改,本质是把一个 Command 序列化后发给其他客户端;其他客户端收到后执行同一条命令。这样服务器不需要感知“单元格被改成什么值”,只需要转发“谁执行了什么操作”。整套链路非常干净。
这个设计也直接影响后文要讲的工作表保护:设置保护本身就是一条命令,权限校验就挂在命令执行前的中间件上。理解了命令系统,你就能理解为什么 Univer 能实现很细粒度的编辑限制。
2.3 插件体系的边界:哪些是官方能力,哪些要自己写
Univer 官方已经把表格场景里的高频能力都实现了,包括:
- 公式的输入、计算、自动重算
- 单元格样式、合并单元格、行列拖拽
- 筛选、排序、数据透视表
- 条件格式
- 数据验证(下拉选项、数字范围等)
- 导入 / 导出 XLSX
- 在线协同(配合官方后端或自建服务)
但有两类事通常需要自己开发:
第一类是业务联动。比如“某些单元格只能由特定角色的用户修改”,这属于业务权限,Univer 只提供“锁定/解锁单元格 + 保护工作表”的基础机制,具体谁能改、什么时候能改,要由你的后端接口和数据模型来决定。
第二类是场景包装。比如你要做一个“数据收集表”,用户打开之后不需要看到 Excel 那样的复杂工具栏,只想看到一张简洁的填写页。这需要你基于 Univer 的插件能力或 UI 定制能力,把工具栏收起来、只暴露指定范围的交互。这件事本身不难,但需要理解它的 UI 扩展点在哪。
我的建议是:遇到一个 Univer 没直接提供的功能,先想一下“是否可以用命令系统或中间件实现”,而不是直接改源码。改源码一时爽,日后升级火葬场。
3. 从零跑通一个在线表格应用:依赖安装、初始化与数据加载
3.1 推荐的项目搭建方式和最小依赖组合
我建议用 Vite + TypeScript 起步,Univer 官方示例也基本都是这个组合。新建项目后,核心是安装 Univer 相关依赖。
不同版本包名会有差异,以我当时用的 0.x 系列为例,最省事的方式是使用 preset 包:
npm install @univerjs/presets @univerjs/presets-sheets如果你想更精细地控制依赖,可以分开安装:
npm install @univerjs/core @univerjs/sheets @univerjs/sheets-ui @univerjs/ui @univerjs/engine-render @univerjs/engine-formula我个人的建议是:如果只是做 POC,用 preset 包;如果准备正式生产,用独立包并明确锁定版本。因为 preset 是“官方拍板的最佳组合”,版本耦合关系已经处理好了,减少了不少坑。
3.2 初始化代码与核心参数说明
如果你用的是 preset 系列,初始化长这样(示意代码,具体以你安装到的版本官方文档为准):
import { Univer, UniverPresetSheets } from '@univerjs/presets'; import { UniverPresetSheetsFormula } from '@univerjs/presets-sheets-formula'; const univer = new Univer(); const preset = new UniverPresetSheets({ container: 'univer-demo', worker: { // 公式引擎可以在 Worker 中运行,避免阻塞主线程 formula: true, }, }); univer.addPlugin(preset); univer.addPlugin(new UniverPresetSheetsFormula());如果你的版本里没有 preset 包,用传统插件方式也能达到同样效果,只是需要多写几个addPlugin,把 core、render、sheets、UI 这些模块按顺序装好。
这里有两个容易忽略的点:
一是container对应的 DOM 元素要有明确高度。如果容器高度是 0 或者没设置,Canvas 渲染后会显示空白,排查半天才发现是 CSS 问题。
二是公式计算建议开 worker。公式引擎默认可以在主线程跑,但表格一旦出现大批量重算,会明显卡顿。放到 Worker 后,UI 线程保持流畅,代价是初始化时多一个 Worker 文件要配置。生产环境我强烈建议打开。
3.3 表格数据从哪来:loadData 的 JSON 结构与常见来源
Univer 的工作簿数据是一套 JSON 结构,核心单位是workbook,下面有sheet,每个 sheet 里有cellData、rowData、columnData、styles这些字段。最简单的方式是拿到一个工作簿快照后,用 API 加载进去。
我常用的一种加载方式是:
import { IWorkbookData } from '@univerjs/core'; const workbookData: IWorkbookData = { id: 'workbook-01', sheets: { 'sheet-01': { name: 'Sheet1', id: 'sheet-01', cellData: [ // 二维数组,表示单元格值 [{ v: '姓名' }, { v: '分数' }], [{ v: '张三' }, { v: 90 }], [{ v: '李四' }, { v: 85 }], ], rowData: [{ h: 24 }, { h: 24 }], columnData: [{ w: 100 }, { w: 100 }], }, }, }; univer.createUniverSheet(workbookData);我在实际项目里发现一个很实用的来源:直接把 XLSX 解析后的快照塞进去。Univer 的 UI 本身有导入 Excel 的能力,但如果你想在服务端预处理文件、然后把结果下发到前端,可以走解析流程生成 workbookData,再交给 Univer 渲染。
这里要留意一个坑:JSON 里的id必须全局唯一。如果你每次加载都复用同一个id,Univer 可能会把它当成同一个工作簿,导致状态残留。我建议每次生成新快照时用 UUID 重写工作簿和 sheet 的 id。
4. “用户填表、其他单元格锁死”的核心实现:工作簿保护与原理解析
4.1 先理解 Excel 系保护模型的默认状态:locked 与保护开关
现在进入这次需求的重点:“用户定义表格,其他单元格无法修改”。Univer 的模型和 Excel 一脉相承,里面有两个关键概念:
- 单元格锁定状态(locked):每个单元格样式里都能标记
locked或unlocked。 - 工作表保护(protection):保护开关,控制当前 sheet 是否启用“只读限制”。
默认情况下,所有单元格都是locked状态,但保护开关是关闭的。也就是说,没有启用保护时,locked 不生效,所有单元格都能编辑。
所以要达到“只有指定区域能填”的效果,正确的思路是:
- 把允许编辑的区域标记为 unlocked。
- 启用工作表保护。
- 可选地,设置保护细节,比如是否允许选中锁定单元格、是否允许排序、是否允许筛选。
这个思路和 Excel 里的“保护工作表”操作几乎一样。如果你做过 Excel VBA 里的UserInterfaceOnly或“锁定单元格后保护工作表”的操作,到这里会非常有亲切感。
4.2 实现步骤:解锁可编辑区域、锁定其余区域、开启工作表保护
Univer 中控制单元格锁定主要在数据快照或样式中,然后通过保护命令开启保护。
在实际业务里,我把它拆成三步:
第一步,确定可编辑区域。比如你要让用户在B2:C10填写数据,其余部分只读。那么在构造快照时,给这些单元格设置“未锁定”样式:
const editableCellStyle = { locked: 0, // 0 表示不锁定,允许编辑 }; const lockedCellStyle = { locked: 1, // 1 表示锁定 };如果你的数据来源是动态的,建议在加载或生成工作簿数据时,遍历你要开放的区域,给对应单元格写入 unlock 样式。
第二步,调用命令开启工作表保护。以我的项目经验,Univer 的 sheets 插件里提供了设置工作簿保护的命令,大致是这样(不同版本名称可能不同,以你实际安装版本为准):
import { SetWorksheetProtectionCommand } from '@univerjs/sheets'; univer.getCommandService().executeCommand(SetWorksheetProtectionCommand, { sheetId: 'sheet-01', protection: { password: 'optional-password', permission: { selectLockedCells: 0, // 是否允许选中锁定单元格,0 为不允许 selectUnlockedCells: 1, // 是否允许选中未锁定单元格 sort: 0, filter: 1, }, }, });这里我特别提醒一个点:保护开关只是限制了 UI 上的编辑入口。如果你把权限设置得很严,用户可能连“点一下锁定单元格”都做不到,这在某些填表场景下反而影响体验。所以我在做“数据收集表”时会允许选中锁定单元格、只限制编辑,而不是把它们当纯图表一样完全锁死。
第三步,验证效果。保护开启后,用户点击锁定单元格时,输入示例应该不会进入编辑态;而点击你解锁的区域,可以正常输入和修改。如果发现可编辑区域也变灰了,优先检查是不是样式数据没写进对应的单元格索引。
4.3 更精细的权限控制:允许排序、允许筛选、按用户区分的思路
Univer 的保护命令不止能限制编辑,还可以控制排序和筛选。这在“运营定义好一张表,业务人员只能填数据,不能打乱行顺序”的场景里非常实用。
- 允许筛选,但禁止排序:让用户可以按条件过滤自己需要填的行。
- 禁止筛选但允许编辑:适合纯录入场景,所有行都要看到,不能隐藏。
- 禁止调整行列:可以防止用户拖动行高列宽,保持模板外观稳定。
如果你的需求是“不同用户看到/编辑不同区域”,单靠一套保护规则不够。我目前用的方案是组合拳:
- 后端根据用户角色返回不同的模板快照,在快照阶段就标好哪些区域 unlocked。
- 前端加载后,再根据用户权限设置开启保护。
- 对于一些特殊约束(比如“管理员可以随时编辑任意区域,普通用户只能填白底格子”),我会在命令系统中间件里再加一道校验。
中间件的思路很简单:拦截单元格编辑命令,判断命令针对的行列是否在用户允许范围内,不在就拒绝执行。这是一种比工作表保护更灵活的“二次防线”。适合你不想依赖样式状态、希望把权限逻辑独立出来的情况。
无论是哪种方式,我都建议牢记一条原则:前端的保护只能解决“体验问题”,真正要防止有人绕过前端直接写数据,必须在后端接口做同样的权限校验。Univer 保护再强,也只是浏览器里的边界。
5. 把填表场景做得更专业:数据验证、公式联动与条件格式
5.1 下拉选项约束用户输入
“只让填指定单元格”只是第一步,现实里你往往还希望“填的内容必须合法”。Univer 自带数据验证能力,我最常用的是列表验证,也就是给某个区域配置下拉选项。
在表格里选择要设置的数据区域,配置数据验证规则,类型选“列表”,来源填上选项值,比如:
- 已提交 / 待处理 / 已驳回
- A / B / C
这样一个单元格就是下拉选择了,用户没法乱填。这在“运营定义模板、业务填写”的场景下非常有用,数据质量直接从源头提升。
如果你需要在代码里动态配置数据验证规则,Univer 也提供了相应的命令或 API。我建议优先在 UI 上进行手动配置,先看效果,再考虑把这套规则落到模板快照中。
5.2 公式联动:让汇总、评分、状态判断自动计算
模板制作者定义表格时,通常会预留公式单元格。比如:
- 一个“金额”列,等于“单价”乘以“数量”;
- 最底部一个“合计”,自动对所有填写行求和;
- 一个“状态”列,根据其他单元格填没填来自动判断“未完成/已完成”。
Univer 的公式引擎支持这种联动。在单元格里写入公式后,用户修改上游单元格,公式结果会自动更新。你不需要在前端自己监听单元格变化再做计算,公式引擎会处理依赖关系。
实际体验下来,用 Univer 做这种模板比纯表单更灵活。因为填写人可以临时加行、复制公式、甚至拉选填充,Excel 的习惯在这里依然适用。当然,如果你不希望用户加行,还是得靠保护规则把“插入行”也禁掉。
5.3 条件格式:把未填项标出来,减少人工核对
模板一旦被多人填写,收集方最头疼的是“谁还没填、哪块没填”。我一般用条件格式来兜底:
- 对某个必填区域设置规则:如果单元格为空,背景标成浅红色。
- 对“已提交”状态的行设置规则:整行变成浅绿色。
- 对超出范围的值(比如分数大于 100)设置异常标记。
这种视觉反馈能在用户填写过程中即时出现,比事后去后端跑数据校验直观得多。Univer 的条件格式能力覆盖了大多数我需要的场景,配置起来也不算复杂。
到这里,整个“可收集数据的表格模板”概念就很完整了:锁定区域保持格式,开放区域允许录入,录入内容被数据验证约束,下游公式自动计算,异常情况通过条件格式高亮。你会发现,这不只是一个 Excel 编辑器,而是一套面向业务的可配置数据收集系统。
6. 从 Demo 到生产环境的几个警告:性能、协同与版本
6.1 渲染层需要注意的交互细节
Univer 用 Canvas 做渲染,性能比 DOM 方案好很多,但这不代表你可以毫无顾忌地往里塞数据。
我的体会是:Canvas 渲染的瓶颈不在“显示 10 万行”,而在“频繁的全量重绘”。如果你把一个很大的数据快照一次塞进工作簿,初始渲染时仍然会有明显卡顿。解决办法是按需加载:先让表格展示一个较小的数据范围,用户滚动到接近底部时再加载下一批数据。
另外,Canvas 渲染的表格在“选中状态”“光标闪烁”“键盘输入”这些细节上,和传统 DOM 组件的体验有区别。比如中文输入法在某些版本上需要额外适配,会出现“候选框位置不正确”或“拼音上屏后内容错乱”的情况。遇到这类问题,优先查 Univer 的 issues,大概率已经有人提过,照着官方建议升级或改配置。
6.2 多人协同能力与后端服务的边界
Univer 在开源版里提供了协同计算的底层能力,比如基于命令的同步和合并机制。但要把项目真正变成“多人同时在线编辑一个表格”,你还需要处理:
- WebSocket 或 SSE 长连接服务
- 房间管理和连接状态维护
- 服务端鉴权与数据持久化
- 操作冲突时的最终一致性策略
我在评估协同时得到一个比较清晰的认识:Univer 解决的是“协同编辑器”这一层,你仍然需要一个实时后端。如果你不想自己搭全套服务端,可以考虑官方提供的云端协作能力或 Univer Online 托管方案;如果必须私有化部署,就要准备好投入相当精力在实时后端上。
对于大多数“用户填表”场景,其实不一定需要真正意义上的多人实时协同。更稳妥的做法是:每个人打开的是模板实例,数据提交到后端,后端合并展示。这样既避开了协同的复杂性,又满足了业务需求。只有当你要做“多个人同时改同一个工作簿”的强协同场景时,才值得引入完整协同服务。
6.3 版本迭代很快,锁定版本比追新更重要
最后这条是我最想强调的:Univer 的迭代速度很快,API 变动也不小。我在早期使用时,就遇到过从一个版本升级到另一个版本,addPlugin的参数结构发生了变化,或某个命令的字段名被重命名的情况。
我的建议是:
- 进入生产项目前,锁死主版本号。
- 升级前,先阅读官方 changelog,确认是否有破坏性变更。
- 不要一看到新版就升级,先把新版在独立分支里跑通示例再决定。
- 尽量把 Univer 相关的初始化代码收拢到一个单独文件里,这样未来适配变化时,改动范围可控。
如果你准备在正式项目里上 Univer,我的最后建议是:先盯着官方仓库的examples目录把常见场景跑一遍,再谈定制。它不是一个开箱即用的业务组件,而是一套需要磨合的底层框架;一旦你把它的架构思路摸透了,换来的将是比商业组件更自由的扩展空间。