☰
用Univer构建在线填报表格:单元格锁定与数据验证实战
2026/10/1 14:14:43 网站建设 项目流程

如果你要在自己的产品里嵌入一个在线表格,并且希望它像 Excel 一样能编辑、能美化,但又想控制用户只能填某些格子、其他格子全都锁定,那我强烈建议你认真看一下 Univer。它是一个用 TypeScript 写的开源电子表格基础设施,相当于一个你可以完全自己部署、自己定制、自己扩展的在线 Excel。这篇文章从一个真实需求出发——做一个带模板性质的在线填报表格,用户只能填写我指定的单元格,其余区域一律不可修改——完整记录我如何用 Univer 把这个功能落地,包括权限保护、数据验证、导入导出以及协作场景下的各种坑和心得。

全文不吹概念、不堆术语,只讲我怎么想的、怎么做的、踩了什么坑,以及代码可以怎么抄。适合正在选型前端表格引擎、或者已经在用 Univer 但还没搞明白权限体系的朋友。

1. 为什么用 Univer 做“可填写模板表格”

1.1 这个需求到底在说什么

先把需求拆开:我需要做一个在线表格,表格的列头、标题、公式、样式都是固定的,普通用户进来以后只能往某些空白单元格里填数据,其他单元格包括合并单元格、标题行、公式列统统不能动。以前这种需求特别常见于人力资源信息采集、供应链报价单、项目进度填报这类场景,本质上就是“把纸质表单变成线上模板”。

实现路径上,一般有两种思路。一种是完全自己画 UI,用 input 模拟单元格,这种做起来控制力最强,但一旦数据量上来、需要公式和样式排版,开发成本会翻好几倍。另一种是找一个现成的表格组件,通过配置项锁定单元格。Univer 走的就是第二条路,而且它把“锁定单元格”这个能力做成了权限体系,而不是简单的只读开关,这点很关键。

1.2 对比了一圈开源表格引擎后我选了它

我在选型时比过四个方案:Luckysheet、Handsontable、x-spreadsheet、Univer。直接说结论,直到今天,如果你想完全开源、免费、可商用、并且原生支持在线协作,这四个里头只有 Univer 能站得住。

方案协议技术栈公式/样式数据导入导出单元格权限实时协作二次开发友好度
UniverApache 2.0TypeScript / Canvas支持支持支持(权限点)原生支持高,插件机制
LuckysheetMITJS / jQuery支持支持有限需额外搭建中,维护速度一般
Handsontable商用/需授权JS部分支持部分支持支持无原生中,协议受限
x-spreadsheetMITJS基础有限无无低,功能简单

我重点说下 Handsontable,它确实是个很成熟的组件,权限控制也好做,但商用授权费用不低。如果你只是个人项目无所谓,一旦是公司产品商业化,要么付费要么换方案。Luckysheet 早期很火,社区基础好,但它底层依赖 jQuery,在 React/Vue 里集成总感觉有一层隔靴搔痒,而且它的在线协作能力不像 Univer 那样是从架构上设计进去的。

Univer 给我的感觉是“一开始就按产品级标准做基础设施”,它不止有 Sheet,还有 Doc 和 Slide,所以我只需要学习一套数据模型和命令机制,以后扩展别的不怕。

1.3 Univer 的架构优势在哪里

Univer 的架构核心是插件化。它把数据模型、渲染引擎、UI 组件、命令系统拆成了不同的包,比如@univerjs/core管数据,@univerjs/engine-render管 Canvas 渲染,@univerjs/ui管界面布局。这意味着你可以只加载需要的模块,也可以把表格嵌入到自己的产品 UI 里,而不是被它的框架反客为主。

另一个让我选择它的理由是“命令机制”。Univer 的所有操作都走命令,包括用户点击、API 调用、远程协作同步,都会变成一条条命令。这种设计看起来繁琐,但好处是权限校验可以在命令层统一做。也就是说,用户在界面上点击、或者第三方调 API,最终都会过同一套逻辑,不会出现界面锁死了但 API 还能改的漏洞。这也是我后面做单元格锁定最看重的一点。

2. 上手前的关键认知:先搞懂 Univer 的对象模型

2.1 最小化实例化:一个 sheet 就够

