☰
Univer在线表格实战:自定义收集表的权限控制与后端联动
2026/10/2 15:02:33 网站建设 项目流程

最近因为要做一套“用户自定义收集表格”的功能,我把 Univer 这个在线表格开源库从头到尾完整试了一遍。需求说起来很简单:领导或者管理员先在页面上定义一张表,把表头、说明文字、下拉选项都配好,然后把这张表发给其他人填写;填写的人只能往指定的格子里录入内容,其余位置一律不允许动。就是这样一个听起来不难的“填表”场景,真落到 Excel、WPS、老牌表格控件这些方案上时,每个都有让人难受的地方。Univer 是这段时间搜索热度上升最快的在线表格基础库,它给我的第一印象不是“又一个表格工具”,而是一套可以嵌进自己业务系统的表格基础设施。这篇东西算是我完整跑通 Univer 的记录,包含环境准备、权限控制思路、后端联动方案,以及几个我在实际项目里踩到并解决的坑。

1. 为什么我会把 Univer 当成“表格基础设施”,而不是又一个在线表格

1.1 它解决的是“业务系统需要一张活表”的问题

过去做在线填表,大多数团队会走两条路。

第一条路是直接用 Excel 文件:后端生成一个模板文件,用户下载后本地填写,再上传回来。这条路看着简单,实际上从模板生成、格式校验、数据清洗到版本管理,每一步都在给项目埋雷。用户改了不该改的表头,把公式删了,或者填了一堆根本不合逻辑的数据,你都得事后靠脚本去救。

第二条路是自己基于 HTML 画一个“看起来像表格”的表单界面。这在小规模场景下还凑合,但一旦涉及行数很多、单元格之间有关联计算、需要粘贴批量数据、要呈现冻结行列和筛选视图,自研成本就会疯涨。

Univer 在这两条路之外提供了一种新选择:它是一个以 TypeScript 为核心的办公套件 SDK,可以在你的页面里直接渲染出一个功能完整、体验接近桌面表格的在线表格。它不是 SaaS,不要求你把数据放到某个特定平台,而是把一个表格“组件”完整地交给你,由你自己的业务系统决定数据从哪来、存到哪去。对做填报表、台账、项目跟进表这类场景来说,相当于把最难的“表格引擎”外包掉了,你只需要专注自己业务逻辑那一层。

1.2 它拆出来的三个核心能力

我理解 Univer 的能力模型可以拆成三层:

  • 文档模型层(Core/Sheets):负责表格里单元格、行列、样式、合并、公式、数据验证这些数据的存储与计算,在整个 Univer 架构中相当于表格的大脑;
  • 渲染引擎层(RenderEngine):负责把文档模型画到浏览器里,处理选区、滚动、缩放、编辑框这些交互,相当于表格的眼睛和手;
  • 业务插件层(UI 相关插件):负责菜单、工具栏、右键菜单、弹窗等界面逻辑,让表格有完整的人机交互体验。

日常问题里的“用户能不能改这个单元格”,就同时涉及文档模型里的锁定位、渲染引擎里的编辑器交互,以及权限配置。你只改样式不够,只改交互也不够,这也是很多人在实践中最容易搞混的地方。我后面会专门讲这层拆解。

1.3 开源协议和商业使用的边界

Univer 主仓库采用的许可证以仓库里实际包含的 LICENSE 文件为准,主要包遵循开源许可方式发布。这意味着你可以把它拉下来看源码、改源码、部署到自己的服务器上,不用向某个平台注册或申请。但有两个细节需要注意:一是如果你修改了核心代码并对外提供服务,可能需要按开源协议要求保留版权声明并公开修改后的源码;二是 Univer 仓库里可能有部分字体、图标或设计资源遵循不同许可,使用时要留意。团队在决定是否采用前,建议把项目根目录的 LICENSE 和 NOTICE 文件都过一眼,这是正规流程,别跳过。

2. 十分钟跑通 Univer 工作区:从安装到渲染出第一个格子

2.1 环境准备和包依赖

