最近在做一张内部数据收集表,需求特简单:让业务部门只能填“本月目标”“实际完成”两个列,其他像“部门”“负责人”“去年同期对比”这类由系统生成的单元格,得全部锁死。折腾了几天,最终在一个叫 Univer 的开源在线表格引擎上稳定跑起来了。Univer 用 Canvas 做渲染,公式计算放进 Web Worker,前端交互和 Excel 基本一致,相比旧版 Luckysheet 或者给 Excel 做 Web 套壳,Univer 在性能、可维护性和权限控制上都更让人省心。这篇就把“如何用 Univer 做一张锁定大部分单元格、只允许用户填特定区域”的完整方案拆开讲,里面有我踩过的坑,也有可以直接抄走的代码思路。
1. Univer 是什么,凭什么适合做在线表格
1.1 从需求说起:模板填报、权限控制是刚需
我接到这类需求不是一两次了。早些年做法很粗暴:直接让用户下载 Excel 模板,填完再上传,后端解析。听着简单,实际问题一堆——用户会改模板结构、会把公式删掉、会在不该填的地方填数据,甚至有人直接破坏单元格格式再上传。你花百分之三十的精力做完功能,剩下百分之七十都在清洗脏数据。
后来大家意识到,与其事后补救,不如把“可编辑范围”这件事直接收死在客户端。于是前端表格引擎成了必须品。需求通常长这样:
- 管理员能自由设计表格结构:表头、列宽、初始数据、公式、下拉校验。
- 普通用户打开表格后,只能编辑被允许的若干单元格或区域。
- 其他单元格虽然不能编辑,但要能被选中、可复制、可查看公式结果。
- 最好还支持并发协作、数据校验、锁定行列,避免用户误操作。
这些需求拆到最后,其实指向两个底层能力:一个是“单元格级别的锁定标记”,另一个是“工作表级别的保护开关”。Univer 恰好这两样都有,而且做得比较干净。
1.2 Univer 的三个核心优势
选 Univer 之前,我对比过好几套方案,最终让我下决心的是下面三点:
Canvas 渲染,数据量大不卡。
Univer 的表格主体不是用 DOM 一个个画格子,而是基于 Canvas 渲染,配合画布分层和按需绘制。同样两三万行数据,用 DOM 表格滚动起来掉帧是常态,Univer 这边基本能保持稳定帧率。你可能会说“我报表也就几千行”,但模板类表格拖久了数据会涨,一开始就选个上限高的引擎,后面不用重构。
公式与数据处理跑在 Web Worker 里。
表格计算最容易拖垮页面主线程。Univer 把公式引擎放到 Web Worker,再做一层和主线程的通信代理。大量单元格调公式、跨表引用的时候,主线程不会卡住,用户还能继续滚动和输入。这个特性在填报类场景里特别重要,因为这类表格往往全是引用公式,例如“实际完成/目标”的达成率列全是自动算的。
插件化架构,核心干净,功能按需装。
Univer 核心包只提供数据模型、命令系统、单元管理和渲染基础能力。具体功能比如右键菜单、剪贴板、公式、数据验证、协同编辑,全是独立插件。你要做的是按需组合,不需要的功能不注册就不打包。这一点对代码体积和可维护性都很关键,后面我会提到如何用官方 Preset 快速装配。
1.3 和其他开源表格库的横向对比
我拿最常见几个开源方案在同样需求下做了简单对比:
| 方案 | 渲染方式 | 单元格锁定/工作表保护 | 大数据量表现 | 许可与商用 | 维护活跃度 |
|---|---|---|---|---|---|
| Univer | Canvas | 支持,命令体系完整 | 优秀 | 开源核心可用,部分企业插件需授权 | 高 |
| Luckysheet | DOM + CSS | 保护能力弱,实现靠改源码 | 一般 | MIT,但作者更新放缓 | 中低 |
| Handsontable | DOM | 支持,但部分能力取决于版本 | 一般 | 商业授权 | 中 |
| x-data-spreadsheet | Canvas | 无原生保护机制 | 中等 | MIT | 低 |
这里说一句实在话:如果你的项目只是“展示一张静态表格”,那用哪个库都无所谓;但如果是“可填写、可校验、可锁定的业务表格”,Luckysheet 那种靠 DOM 模拟表格的方案,在复杂交互和大数据量下会越来越吃力。Handsontable 的编辑能力确实强,但商用授权成本不低。Univer 把“开源核心+扩展插件”的路线走得很明确,基础表格、单元格锁定、公式这些能力都能自己掌控,这是我们最终选它的主要原因。
2. 核心功能拆解:单元格锁定与编辑权限怎么实现
2.1 理解 Univer 的权限模型:锁定标记 + 工作表保护 + 交互开关
很多第一次接触 Univer 的人会混淆两个概念:单元格的locked属性和工作表保护。
简单打个比方:每个单元格天生带了一个“门锁标记”(locked),默认是锁上的。但这个锁有没有真正拦住用户,取决于“这栋楼有没有启用门禁”(保护是否开启)。只有工作表保护被启用时,locked 为 true 的单元格才不可编辑;如果保护没开,就算你给所有格子标了 locked,用户照样随便改。
Univer 的权限模型大致三层:
- 单元格锁定属性:每个单元格可以标记为锁定或未锁定,这是数据层面的基础标记。
- 工作表保护:对整个 Sheet 启用保护,并设置哪些操作被允许、哪些被禁止。比如是否允许编辑格式、插入行列、排序、筛选。
- 交互开关:保护选项里还有
selectLockedCells、selectUnlockedCells这类选项,控制用户能否选中被锁定的单元格。
这三层配合使用,才能真正做到“你指定可填的就那几个格子,其他区域既不能改,也尽量避免误操作”。
2.2 解锁编辑区域的完整链路:先解锁,再保护
实际开发时,我会把它拆成两条明确步骤:
第一步,先用批量命令把允许用户编辑的区域里的单元格标记为“未锁定”。Univer 里可以通过设置区域的值或属性来修改单元格属性,包括 locked 标记。这个操作要在工作表保护开启之前做,或者在前端装配阶段直接写进初始数据。
第二步,再执行工作表保护命令,把整个 Sheet 保护起来。因为默认所有单元格都是锁定态,所以保护开启后,唯一可以编辑的就是你刚才解锁的那一小块区域。
第三步,把保护选项里那些“危险操作”全部关掉。这里最容易漏:你光设 lock 不设 options,用户可能还是能插入行列、删列、改格式。需要把格式编辑、行列插入删除、排序筛选这些选项按业务需要关闭。
最后一个细节是选择行为。如果保护选项里禁止选择锁定单元格,那么用户点击被锁区域时,Univer 会直接拦截选中。这种体验在某些纯填报场景里很合适,但假如你的表格里有一些说明文字,用户想把说明文字复制走,这就会比较痛苦。所以一般我会把selectLockedCells保留为 true,只禁止编辑,不禁止复制和选中。
2.3 表单场景的完整交互设计:校验、下拉、公式联动
单元格锁定只是第一步。落地到业务填报,通常还要叠加三件事:数据校验、下拉选择、公式联动。
Univer 支持数据验证插件,可以给指定单元格配置下拉选项、数字范围、文本长度之类的规则。比如我在“状态”这一列配上下拉选项“未开始/进行中/已完成”,用户只能在三个值里挑,不会填出奇怪的内容。注意,数据验证和单元格锁定是两套独立机制,一般我会对解锁区域做校验,对锁定区域不做校验,因为锁定区域的公式会自动计算结果。
公式联动也是关键点。比如用户只需要填写“目标”和“实际完成”,而“达成率”这一列是锁定单元格,里面写的是=IF(实际列=0,0,实际列/目标列)。这样用户既能看到实时计算结果,又没法篡改结论。这正是“让用户填表格,但不让用户碰公式”的理想形态。
还有一点容易被忽略:协同时代的数据权限不能只靠前端界面。Univer 前端锁定是做给普通用户看的,懂技术的人依然可以调浏览器接口直接改单元格数据。所以如果你真要严谨,后面得配一层后端校验,我会在第四章详细讲。
3. 实操:从零搭建“只能填指定格子”的 Univer 表格
3.1 环境准备与依赖安装
我用 Vite + TypeScript 做前端工程,Univer 官方文档对这类工程支持得比较好。先初始化工程:
pnpm create vite univer-lock-demo --template vanilla-ts cd univer-lock-demo然后安装 Univer 相关依赖。这里要注意版本差异:Univer 3.x 起主推 Preset 装配方式,通过@univerjs/presets-sheets这类包把常用插件打包好,省去一个个手动注册的繁琐步骤。我建议直接按你实际安装版本的官方文档来,下面用的是相对新的装配写法:
pnpm add @univerjs/core @univerjs/presets-sheets如果你还需要公式、数据校验等功能,再额外安装对应插件包:
pnpm add @univerjs/sheets-data-validation @univerjs/sheets-formula装完依赖后,直接在入口文件里初始化。
3.2 初始化 Univer 实例
新建src/main.ts,写入 Univer 的初始化逻辑:
import { Univer, LocaleType } from "@univerjs/core"; import { UniverSheetsPreset } from "@univerjs/presets-sheets"; const univer = new Univer({ locale: LocaleType.ZH_CN, presets: [ UniverSheetsPreset({ // 开启你需要的基础能力 }), ], });这段代码做完,Univer 已经具备渲染工作表、编辑、快捷键、右键菜单等核心能力。接下来创建空的工作簿数据。
注意:如果你用的是更早的 1.x/2.x 写法,初始化方式会不一样,常见的是univer.registerPlugin(UniverSheetPlugin)这种手动注册方式。因此安装时候一定要锁定版本,别让 eslint 和类型检查逼着你迁移 API,那是浪费时间。
3.3 创建表格并配置单元格初始数据
Univer 创建单元时需要一个IWorkbookData结构。对于“一张表”的需求,最简单的数据如下:
const workbookData = { id: "demo-workbook", sheets: [ { id: "sheet-01", name: "月度目标填报", rowCount: 50, columnCount: 8, cellData: { 0: { 0: { v: "部门" }, 1: { v: "负责人" }, 2: { v: "本月目标" }, 3: { v: "实际完成" }, }, 1: { 0: { v: "销售一区" }, 1: { v: "张三" }, }, 2: { 0: { v: "销售二区" }, 1: { v: "李四" }, }, }, }, ], };cellData用“行号-列号”方式定位单元格。注意 rowCount 和 columnCount 要一起配好,否则默认列数可能不够用。这里面没有写“达成率”之类的公式,是因为我准备在后面单独用命令补,你也可以直接在 cellData 里写{ f: "=..." }来存公式。
创建单元:
const workbook = univer.createUnit(UniverSheetsPreset, workbookData);创建成功之后,页面应该能看到一个可交互的表格。初始阶段所有单元格默认锁定,但你还没开启保护,所以现在谁都能编辑。下一步才是真正的锁定逻辑。
3.4 执行解锁与保护命令:核心步骤
这是整篇文章最核心的部分。我先把允许用户编辑的区域(假设是“本月目标”和“实际完成”这两列,对应列索引 2 和 3)设置为未锁定,然后开启工作表保护。
在 Univer 的命令体系里,所有操作都是命令。我们直接用ICommandService执行命令。先拿当前单元和工作表:
import { SetWorksheetProtectionCommand } from "@univerjs/sheets"; const commandService = univer.getCurrentUnit()?.getCommandService(); const worksheet = univer.getCurrentUnit()?.getActiveSheet(); const unitId = workbook.getId(); const subUnitId = worksheet.getId();然后执行解锁。这一部分在不同版本里参数结构略有差异,核心思路是调用设置区域值的命令,把指定区域的属性修改为未锁定:
await commandService.executeCommand(SetRangeValuesCommand.id, { unitId, subUnitId, range: { startRow: 1, endRow: 19, startColumn: 2, endColumn: 3, }, value: { 1: { 2: { s: { locked: false } }, 3: { s: { locked: false } }, }, // 如果区域有多行,需要逐行列出 // 更通用做法是配合服务端生成整个对象矩阵 }, });如果你觉得逐行手写 value 太笨,完全可以用循环构造一个对象矩阵,把目标区域所有格子都设成{ locked: false }。这一步的关键是:只对允许编辑的区域置 false,其余区域保持默认的 true。
解锁完之后,执行保护命令:
await commandService.executeCommand(SetWorksheetProtectionCommand.id, { unitId, subUnitId, protection: { password: undefined, options: { selectLockedCells: true, selectUnlockedCells: true, formatCells: false, formatColumns: false, formatRows: false, insertColumns: false, insertRows: false, deleteColumns: false, deleteRows: false, sort: false, filter: false, }, }, });到这里,表格中除了解锁区域之外的所有单元格,都处于“可看不可改”的状态。用户点进本月目标、实际完成这两列,可以正常输入;点到其他列,虽然能选中能复制,但输入内容和编辑格式都会被拦截。
这里我要特意提醒一点:保护命令的参数结构在不同版本里经常微调,尤其是options里的字段名。你别背我这份代码,要用你装的版本里的类型定义为准。但“先解锁指定区域-再保护整表-最后关闭危险操作”这个链路是通用的,换版本也只是换字段名。
3.5 监听变更并做二次校验
前端锁定做完之后,我还加了一层前端兜底:监听单元格变更事件。Univer 的命令执行后会派发事件,我可以订阅它,判断这次变更是否落在合法区域内。
commandService.onCommandExecuted((command) => { if (command.id === SetRangeValuesCommand.id) { const params = command.params; // 校验 params.range 是否在允许编辑区域内 // 如果不在,可以提示用户并回滚,或调用 undo } });这段逻辑是给“非正常路径”兜底的。比如某些 UI 入口绕过保护限制,或者插件内部直接改数据,我们都能在命令层拦一下。但也别指望这层能做权限边界,真正安全还得靠后端。
4. 常见问题速查与实战避坑清单
4.1 保护后目标列仍不可编辑/仍可编辑,怎么定位
我遇到过最多的场景:明明解锁了指定列,开启保护后却怎么都点不进去;或者相反,锁了几个小时发现别人还能改。
先说第一种。优先检查options.selectUnlockedCells是否为 true。这个选项控制用户能不能选中解锁单元格,如果它为 false,哪怕单元格本身解锁,你也进不去编辑态。
再说第二种。假如解锁区域还能编辑,但被锁区域也能编辑,那大概率是工作表保护压根没开启。你可以检查一下命令返回结果或者 UI 上的工作表状态,有些版本会有isSheetProtected之类的状态查询接口。保护没生效常见原因是你没有传入对应 subUnitId,导致保护命令执行到了别的表格页签上。一张工作簿如果有多个 sheet,每个 sheet 都有自己的 subUnitId,保护是按 sheet 独立生效的。
4.2 命令执行后不生效,多半是参数结构问题
Univer 的命令系统对参数结构要求比较严格,字段名拼错、嵌套层级不对,命令执行后往往是静默失败或者直接抛异常。我的排查习惯是三步:
- 看命令返回结果,Univer 的大部分 executeCommand 返回的是 Promise,能拿到执行成功还是失败。
- 在 TypeScript 里把参数对象声明为命令定义的 Params 类型,让编辑器帮你检查字段。
- 如果还是不行,用最小复现,只执行一条空的工作表保护命令,不加任何 options,确认保护机制本身通不通,再逐步补选项。
还有一个隐蔽点:命令执行有前置条件,有些命令依赖 UI 已经注册完毕。如果在初始化的最早期就执行解锁和保护命令,可能拿到的是还未注册完的 commandService。稳妥做法是放在univer初始化完成并且页面渲染之后,再调用。
4.3 前端锁定只是表现层,后端要做三件事
这一点我必须单独拎出来说,因为很多团队栽在这里。Univer 的前端锁定本质是用户体验层面的约束,它解决的是“用户不会乱改”,而不是“用户不能乱改”。一个稍微懂点前端的人打开控制台,仍然能调用接口往你的表格数据源里塞内容。
所以真正严谨的权限控制要在后端补齐三件事:
第一,保存变更时校验范围。后端接口接收表格变更时,必须校验这次变更涉及的单元格 range 是否在服务端记录的可写范围内,范围外直接拒绝。
第二,工作表和单元格的授权规则存服务端。管理员定义了哪些列可写,这些规则要落到数据库,而不是只写在前端配置里。前端只负责从接口拉规则并按它设置锁定,真正的判定必须以后端规则为准。
第三,协同编辑场景下做冲突处理。多人在线填同一张表时,前端锁定能挡住大多数误操作,但并发写入同一单元格仍然可能发生。Univer 官方提供协同编辑能力,接入时一般会搭配服务端协同服务。如果你只是做了静态锁定而没有上协同,那就别开放多人同时编辑。
这三点做完,才敢说这套表格是能真正拿出去给业务部门用的。
5. 最后说几句我的落地体会
这套方案我实际落地过之后,最大感受是:Univer 把“Excel 体验”搬到 Web 端这条路走得很扎实,但它的学习成本不像表面看起来那么低。不是因为难,而是因为“命令太多、版本迭代快”。如果你决定用 Univer,第一件事是锁版本、读类型的提示,第二件事才是看文档写业务。别一上来就抄网上旧代码,大概率会踩 API 变更的坑。
我在第一次做解锁保护的时候,光是为了搞清楚“为什么保护命令执行了还是能编辑”,就花了大半天,最后发现是 options 里的 selectLockedCells 配置理解反了。这种问题在你自己的项目里也大概率会遇到。我的建议是:先把最小功能的 demo 跑通,再往里面加公式、校验、协同。先把“锁定-解锁”这条主链路打通,后面的复杂需求都是在它上面盖楼。
如果你只是做一张给用户填写的业务表,Univer 这套方案已经很够用;如果你想做一套完整的在线协作表格产品,Univer 的插件体系和协同能力也给了扩展空间。表格这东西,看着简单,真正做起来全是细节,希望这篇能帮你少走几步弯路。