在动手写权限之前,先把 Univer 跑起来。目前官方推荐的快速接入方式是用@univerjs/presets,这个包把常用模块打包成了一个预设。我用的是 Vite + React + TypeScript 的项目,初始化核心代码只有这么点:

npm create vite@latest univer-form -- --template react-ts cd univer-form npm install @univerjs/presets

然后修改src/App.tsx:

import { useEffect, useRef } from 'react'; import { Univer, UniverPresetSheet } from '@univerjs/presets'; import '@univerjs/presets/lib/styles/index.css'; export default function App() { const containerRef = useRef<HTMLDivElement>(null); useEffect(() => { const univer = new Univer({ theme: 'default', presets: [ UniverPresetSheet({ container: containerRef.current!, header: true, toolbar: true, formulaBar: true, statusbar: true, footer: true, palette: true, }), ], }); return () => { univer.dispose(); }; }, []); return <div ref={containerRef} style={{ width: '100%', height: '100vh' }} />; }

跑起来之后,你会看到一个基本完整的在线表格界面,有工具栏、公式栏、行列表头、底部标签页。这里要特别留个心:container传入的 DOM 元素必须有明确的宽度和高度,否则 Canvas 渲染出来是零尺寸,页面白屏。我第一次接的时候就是容器高度没设置,结果表格区域直接“消失”了。

2.2 必懂概念:Unit / Workbook / Sheet / Range / Cell

Univer 的数据模型不复杂,但命名需要先对齐。最顶层叫 Univer 实例,一个实例可以包含多个 Unit,每个 Unit 对应一个文档类型,比如 Sheet Unit 就是一个工作簿。工作簿下面再挂多个 Worksheet,也就是我们常说的 Sheet 标签页。每一个 Sheet 里,单元格用 Cell 表示,多个单元格组成 Range 区域。

如果你只要做一个单页的填报表格,那结构基本就是一个 Univer 实例 + 一个 Sheet Unit + 一个 Worksheet。关键操作用到的 API 基本都是围绕Workbook和Worksheet这两个对象展开的。比如我后面的权限控制,就是先拿到某个 Worksheet,再对它做保护配置。

创建一个带初始数据的 WorkBook,常见做法是通过初始化时传入workbookData,它的结构大概是这样的:

const univer = new Univer({ theme: 'default', presets: [ UniverPresetSheet({ container: 'app', header: true, toolbar: true, workbookData: { id: 'workbook-01', sheets: { 'sheet-01': { id: 'sheet-01', name: '员工信息填报', rowCount: 100, columnCount: 26, cellData: { '0': { '0': { v: '姓名', t: 'STRING' }, '1': { v: '工号', t: 'STRING' }, '2': { v: '部门', t: 'STRING' }, '3': { v: '入职时间', t: 'STRING' }, }, }, }, }, }, }), ], });

这里的v是单元格值,t是单元格类型,STRING是字符串类型。Univer 的数据类型枚举还有NUMBER、BOOLEAN、FORCE_STRING等。模板的列头我就是这样写死的。

2.3 命令、变更与服务:Univer 的“一切皆命令”

刚接触 Univer 时,最让我不习惯的是所有修改都要通过命令去执行,而不是直接改对象属性。比如改一个单元格的值,不是sheet.setValue(...)这种简洁调用,而是要执行SetRangeValuesMutation这条命令:

import { ICommandService, SetRangeValuesMutation, CellValueType } from '@univerjs/core'; const commandService = univer.getCommandService(); await commandService.executeCommand(SetRangeValuesMutation.id, { unitId: 'workbook-01', sheetId: 'sheet-01', range: { startRow: 1, startColumn: 0, endRow: 10, endColumn: 3 }, value: { '1': { '0': { v: '张三', t: CellValueType.STRING } }, }, });

这是初始模板填充数据的方式。我一开始也觉得绕,但用熟了以后体会到它的价值:命令是“可记录、可撤销、可拦截”的。权限系统完全可以监听或拦截某些命令,比对用户是否有权限执行某一类操作。这种设计对在线协作尤其重要,因为所有人的操作都要通过命令合并成操作流,再同步给其他客户端。没有这套机制,想做协作和权限就是一团乱麻。