我用的环境是 Node 18 以上,前端脚手架是 Vite。Univer 本身对构建工具没有特别要求,Webpack、Vite 都能正常跑。安装时先按需引入这几个包:

  • @univerjs/core:核心模型,包括工作簿、工作表、单元格、样式、公式引擎;
  • @univerjs/sheets:电子表格插件,提供表格文档模型的核心能力;
  • @univerjs/sheets-ui:电子表格的用户交互界面,包括选区、编辑框、右键菜单;
  • @univerjs/sheets-render:渲染引擎,负责在 canvas 上绘制表格;
  • @univerjs/sheets-formula:公式引擎,需要计算能力时引入;
  • @univerjs/ui和@univerjs/design:基础 UI 框架与设计语言。

安装命令:

npm install @univerjs/core @univerjs/sheets @univerjs/sheets-ui @univerjs/sheets-render @univerjs/ui @univerjs/design

如果你要用数据验证下拉、批注、查找替换、条件格式这些能力,还要从@univerjs/sheets-*系列里找对应插件。Univer 的包粒度比较细,好处是你不会把没用的代码打进来,坏处是刚入门的人容易被包名搞晕。我的建议是:先只装核心六个包跑通一个空表格,再按功能需求逐个加插件,不要一上来全装。

2.2 初始化一个最简工作簿

我接入时写的第一版初始化代码大致是这样的:

import { Univer, LocaleType } from '@univerjs/core'; import { UniverSheetsPlugin } from '@univerjs/sheets'; import { UniverSheetsUIPlugin } from '@univerjs/sheets-ui'; import { UniverRenderEnginePlugin } from '@univerjs/sheets-render'; import { UniverUIPlugin } from '@univerjs/ui'; const univer = new Univer({ locale: LocaleType.ZH_CN, }); univer.registerPlugin(UniverRenderEnginePlugin); univer.registerPlugin(UniverUIPlugin, { container: 'univer-container', }); univer.registerPlugin(UniverSheetsPlugin); univer.registerPlugin(UniverSheetsUIPlugin); const workbook = univer.createUniverSheet({ id: 'collect-sheet', sheetOrder: ['sheet-1'], sheets: { 'sheet-1': { id: 'sheet-1', name: '收集表', rowCount: 100, columnCount: 20, cellData: {}, }, }, });

页面里只需要放一个<div id="univer-container"></div>,样式高度自己控制。这一版跑起来后,你能得到一张 100 行 20 列、可以点击和输入内容的在线表格。我当时试完第一反应是:“这比我想象中轻。” 它的交互流畅度、选区绘制、滚动跟手程度,都已经是可以直接拿给业务方看的水平。

2.3 从业务 JSON 生成表格数据

Univer 创建表格时接受一段 JSON 来描述整个工作簿。单元格的定位是按{行号: {列号: 数据}}来组织的,行号和列号都从 0 开始。例如我想做一个报名登记表,把姓名、手机号、报名时间这几个表头放在第一行,初始数据按下面的结构传:

const sheetData = { id: 'collect-sheet', sheetOrder: ['sheet-1'], sheets: { 'sheet-1': { id: 'sheet-1', name: '报名登记', rowCount: 200, columnCount: 10, cellData: { 0: { 0: { v: '姓名' }, 1: { v: '手机号' }, 2: { v: '报名时间' }, }, }, }, }, }; const workbook = univer.createUniverSheet(sheetData);

这样你就拥有了一张带标题的空白表。核心的一点是:Univer 接受的是“工作簿快照”,也就是说你传给它的 JSON 既是创建参数,也是数据持久化格式。换句话讲,你把这份 JSON 存到数据库里,下次打开时原样丢回来,页面就能恢复成之前的状态。这就是我后面做“Univer 在线”时数据持久化最简单的入口。

2.4 渲染引擎的一些使用感受

Univer 的渲染是用 canvas 做的,这点和很多传统表格控件用 DOM 逐格渲染有本质区别。好处是就算你建一个几千行的大表,滚动起来也不会因为单元格数量太多而把浏览器拖垮。但代价也很明显:你不能用 CSS 去改单元格内部的表现,所有“长得好看”的需求都得通过单元格样式协议来控制。另一个体验是,如果你在container还没布局完成时就注册 UIPlugin,表格可能会出现宽高计算不对的问题。我习惯在window.onload之后再初始化,或者用ResizeObserver监听容器变化并调用刷新。

