ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Univer在线表格实战:锁定单元格与工作表保护实现指定区域填写

Univer在线表格实战:锁定单元格与工作表保护实现指定区域填写 接手过几个内部工具项目之后我越来越确认一件事业务方真正想要的“在线表格”往往不是又一个完全自由的Excel而是“长得像Excel的填单页”。大概两年前我接到这样一个需求客户希望在一个网页上直接填写合同信息但只允许填“乙方名称”“联系方式”“补充说明”三个位置其余表格内容一律不可改动。我的第一反应是做成网页表单结果对方负责人摆手说“我们团队平时都用Excel对数据就想要一张表我们习惯。”就这么一句话让我最终把项目底座换成了 Univer也把“表格里哪些单元格能编辑、哪些不能动”这件事从头到尾研究了一遍。现在的 Univer是一个开源、基于 TypeScript 的在线办公套件不光能嵌入网页显示表格、支持公式和数据处理还可以通过单元格解锁、工作表保护等方式精确控制使用者能触碰哪些单元格。如果你也想实现“用户定义表格、填写指定单元格、其余区域不可修改”的效果这篇就是我基于实际项目整理的落地过程从原理到踩坑都有。1. 从“一张不能乱填的表”说起Univer 的定位和选型先说选型时的处境。我当时的项目里表格要做到三件事第一展示一张带有完整表头、合并单元格、配色和预置数据的表格第二用户只能在特定位置录入信息第三录入后的数据要能回到业务系统里保存。这个组合听起来简单但真做起来每条路都不太省心。1.1 真正被提出的问题不是“在线Excel”而是统一填单模板业务方口中说的“表格”往往是他们脑子里的那张 Excel 样式第一行是标题第二行是字段名中间还有几条从旧系统里导出来的历史数据底部有统计公式。他们不希望用户把整张表都改了只希望用户把空出来的几个格子填掉。用户拿到的不是“编辑权限”而是“填写权限”。这就带来一个非常关键的产品判断你要做的不是给一个在线Office而是做一个以表格为外壳的表单引擎。Univer 恰好落在中间它既能百分百还原Excel的表格外观又能通过保护、校验、锁定等机制把编辑行为约束在指定区域。这也是我为什么没有自己写HTML表格也没有套用现成表单组件的原因。1.2 从四条技术路径看 Univer 的优势我把当时考虑过的方案列了一个对比四个选项分别是纯前端手写表格、上传Excel后解析展示、直接用在线文档平台、以及嵌入 Univer。方案实现成本表格还原度可编辑约束数据回收适合场景手写HTML表格低一般需要自己实现事件拦截自己拼JSON简单展示Excel解析后渲染中高规则繁琐回写麻烦一次性导入导出在线文档平台低高依赖平台权限数据在平台手里内部协作Univer中高原生支持锁定与保护通过API/JSON对接嵌入业务系统手写表格看着快但当你需要合并单元格、条件格式、冻结表头、公式联动时工作量会指数级上升。上传Excel这条路更适合“看数据”不适合“填数据”。在线文档平台确实强大但数据和业务系统隔离而且权限模型通常没有细到单元格。Univer 的优势在于它是开源组件可以嵌入我们自己的页面数据模型直接和业务后端对接。1.3 简看 Univer 的架构插件与数据模型Univer 给我的第一印象是“模块拼装”。它不是一个大而全的JS包而是若干个独立插件核心负责数据模型表格插件负责行列和单元格UI插件负责渲染工具栏与右键菜单公式引擎单独跑在Worker里。这样的好处是可以用最小集合启动一个只有表格的实例不需要的模块就不引入。更重要的是Univer 的数据模型和视图是分离的。也就是说你给 Univer 一份 JSON 格式的 workbook 数据它能渲染出一张表用户在界面上的每一次编辑又会回到这个 JSON 模型里。这个特性对“锁定单元格”的实现非常关键因为锁定的信息就存在于数据模型中而不是画在界面上的视觉效果。2. 核心机制单元格“锁定”和保护规则在 Univer 里是怎么配合的网上关于 Univer 的讨论里出现频率最高的一句话就是“Univer 支持用户定义表格然后让用户去填写一些单元格其他的单元格用户无法修改”。这句话其实就是在说两件事单元格锁定的粒度以及保护规则的作用范围。要把它做好得先理解这套机制背后是两道开关在协同工作。2.1 理解 Excel 的“锁定 工作表保护”双开关如果你用过 Excel 的“保护工作表”功能应该知道一个反直觉的设计Excel 里所有单元格默认状态是“锁定”的但你正常编辑却毫无阻碍。这是因为还有一个总开关——工作表保护——默认是关闭的。只有当工作表保护开启后被锁定样式的单元格才真正不可编辑。Univer 延续了这个模型单元格样式里有一个protection.locked字段表示这个格子本身是否允许被编辑工作表层面又有一个保护状态相当于总闸如果总闸没合上锁不锁都不影响输入总闸合上了锁定样式的格子就会被禁止编辑。要在界面里做到“只有我可以填的地方能填”正确的组合是把想开放的单元格解锁其余保持默认锁定最后启动工作表保护。只要启动保护所有锁定格子立即进入只读态而解锁的格子照常输入。2.2 Univer 数据模型单元格样式中的 protection 字段Univer 的单元格数据结构里值存在v字段样式存在s字段。样式对象里就包含protection配置。做成伪代码看是这样// 用户可填写的单元格解锁 底色提示 { row: 8, col: 1, v: , s: { protection: { locked: false }, bg: { rgb: #FFF9E6 } } } // 普通只读单元格保持锁定 { row: 8, col: 0, v: 乙方名称, s: { protection: { locked: true } } }一开始我看到protection.locked这个字段时下意识以为把locked: true放上去就完事了。后来才确认它只是“声明”这个格子不可编辑实际约束力要在保护规则生效后才体现。所以如果只改了单元格样式没有对工作表启动保护用户照样能点进去改只是改了以后样式状态和实际行为不一致容易造成“明明没解锁怎么不能填”的反向困惑。2.3 两种保护思路的取舍实际做保护配置时有两条路我分别试过思路一默认全部锁定只解锁指定区域。适用于表格结构长期稳定、以后还可能新增字段的场景。新增的格子如果没有主动解锁默认就是只读安全性最好。思路二默认全部解锁只锁定需要保护的区域。适用于大量区域需要自由编辑、仅有少数表头和数据区不能动的场景配置量更少但风险是忘了给新区加上锁。做合同填写这种业务时我强烈建议用思路一。因为这类表格的填写区通常是固定的几个字段而表格里承载的历史数据、说明文字、汇总公式往往更多全部锁定再逐个解锁逻辑上更符合“默认防御”的心态。3. 落地实操制作一张“只能填指定格子”的在线申请表原理清楚了接下来就是动手。我不会把某个版本的完整代码贴到这里因为 Univer 的版本更新速度快包名和初始化写法在不同系列里有差异但核心步骤和代码骨架是稳定的。以下方式配合当前主流版本使用没有大问题细节上以你自己的版本为准。3.1 最小化工程搭建我习惯先用官方预设包univerjs/presets起一套最小环境它会把核心模块、表格插件、UI 插件一次性注册好省去手动配依赖的麻烦。引入之后初始化一个 Univer 实例并创建带数据的 workbookimport { Univer, UniverInstanceType } from univerjs/core; import { UniverPreset } from univerjs/presets; // 创建 Univer 实例 const univer new Univer({ locale: zhCN, theme: default }); // 通过预设加载表格能力 UniverPreset.register(univer, { sheet: true, ui: true }); // 加载业务数据 univer.createUnit(UniverInstanceType.UNIVER_SHEET, { sheetOrder: [sheet1], sheets: { sheet1: { id: sheet1, name: 客户填写表, rowCount: 30, columnCount: 8, cellData: { 0: { 0: { v: 项目申报表 } }, 1: { 0: { v: 字段名称 }, 1: { v: 填写内容 } } } } } });这段代码跑起来后页面上已经有一张表格了。注意cellData的对象键是行号、列号从 0 开始计算未设置的单元格不会被遍历到所以不需要把 30×8 个格子逐个传上去。3.2 把需要开放的单元格解锁并给出视觉提示这一步的关键是把“能填的格子”在数据和视觉上同时突出。我给所有可填写单元格统一设置了浅黄色背景和locked: false然后在旁边用说明列写提示文字“此处可填写”。// 将第3行第2列即D列第4行按行列数量因设计而异设置为可填写 const editableCells [ { row: 3, col: 1, label: 乙方名称 }, { row: 4, col: 1, label: 联系方式 }, { row: 5, col: 1, label: 补充说明 } ]; editableCells.forEach((cell, index) { // 标签列 cellData[cell.row] cellData[cell.row] || {}; cellData[cell.row][0] { v: cell.label, s: { protection: { locked: true } } }; // 填写列 cellData[cell.row][1] { v: , s: { protection: { locked: false }, bg: { rgb: #FFF9E6 } } }; });视觉提示很重要。用户看到灰色区域不会去想“我能不能点”但看到黄底白框的区域就会自然判断“这是让我填的”。没有视觉引导的保护设置上线后一定会接到问询电话。3.3 对整张表启动保护或走代码保护轨道当页面上的表格渲染出来后我一般先人工检查一遍解锁情况然后启动“保护工作表”。在 Univer 的界面里可以通过工具栏或右键菜单找到“保护工作表”入口也可以在代码中直接设置保护规则。保护规则的关键参数是允许编辑的对象通常有两类任何人都不允许编辑除了解锁的单元格指定某个用户、某个角色允许编辑某些范围。第一种就是项目初期的状态。界面操作时Univer 会弹出设置面板让你添加“允许编辑的区域”这里的逻辑实际上是把提示里的“锁定样式”再次确认一遍。我在代码里则更倾向于提前把protection配置写进数据模型界面保护只做兜底。// 示意对工作表开启保护 const workbook univer.getActiveUnit(); const sheet workbook?.getActiveSheet(); sheet?.getProtection?.()?.setSheetProtection({ locked: true, allowSelectLockedCells: false, allowSelectUnlockedCells: true, allowEdit: false });这段代码在不同版本里 API 名称可能不同但它表达的是同一个动作锁定工作表选中的可编辑格子仍然开放。编辑时如果发现 API 报错优先去查当前版本的类型声明文件而不是硬套旧文档。3.4 配合数据验证限制输入内容只开放格子还不够。在合同填写场景里有些字段必须限制内容格式比如“联系方式”只能是手机号“补充说明”最多 200 字。Univer 的数据验证功能可以做到下拉列表、数字范围、文本长度甚至自定义公式校验。我通常给“联系方式”加一个文本长度的数据校验并设置错误提示给“部门类型”配置一个下拉选项让用户只能从预置列表里选。这样防护又深了一层用户即使获得了解锁格子的编辑权输入也不容易出格。数据验证建议在初始化数据时一并配置而不是等用户打开页面后再临时设置。因为 Univer 的校验规则也是数据模型的一部分提前写进去能保证每次打开都在同一套规则下。4. 我也踩过的几个坑保护范围、粘贴穿透与性能这部分是该篇博文里最想分享的实操教训。订阅配置看似简单真正上线后会遇到各种边角情况以下四个坑几乎每个都让我的项目卡过一两天。4.1 合并单元格与保护范围交错时的规则解析项目里为了表格美观我合并了不少表头单元格比如“基本信息”横跨三列“项目指标”又纵跨两行。问题来了当合并单元格区域被锁定而保护范围又把合并区域的一部分当成“可编辑范围”传入时Univer 的解析结果会不确定有时候整个合并区域都能编辑有时候完全点不动。我的排查结论是保护范围尽量不要与合并区域发生局部重叠。要么把整个合并区域设为解锁要么把整个合并区域设为锁定避免出现“半个合并区域可编辑”这种模糊状态。后来我调整了策略所有合并单元格统一处理成锁定和只读填写的单元格全部避开合并区域单独设置成独立格子。表格虽然看起来少了一些跨行跨列的“大气”但行为变得可靠了。如果你确实需要让用户在一个合并区域内填内容就把整个合并区域都解锁不要只解锁其中某一行。4.2 锁定不是防火墙CtrlV 粘贴越界的处理有一个很隐蔽的坑发生在浏览器端。用户在一个解锁单元格里复制了一大段内容然后鼠标点到旁边锁定单元格按CtrlV。部分浏览器对粘贴事件的默认行为不严格检查编辑权限内容可能被硬塞进锁定单元格界面上的数据模型也被污染了。我最初以为 Univer 会拦截所有越界输入后来发现它对键盘输入、单元格聚焦点击的拦截到位但浏览器原生粘贴事件这个环节在不同系统和浏览器上表现不一致。解决方式也很朴素监听表格区域的beforepaste事件在事件里判断当前激活的单元格是否锁定如果是直接preventDefault()阻止粘贴动作。tableContainer.addEventListener(beforepaste, (e) { const cell currentActiveCellRef.current; if (cell cell.isLocked()) { e.preventDefault(); } });不过也要承认这类前端拦截只能防住普通用户误操作拦不住刻意篡改。真正不可改的数据安全必须依赖后端校验。下面第5章会专门说这件事。4.3 不要把一万行空数据硬塞进初始化对象有次我拿到一份从旧系统导出的表格里面有一万多行。我脑子一热生成的时候把每行、每列的cellData都填上了{ v: }这种空对象页面上渲染速度直接掉到令人崩溃的程度滚动卡顿、输入延迟连工具栏都跟着掉帧。后来才意识到Univer 的表格模型对空白区域是按需处理的。cellData里没有的格子在模型层会被视为真正的空单元格并不会占内存。我那种把所有格子都塞满对象的行为等于凭空造出了一万行×几十列的数据压力。正确做法是只在cellData中写入有值的格子表头、公式、默认数据、解锁填写区。空白区域不要初始化让 Univer 自己处理。4.4 在 React 中使用 Univer 的生命周期问题项目是用 React 搭的我在一个弹窗组件里动态创建 Univer 实例踩到过两个典型的生命周期坑。第一个坑组件因状态更新导致重新渲染于是useEffect又被执行了一次新一轮的new Univer()被创建页面上出现了两张重叠的表格事件监听也重复了。第二个坑弹窗关闭时我只清理了 React 组件没有销毁 Univer 实例导致内存泄漏再次打开弹窗时变得异常卡顿。正确做法是把 Univer 实例存到useRef里只在空 ref 时创建组件卸载时调用实例的销毁方法并断开对 DOM 容器的引用。const containerRef useRefHTMLDivElement(null); const univerRef useRefUniver | null(null); useEffect(() { if (!univerRef.current containerRef.current) { univerRef.current createUniverApp(containerRef.current); } return () { univerRef.current?.dispose(); univerRef.current null; }; }, []);这个细节表面上是代码规范问题实际上影响着线上稳定性。Univer 这种工具类组件生命周期管理必须当一等公民对待。5. “在线”的完整含义多人协作与后端校验闭环“univer在线”这个热词包含了很多人对 Univer 的期待。实际项目里在线并不意味着只要把表格塞进网页就行更多时候还需要协作、权限、数据回传这一整套闭环。5.1 单机嵌入与 WebSocket 协作的差别Univer 对协作支持很完整但有一个前提协作必须建立在对应的同步机制上。简单说如果你只是把 Univer 嵌入一个普通页面每个人看到的都是自己的一份数据副本改完互不相干这属于单机嵌入模式。要让多个用户同时编辑同一张表数据必须经过一个同步通道。Univer 的服务端协议解决了这个问题所有客户端连接到同一个服务端每边操作都通过增量更新广播出去状态最终一致。我这里想提醒的是不要把一个单机嵌入的 Univer 页面包装成“多人实时在线表格”。用户 A 改了接口用户 B 看不到那不是在线协作是“多终端编辑同一个静态副本”。做真正的协作至少要考虑服务端部署、连接管理、冲突处理三层问题。5.2 真正可靠的权限闭环编辑器保护 服务端校验我在第4章已经说过前端锁定防不住技术型用户。浏览器开发者工具可以把保护规则改掉也可以直接用脚本向 Univer 的数据模型注入内容。如果你只靠protection: { locked: true }来保证数据安全那是拿纸糊墙。可靠的做法是双层校验第一层Univer 层面的锁定与保护用来规范普通用户的输入行为提升体验第二层业务后端在收到数据时按配置好的可编辑范围重新校验。比如通过接口提交的数据中如果包含了锁定单元格的改动后端直接拒绝这条更新或只把允许的字段写入数据库。我现在的习惯是把“哪些单元格可编辑”这份配置存一份在服务端数据库里用户每提交一次变更后端都对照这份配置做校验。前端锁定是体验后端校验才是秩序。5.3 数据回收与业务系统打通表格页面如果只是给人填填完数据没人管那这个在线表格就只完成了一半。Univer 的数据模型可以导出成 JSON也可以把用户修改的增量数据抛给接口。我当时的做法是监听 Univer 的单元格变更事件记录发生变化的行号和列号把变更内容打包成一个变更列表定时或点击“提交”时发送到后端。后端再把这批变更合入数据库里的目标记录。univer.getActiveUnit()?.onCommandExecuted((command) { // 只关心单元格值变更类命令 if (command.id sheet.mutation.set-range-values) { collectChanges(command.params); } });这里面的关键是增量收集千万不要每改一个格子就把整张表传给后端。尤其对大表格来说全量提交既浪费流量又容易引起版本覆盖问题。6. 一点后续扩展和我个人做这类项目的固定习惯功能上线后这个“有保护的在线填写表”其实变成了部门里一个日常使用的工具。后来我又顺手加了几个能力让它的使用体验更接近一个真正的表单系统。我加了下拉选择把“地区”“合同类型”这些字段配置成预置选项用户只能从里面选给填写区加了字数统计和必填校验提交成功以后自动把整张表导出一份 PDF 快照方便存档。这些扩展成本不高因为它们都建立在 Univer 现有能力之上。按我个人习惯现在再做这类项目会直接固定住几件事初始数据里只写有值的格子空白区域绝不给空对象可填写区统一用浅色背景标记并同步配置数据校验工作表保护默认开启解锁单元格数量越少越好服务端保存一份“可编辑范围配置”提交时做二次校验组件销毁时记得调用 Univer 实例的销毁方法防止内存泄漏。Univer 是一个足够灵活的底座但灵活也意味着责任。你精心设计的保护规则、锁定样式和数据校验最终的兜底能力还是要靠前后端配合。如果你把“锁定单元格”理解成一道 UX 设计而不是一道安全边界那么用它做出来的在线填写表就能既好看又稳当。
返回列表