3. 实现“其它单元格不能改”:权限保护的完整实操

3.1 权限分层:工作簿/工作表/区域三级保护

Univer 的权限体系是我觉得它最值钱的地方。它把保护分成了几个层级:工作簿级、工作表级、区域级。工作簿级覆盖所有 Sheet,比如禁止所有人插入新的 Sheet、禁止调整工作表标签顺序;工作表级针对某个 Sheet,比如禁止编辑整个 Sheet 的单元格;区域级则是在工作表上划出特定范围,单独设置规则。

如果我直接在整张表上开启“禁止编辑”,那用户连模板区域带可填区域全锁死了,这不行。所以正确思路是反过来:先把整张工作表锁定,再在需要填写的位置加上区域保护例外规则,允许用户编辑这些区域。这有点像用编辑器里的“只读模式 + 局部白名单”。

Univer 的包名在不同版本里略有变化,总体是@univerjs/sheets-protection或@univerjs/protection这类模块。核心 API 围绕“规则 + 权限点”展开,权限点就是具体你能做什么、不能做什么。比如SetCellValuesPermissionPoint控制能不能改单元格值,SetCellStylesPermissionPoint控制能不能改样式,InsertRowPermissionPoint控制能不能插入行。

以我自己用的版本为例,伪代码如下:

import { SetCellValuesPermissionPoint, InsertRowPermissionPoint, DeleteRowPermissionPoint, EditContentPermissionPoint, } from '@univerjs/sheets-protection'; // 先拿到当前 Sheet const workbook = univerAPI.getActiveWorkbook(); const sheet = workbook.getActiveSheet(); // 设置工作表保护:拒绝所有编辑操作 sheet.getPermission().setPermissionPoint(new EditContentPermissionPoint(false)); // 为可填区域设置区域保护规则,覆盖默认的“拒绝”策略 sheet.addRangeProtection({ name: '可填写区域', ranges: [{ startRow: 1, startColumn: 0, endRow: 10, endColumn: 3 }], permissionPoints: { [EditContentPermissionPoint.id]: true, [SetCellValuesPermissionPoint.id]: true, [SetCellStylesPermissionPoint.id]: true, }, });

这段代码的意思非常直白:整个 Sheet 默认不能编辑,但第 1 行到第 10 行、A 到 D 列的范围是白名单,允许改值和样式。注意,不同 Univer 版本的方法名可能从addRangeProtection变成addRangeRule或者统一的SheetPermission服务提供,具体以你 npm 包里实际导出的类型为准,但思路是通用的。

3.2 前端直接锁死:保护整个表再解锁可填区域

有人可能会问,如果我把所有单元格锁死,但前端可填区域也允许编辑,用户能不能绕过前端直接通过浏览器控制台改数据?答案是能,纯前端锁定本来就是体验层面的限制,真正防篡改必须依赖后端校验。但产品这种场景,锁定不是为了防黑客,而是为了防误操作、防用户改坏模板,所以前端锁定完全够用。

在 Univer 里,还有一种更细的做法,就是给不同的区域设置不同的保护规则。比如 A 列字段名区域禁止编辑且禁止选中,B 列填写区域允许编辑但只允许填数字,C 列公式区域禁止编辑但允许查看。区域规则之间可以叠加,优先级高的规则生效。这个能力是真正支撑复杂业务场景的关键,比单纯的整表只读要灵活得多。

实操中建议先把模板所有静态内容写入,再开启保护。如果先开启保护再写数据,写数据这个动作本身可能被权限拦截,就得先临时关闭保护,写完再恢复。我一开始就在这个顺序上卡了好几次,后面统一改成“先填模板再上锁”。

3.3 配合数据验证,让可填区域也“只能填对的数据”

锁定之外,还有一层需求很常见:填写区域不能是任意内容。比如“员工工号”必须填数字,“部门”必须从下拉列表里选,“入职时间”必须符合日期格式。这就是数据验证(Data Validation)的活。

Univer 官方有数据验证模块,包名一般是@univerjs/sheets-data-validation。数据验证支持的类型包括数字范围、日期、文本长度、下拉列表、自定义公式校验等。和 Excel 的用户体验基本一致:选中单元格后会显示提示,输入非法内容时给出校验错误。