3. “能填的格子”和“不能填的格子”:Univer 里拆成三层来看

3.1 先拆需求:用户口中的“不能改”其实有三种含义

我拿业务方的原话去对实现方案的时候,发现“其他单元格用户无法修改”这句话在不同人嘴里意思不一样。归纳下来有三类:

  • 第一类:单元格内容不可被鼠标选中并修改,也就是只读锁定;
  • 第二类:单元格可以看,但我不想让它出现在编辑状态里,比如“可以复制但不许改”;
  • 第三类:单元格允许被改,但要满足格式约束,比如只能在几个选项里选,不能乱填。

这三类需求在 Univer 里对应的实现手段完全不同。如果你不拆开,写出来的代码很容易是“看起来限制了,但绕一下就能破”。下面我按层讲。

3.2 第一层:单元格样式锁定 + 工作表保护

Univer 的单元格样式协议里有一个锁定位。你可以把它想象成 Excel 里的“锁定单元格”,它只是给每个格子打了一个标,真正让锁定生效的前提是“工作表处于保护状态”。

实际做法是:创建表格时,把不允许用户编辑的表头、说明区域、公式区域统一打上锁定标记,其余需要用户填写的空格保持默认不锁定;然后对整个工作表开启保护。开启保护后,所有被打过锁定标记的单元格都会变成不可编辑状态,而未锁定的单元格仍然可以正常录入。

在 Univer 丢数据时,单元格样式可以放在单元格数据的s字段里,大概是这样:

