Univer在线表格:单元格锁定与权限控制的完整实现方案 最近在做一张内部数据收集表需求特简单让业务部门只能填“本月目标”“实际完成”两个列其他像“部门”“负责人”“去年同期对比”这类由系统生成的单元格得全部锁死。折腾了几天最终在一个叫 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 和其他开源表格库的横向对比我拿最常见几个开源方案在同样需求下做了简单对比方案渲染方式单元格锁定/工作表保护大数据量表现许可与商用维护活跃度UniverCanvas支持命令体系完整优秀开源核心可用部分企业插件需授权高LuckysheetDOM CSS保护能力弱实现靠改源码一般MIT但作者更新放缓中低HandsontableDOM支持但部分能力取决于版本一般商业授权中x-data-spreadsheetCanvas无原生保护机制中等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 的插件体系和协同能力也给了扩展空间。表格这东西看着简单真正做起来全是细节希望这篇能帮你少走几步弯路。