设置下拉列表的核心伪代码如下:

import { ICommandService } from '@univerjs/core'; import { SetDataValidationCommand } from '@univerjs/sheets-data-validation'; await commandService.executeCommand(SetDataValidationCommand.id, { unitId: 'workbook-01', sheetId: 'sheet-01', range: { startRow: 1, startColumn: 2, endRow: 10, endColumn: 2 }, rule: { type: 'list', formula1: '["研发部","市场部","人事部"]', showDropDown: true, allowBlank: false, }, });

这里formula1传的是逗号分隔的下拉项字符串。用户点击单元格会出现下拉箭头,只能选择我预设的部门名称,手动输入其他值会被拦截。在模板场景中,数据验证和单元格保护是搭配使用的,一个管“能不能写”,一个管“写什么”,两者结合体验就很完整了。

注意一点:数据验证的规则在导入导出 Excel 时的兼容性不能 100% 保证。我实测下来,Univer 导出到.xlsx文件后,再用 WPS 或 Office 打开,下拉列表、数字限制这类简单规则能保留,但非常复杂的自定义公式校验会退化成普通文本,这个在正式项目里要提前有心理预期。

3.4 在线协作场景下的权限补充

Univer 团队做这个项目时,实时协作是原生能力,不是后来加的补丁。每个客户端都连着协作网关,操作通过 WebSocket 发送到服务端,服务端做冲突处理和状态合并,再广播给其他客户端。在这个架构下,权限校验不能只依赖前端界面隐藏按钮,否则一个用户通过 API 改了单元格,其他客户端怎么办?所以 Univer 的权限校验逻辑在命令层、服务端都有机会介入。

具体到工程上,如果你需要多人同时编辑同一张表,并且不同用户看到相同模板但只能填自己的那部分,建议这样做:后端保存统一的模板结构,用户打开时前端从后端拿模板数据,渲染后开启同样的保护规则;用户提交的数据只提交填写区域内的单元格,后端合入数据库;如果需要多人实时协同,再部署 Univer 的协作服务端,并把授权校验挂到服务端命令入口。

这里提醒一下,Univer 的脚手架文件包含前后端两部分。前端只是编辑器,真正做数据持久化和权限控制,需要你自建一个服务端。服务端负责读模板、下发模板、接收填写结果、校验内容。如果你的产品是给企业内部几十个人用,不做实时协作其实也够,前端锁定 + 表单提交就能满足业务,不一定非要上完整协作链路。

4. 完整代码示例:从零搭一个在线填报表

4.1 工程初始化:Vite + React + Univer

我的项目结构很简单,就一个前端页面。初始化命令上面已经写过了,这里补充依赖安装时的版本意识。Univer 还在快速迭代,不同小版本的 API 变动比较大,所以建议安装时锁定版本号,不要直接装latest,否则今天能跑的代码下个月升级后可能就编译不过了。

npm install @univerjs/presets@0.1.0 @univerjs/core@0.1.0 @univerjs/sheets-protection@0.1.0

具体版本号以你开发时官方发布为准,我这里只是演示锁版本的做法。

4.2 注入表格数据和模板样式

初始化时如果不用workbookData,也可以通过命令动态创建。两种方式我都用过,模板场景我更倾向用workbookData,因为模板结构是固定的,初始化时就一次性写清楚,逻辑最简单。

模板样式方面,给表头加背景色、字体加粗,可以通过SetRangeStyleCommand来做:

import { SetRangeStyleCommand } from '@univerjs/sheets'; await commandService.executeCommand(SetRangeStyleCommand.id, { unitId: 'workbook-01', sheetId: 'sheet-01', range: { startRow: 0, startColumn: 0, endRow: 0, endColumn: 3 }, style: { bl: 1, // bold bg: { rgb: '#F2F2F2' }, ht: 2, // horizontal alignment center }, });

Univer 的样式字段是缩写,bl是加粗,bg是背景色,ht是水平对齐,vt是垂直对齐,fs是字号。第一次用确实记不住,我通常是查类型定义直接补全,而不是死记硬背。

4.3 导出 Excel 与恢复模板

模板填完之后,用户要能导出成 Excel 文件。Univer 提供了@univerjs/export-xlsx和@univerjs/import-xlsx模块,可以导出当前工作簿或导入已有 xlsx 文件。导出代码很简单:

import { ExportXlsxService } from '@univerjs/export-xlsx'; const exportService = univer.getService(ExportXlsxService); const result = await exportService.exportXlsx({ unitId: 'workbook-01', path: '员工信息填报.xlsx', });

这里有个细节:导入 Excel 后,原来设置的保护规则和数据验证可能丢失或变化。因为 xlsx 文件格式本身对保护和验证的支持不像 Univer 内部 API 那么完整。如果业务要求用户上传 Excel 作为模板,建议后端统一解析一次 Excel 然后转成 Univer 能识别的模板结构,不要在导入后依赖 Excel 自带保护。

4.4 关键代码片段解说

我把最核心的“模板初始化 + 保护 + 数据验证 + 导出”整理成一个逻辑顺序:

  1. 初始化 Univer,传入workbookData定义模板。
  2. 用SetRangeStyleCommand给表头加样式。
  3. 用SetRangeValuesMutation写入模板固定内容(比如标题行、公式列示例数据)。
  4. 获取当前 Worksheet,设置整表不可编辑。
  5. 给填写区域添加区域保护例外规则,允许编辑和修改样式。
  6. 给填写区域添加数据验证规则,限制输入类型。
  7. 用户填写完成后,通过前后端接口提交填写区域的数据。

这套顺序建议做成一个统一的初始化函数,不要让每个页面各自实现,否则后面维护权限逻辑会非常痛苦。权限规则的增删集中在服务端配置下发更安全。

在线协作模式下,模板的保护规则通常由服务端创建好再下发给所有客户端。客户端并不自己定义规则,只是展示规则带来的效果。这样可以让权限唯一集中,不用每个前端各写一套,减少不一致风险。

5. 常见问题与排查技巧实录

5.1 问题速查表

我整理了几个实操中最常遇到的问题和排查方向,直接列成表格,方便大家直接对照。

现象可能原因排查方向
整个表格白屏容器没有宽高检查container父元素尺寸
预设的工具栏按钮点了没反应插件未加载检查 presets 里是否启用了对应模块
设置了保护但还能编辑命令层没走权限校验检查是否真正执行了保护命令,以及权限点名称是否正确
数据验证下拉不显示缺失showDropDown属性确认规则配置里showDropDown: true
导出后保护丢失xlsx 格式限制导出前重新套用规则,或后端生成模板
协作模式下权限不一致客户端各自定义规则改为服务端下发统一规则

5.2 性能与渲染避坑

Univer 用 Canvas 渲染,大数据量下性能比 DOM 方案强很多,但它不是没有成本。如果你在一个 Sheet 里塞了 10 万行数据,初始化渲染依然会有卡顿。建议模板场景下控制数据量,比如实际填报表单的几十行完全无压力,但如果你要做数据大屏展示,那就用数据透视或分页加载的思路,不要试图在单个 Sheet 里渲染无限行。还可以关闭一些不用的预设能力,比如不需要幻灯片就在 preset 里只加载 Sheet 相关模块,模块少了初始化自然更快。

5.3 我踩过的坑和心得

最后分享几个真实的踩坑记录。第一个是最容易忽视的:Univer 的presets默认加载的是中文还是英文界面,取决于语言配置,记得在初始化时显式设置locale,否则会出现一半中文一半英文的界面。第二个是字体问题,Canvas 渲染时如果用了用户本机没装的字体,导出的 PDF 或图片里字体就会回退到默认字体,排版会变样。做正式模板前建议统一指定 Web Font 或用系统字体。第三个是命令失败一定看返回值,executeCommand返回的是命令是否执行成功的布尔值,不检查的话经常默默失败,尤其是权限拦截导致的静默失败,排查起来特别费劲。

我的体会是,Univer 是一个上限很高的工具,但它不是一个开箱即用、零学习成本的组件。你越熟练它的命令系统和权限模型,越能发挥出它的价值。尤其是模板填报这类场景,把保护规则和数据验证组合起来,体验和开发效率都远超自己硬画表单。如果你正在做的项目和这篇里描述的需求相似,直接按这个思路去搭,应该能把最核心的部分快速跑通。

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

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

立即咨询