const sheetData = { sheets: { 'sheet-1': { cellData: { 0: { 0: { v: '姓名', s: { locked: true, fill: { rgb: '#f2f2f2' } } }, 1: { v: '手机号', s: { locked: true } }, 2: { v: '报名时间', s: { locked: true } }, }, 1: { // 这一行没有 locked 标记,允许填写 0: { v: '' }, 1: { v: '' }, 2: { v: '' }, }, }, // 开启保护 protection: { protect: true, }, }, }, };

我再补充一点:protection的具体字段名在不同小版本里有过调整,有的版本需要把权限信息写完整。如果你用的版本和网上贴的不一致,最稳妥的方式是查你本地安装版本的 .d.ts 类型定义文件,Ctrl+F 搜 “protection” 或者 “Protection”,一两分钟就能锁定正确字段。Univer 尚在快速迭代期,网上抄来的代码报类型错误很正常。

3.3 第二层:拦截编辑行为,堵住“复制粘贴”和“键盘敲入”的路

只有单元格锁定还不够。实际产品里用户尝试绕过“只读”的方式很多:全选后按下 Delete、选中锁定区域直接输入、通过复制粘贴把内容覆盖进去。Univer 保护模式对常规操作确实有效,但如果你希望某些操作在交互层就直接被拦掉,可以在命令执行前加一层监听:Univer 的核心是基于命令模式工作的,任何编辑行为都会先产生一个命令,部分版本允许你在命令真正执行前介入并取消它。

通用思路是注册一个命令通道拦截器,监听beforeCommandExecute之类的钩子,判断当前选区是否落在“允许编辑”的范围外,如果是就直接阻止并给页面一个提示:

const allowedRange = { startRow: 1, endRow: 199, startColumn: 0, endColumn: 9 }; function isRangeAllowed(range) { return range.startRow >= allowedRange.startRow && range.endRow <= allowedRange.endRow && range.startColumn >= allowedRange.startColumn && range.endColumn <= allowedRange.endColumn; }

这种“允许范围”的写法,实际效果是把整张表变成了一个表单引擎:只有命中范围的区域可以编辑,其余一律拦截。这比单纯依赖样式锁更符合“部分格子可填”的业务语义,因为你能在拦截器里拿到操作的具体信息,可以做弹窗提示、写审计日志、就连“谁在什么时候尝试改了不该改的地方”都能记录下来。我这套逻辑后来被我单独抽成了一个工具函数,接进四个项目里复用,效果稳定。

3.4 第三层:用数据验证约束用户填进去的内容

能改的格子只能算第一步,填进去的东西合不合法是第二步。Univer 的电子表格插件支持数据验证,也就是给某个区域预设“只能是数字”“只能从下拉列表选择”“必填”这些规则。在做报名表这类场景时,我给手机号那一列加了“11 位数字校验”,给城市那一列加了下拉选项。用户填错时表会直接给出提示,从源头降低脏数据。

在业务里我建议把三套机制组合使用,下面这张表是我自己总结的对应关系:

用户实际需求主要手段补充手段
表头、公式、说明文字不可改单元格锁定 + 工作表保护命令拦截器兜底
只能填指定区域允许范围拦截设置选区样式高亮
填的内容必须是数字或选项数据验证提交前二次校验
填写后不可再修改提交态锁定整表后端拒绝变更请求

3.5 给“自定义表格”业务的设计建议

回到热搜词里的“用户定义表格”:Univer 并不会自动把你变成“填表类应用”,它只提供表格能力,定义流程需要你自己写。我的做法是先把“定义表格”和“填写表格”拆成两个模式:管理员进入“设计模式”时,创建表格、写表头、设置锁定和校验规则;填写人进入“填写模式”时,表格被加载为受保护状态,只有允许格子可以编辑。一份同一个工作簿 JSON,两种界面入口,逻辑互不干扰,这是我认为最省力的架构。

4. “Univer 在线”背后的持久化与权限联动方案

4.1 纯前端渲染不代表能脱离后端

很多人一听说 Univer 是纯前端的,就以为“在线表格”等于把表格渲染出来就够了,实际跑完才发现,渲染只是开始。你的表格数据如果只存在浏览器内存里,用户一刷新就全没了。所谓“Univer 在线”,核心是在线保存和在线协作,这少不了后端参与。我的落地方式是:后端只做两件事,存工作簿 JSON、做权限判断。

工作簿 JSON 可以直接落库。考虑到 JSON 字段可能会嵌套很深,在 MySQL 里我用json类型字段存取,在 MongoDB 里就直接存 document。为了减少传输体积,我会把工作簿快照做一层精简,只保留当前业务需要的 sheet 和 cellData,不把历史版本、临时状态都装进去。

4.2 最少改动的一套前后端接口设计

在设计后端接口时,我尽量避免了“把 Univer 的整个文档模型暴露给每个接口”这种高耦合做法。把接口收敛成四个就够初版跑了:

  • POST /api/sheet:创建一张表,后端生成工作簿 JSON,返回表 ID;
  • GET /api/sheet/:id:加载表,返回工作簿 JSON 和管理员设置的填写范围元数据;
  • PUT /api/sheet/:id:保存表,前端把编辑后的单元格数据形成 diff,提交给后端;
  • POST /api/sheet/:id/submit:完成填写,后端对数据做最终校验,然后锁定整表。

前端拿到管理员配置的允许填写范围后,把它和一整套工作簿 JSON 一起传给 Univer。这样“哪些格子能填”这个规则既存在于前端展示层,也存在于后端校验层。千万别只在页面上限制,接口层面也必须做同样的检查,否则别人构造一个请求就能绕过前端把数据改掉。

4.3 认证与“谁能填哪些格子”的联动

权限模型再往下走半步,就是具体到“不同用户看到同一张表,锁定范围不一样”。比如部门主管填的是审批栏,普通员工只填申请栏。Univer 本身不负责你的登录体系,它是通过“你给它什么样的工作簿快照”来体现权限差异的。

后端在返回工作簿快照时,根据当前登录用户的角色动态生成 cellData 里的锁定标记就行。同一个模板,给普通员工加载时审批栏是 locked,给主管加载时申请栏是 locked。这个方案实现成本非常低,因为 Univer 的锁定信息是数据的一部分,不是组件状态,后端改几个字段就完成了。

4.4 部署打包和体积控制

Univer 的各插件打包后体积并不小,毕竟它是一个完整表格引擎。首屏加载时建议把 Univer 相关的包单独打成 vendor chunk,开启 gzip 或者 brotli 压缩。我的实测经验是,按需加载插件比试图靠 tree shaking 把核心包拆没更靠谱。如果项目首页不需要表格,就把 Univer 初始化代码用动态 import 包起来,点击“编辑表格”那一刻再加载。初次商用之前,建议在低端安卓机上做一轮渲染测试,canvas 渲染在中低端移动设备的滚动手感会跟你开发机上有明显差异,但普通填报表场景的数据量一般不会构成瓶颈。

5. 实测中让我印象深刻的几个坑和应对方式

5.1 看起来锁定了,但双击单元格还是能编辑

这是我第一次做 Univer 权限控制时最典型的“假成功”现场:样式里加了锁定,保护也开了,但页面一跑,双击表单头背后的区域照样能打字。排查到最后发现是两个原因叠加。第一,我把单元格的锁定标记塞进了s字段,但保护配置跟工作表的id没对上,保护等于没生效。第二,编辑框的拦截逻辑写在了渲染层的点击事件上,而 Univer 的编辑启动有自己的一条命令链路,我在错误的位置做了拦截,自然拦不住。

解决路径很直接:先确认保护状态真的打进了文档模型,再确认命令拦截器挂在正确的命令总线上。如果你遇到同样问题,不要急着改代码,先在控制台里把工作簿实例打印出来,找到当前工作表的保护配置和允许编辑范围,看看和页面上呈现的行为是否一致,90% 的怪问题都能在这个环节发现。

5.2 全选删除能把“锁定区域”一起删掉

单元格锁定的常规理解是“不允许输入”,但真实用户的操作习惯千奇百怪。有人会全选整张表后按 Delete,或者拖选一大片区域做复制粘贴,测试时发现部分场景下锁定区域的内容还是会被改动。

这个坑的根源是保护机制和命令拦截器的覆盖范围不同。保护模式能挡住常规的单元格编辑,但批量操作时走的是另一套命令路径。我的修法就是前面说的那套拦截器:把“允许编辑范围”作为唯一事实来源,不管用户是单格编辑、批量粘贴还是区域删除,全部套进同一个范围判断函数里。写完后我把常用的破坏性操作全列成一个测试清单,包括输入、粘贴、删除、填充、拖拽,每一项都实际点一遍,确认没有漏网之鱼才算过。

5.3 用户填写超长文本把表格布局挤飞

Univer 的默认行高列宽是固定计算的,用户填入一大段文字后,单元格显示效果和自己本地 Excel 不太一样,经常出现“字还在,但表格看起来乱糟糟”的反馈。后来我在给填写区设置单元格样式时,把文本换行和纵向对齐都显式写进去了,同时在数据验证里限制了最大长度,体验才正常。这个小问题本身不难,但它提醒我一个道理:在线表格的表单化改造,永远要在需求阶段就把“填完长什么样”“填错怎么办”想清楚。

5.4 不同小版本之间的 API 差异

Univer 处于快速迭代期,我写这篇记录时的 API 形态和网上很多旧教程里的写法已经有出入,再过几个月可能又不一样。这不算缺陷,开源项目快速演进很正常。我的建议是:以你实际安装的版本类型定义为准,不要盲目复制旧文章里的代码。

6. 如果你也想做“可填写的在线表格”,最后给你几个实在建议

如果让我重新选型一次,我仍然会用 Univer,但会在一开始就做好三件事:第一,把工作簿 JSON 当成核心数据资产来管理,所有业务逻辑都围绕这份 JSON 展开;第二,把“可编辑范围”抽象成独立的配置,不给业务方直接暴露底层锁定能力;第三,把前端限制和后端校验当成一件事,永远不要只信任页面上那层规则。

我最后再分享一个小技巧:用 Univer 做填报表时,在创建表格的初始数据里把所有需要用户填写的格子先用浅色背景标记出来,让用户打开表格一眼就知道“这里可以填,那里不能填”。这个设计省下了大量“我该往哪里写”的咨询成本。把表格引擎的细节藏在底层,把业务的引导做在明面上,这就是“给用户一张表”这件事最舒服的体验。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询