ARTICLE DETAIL

资讯详情

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

Univer在线表格:用户自定义表格与单元格权限控制的实践指南

Univer在线表格:用户自定义表格与单元格权限控制的实践指南 最近在社区里看到关于 univer 的讨论明显多了起来热搜词连续出现了univer、univer 支持用户定义表格、univer在线这些组合。作为一个常年折腾表格类产品的开发者我非常理解这个需求为什么会频繁出现在线表格编辑器最核心的价值不是替用户画一堆格子而是让业务人员可以自定义表格结构再按规则开放一部分单元格给填表人。这类场景在报名表、数据收集、内部审批、绩效填报里实在太常见了。Univer 是一个开源的在线表格/文档/幻灯片解决方案底层基于 Canvas 渲染用 TypeScript 开发最大的特点是插件化架构和前后端分离。它解决的核心问题是你不需要从零写一套 Excel 的渲染引擎就能在自己的产品里嵌入一个接近桌面级体验的在线表格。适合的对象很明确想给自己的 SaaS 产品加表格能力的前端团队、做低代码平台需要表单 嵌套表格能力的团队以及像我这样需要快速交付可填写的在线表格项目的个人开发者。在后面的内容里我会从选型思路、需求拆解、环境搭建、权限控制实现到实测中的坑一层层把用户定义表格 指定单元格可填写 其他单元格不可改这套方案讲明白代码给到可以直接跑的程度。1. 为什么是 Univer在线表格的选型与定位1.1 在线表格方案的横向对比做在线表格能力选型的时候大部分团队会在几条路线之间纠结方案优点缺点基于近年流行的开源表格库如 Luckysheet、x-spreadsheet集成快社区有基础代码大数据量渲染卡顿功能扩展基本靠改源码引入重量级套件OnlyOffice 等与 Office 格式兼容性强部署重前后端耦合深定制交互成本高使用商业产品嵌入方案效果好省心有平台绑定无法深度定制与私有化Univer渲染性能好插件化架构前后端分离版本迭代快需要盯紧文档更新我的选择逻辑很简单项目需要在私有化环境中嵌入表格同时要深度定制哪些单元格能编辑这类权限交互所以必须选一个不受平台约束、能控制渲染层和命令层的开源方案。Univer 的 Canvas 渲染引擎在大数据量下依然能保持流畅交互而插件化的设计让我可以只加载表格模块不需要为用不到的功能付出体积和复杂度代价。所谓不受平台约束指的是表格的渲染、命令、数据模型这些关键部分都在自己手里。商业方案通常只提供 iframe 嵌入你在外面只能做有限二次开发Luckysheet 这类的旧一代开源方案虽然也能改源码但底层基于 jQuery Canvas 混合架构越往深处改越吃力。Univer 整体用 TypeScript 编写模块边界清晰这对需要长期维护的业务来说非常重要。1.2 Univer 的核心架构理念理解 Univer 的架构对后面实现权限控制特别有帮助。它分成几层univerjs/core最底层的数据结构和命令系统。所有对表格的操作本质上都是通过命令Command来修改数据模型。univerjs/sheets表格的领域逻辑层负责行列、单元格、选区、样式等纯逻辑计算。univerjs/sheets-uiUI 层负责 Canvas 编辑器界面渲染。univerjs/preset-sheets预设包把核心模块和 UI 模块打包好方便通过 npm 快速初始化。我在实际开发中最大的体会是Univer 的命令系统非常关键。当你有禁止用户修改某些单元格这类需求与其在 UI 层拦截鼠标事件不如在命令层做控制。何况用户的操作入口远不止鼠标输入这一种键盘录入、粘贴、拖拽填充、公式批量填充最终都会走命令通道。在命令层做权限判断才能保证控制逻辑不会漏掉入口。这也是我把 Univer 和其他同类方案区分开的一个重要标准。别的方案是给你一个可配置的编辑器Univer 更像是给你一套可以改写的表格引擎。权限控制这种强业务相关的功能恰恰需要这样的可改写能力。2. 核心需求拆解用户填写指定单元格其他区域锁定2.1 用户定义表格 部分单元格可填到底在说什么把热搜词翻译成业务语言这个诉求通常长这样管理员或业务人员先在系统里创建一个表格自己定义表头、结构和一些计算字段这就是用户定义表格。填表人打开这个表格看到的是管理员设计好的结构。管理员预先开放某些单元格比如姓名联系电话报名项目填表人只能在这些单元格里输入内容。其余单元格比如表头、说明文字、公式区域填表人无法选中编辑更不能修改公式或格式。这是典型的数据收集型表格需求。它的核心不是做一个能自由编辑的表格而是做一个有管理权限的轻量应用。只有把需求拆成表格 静态结构 开放填写区 受控数据这三个部分才能选对实现策略。我见过不少团队在做类似需求时直接用表单组件拼一个界面出来Excel 模板只是展示。这样做的弊端很明显一旦业务人员想调整字段顺序、增加几行备注、改动计算逻辑就要提需求让开发改代码表单的灵活性完全体现不出来。而用表格做容器业务人员可以在受限范围内自己调整结构这正好契合用户自定义表格的诉求。2.2 权限设计策略先解锁再保护做单元格级编辑权限我推荐采用和 Excel 一致的模型它最成熟用户心智成本也最低每个单元格有一个lock属性是否锁定。默认情况下所有单元格是锁定状态。当工作表开启保护模式后锁定的单元格不可编辑未锁定的单元格lock: false可编辑。即使开启保护你也可以通过允许编辑区域allowEditRanges额外指定一些区域不受保护。在这个模型下实现只允许填 B2:C4就变成三步准备数据创建表格、写入表头和说明文字。设置开放区域把需要用户填写的区域如 B2:C4的lock属性设为false。开启保护对整张工作表开启保护此时除 B2:C4 外全部不可编辑。这套策略的好处是如果后续要新增一个可填字段不需要重新做权限判断逻辑只需把新区域的单元格设为lock: false即可保护层不用动。如果你的业务还要求不同角色看到不同填写区那就在角色进入页面时动态执行这套操作逻辑也是同一套。3. 环境准备与 Univer 初始化3.1 从零搭一个最小项目我用 Vite React 做了验证这里也按这个路径写。纯前端项目不需要后端也可以先把效果跑出来。先初始化项目npm create vitelatest univer-demo -- --template react-ts cd univer-demo npm install然后安装 Univer 预设包npm install univerjs/preset-sheets univerjs/core如果你用的是较新的 Univer 版本0.x 系列包路径基本就是这样。不同版本之间 API 可能会有调整但整体结构是稳定的。注意安装版本建议直接使用 npm 上的 latest。Univer 的版本迭代比较快旧版本可能存在已修复的 bug直接踩坑没必要。3.2 最小可运行的 Univer 页面在App.tsx里写一个最简单的 Univer 实例import { Univer, defaultTheme } from univerjs/preset-sheets; import { UniverInstanceType } from univerjs/core; import { useEffect, useRef } from react; import univerjs/preset-sheets/lib/styles/index.css; export default function App() { const containerRef useRefHTMLDivElement(null); useEffect(() { const univer new Univer({ theme: defaultTheme, }); // 创建一个表格单元 univer.createUnit(UniverInstanceType.SHEET, { id: workbook-1, name: 报名信息收集表, rowCount: 30, columnCount: 10, }); return () { univer.dispose(); }; }, []); return ( div style{{ width: 100%, height: 600px }} div ref{containerRef} style{{ width: 100%, height: 100% }} / /div ); }跑起来后你就能在页面上看到一个可以自由编辑的表格了。这里有一个细节Univer实例创建后会自动挂载到默认容器上。如果你在同一个页面创建多个实例需要显式指定容器否则多个编辑器会错乱地挂到同一个节点。我在第一次做多实例测试时就踩了这个坑页面看起来像两个表格叠在一起怎么点都不对。3.3 初始数据准备为了模拟用户定义表格我建议在创建单元时直接写入表头和数据或者在创建后通过命令批量写入。先给出直接写入的方式univer.createUnit(UniverInstanceType.SHEET, { id: workbook-1, name: 培训报名表, rowCount: 30, columnCount: 10, cellData: { // 第 1 行表头 0: { 0: { v: 姓名 }, 1: { v: 联系电话 }, 2: { v: 报名项目 }, 3: { v: 备注管理员填写 }, }, // 第 2 行提示与示例 1: { 0: { v: 张三 }, 1: { v: 仅登记用不开放编辑 }, }, }, });这种初始化方式的优点是单元格数据、样式、合并信息可以一次性声明。但如果你需要从后端读取一个 Excel 或 JSON 模板通常会用 Univer 的导入能力比如univerjs/sheets-import插件把文件转成工作簿结构再交给createUnit渲染。后文我会重点讲在页面上如何通过命令去修改这些数据。4. 实操实现单元格编辑权限控制4.1 关键 API 与执行路径实现权限控制主要涉及两个层面样式层通过设置单元格样式把指定区域的lock设为false。这个动作在 Excel 中对应的就是设置单元格格式 — 保护 — 锁定的取消勾选。保护层对工作表开启保护Worksheet Protection。开启后Univer 会检查单元格的lock状态锁定单元格只读未锁定单元格可写。在 Univer 中可以通过命令服务执行类似下面的命令不同版本命令名可能略有差异以官方文档为准const commandService univer.getCommandService(); // 1. 解析工作簿与工作表 id const workbookId workbook-1; const worksheetId sheet-1; // 2. 将 B2:C4行索引 1-3列索引 1-2设置为未锁定 await commandService.executeCommand({ id: sheet.command.set-range-style, params: { workbookId, worksheetId, ranges: [ { startRow: 1, endRow: 3, startColumn: 1, endColumn: 2, }, ], style: { lock: false, }, }, });上面是解锁区域的命令。接下来是保护工作表// 3. 保护工作表 await commandService.executeCommand({ id: sheet.command.set-worksheet-protect, params: { workbookId, worksheetId, protection: { // 可选参数是否允许选中锁定单元格、是否允许选中未锁定单元格等 selectLockedCells: true, selectUnlockedCells: true, }, }, });执行完这两条命令效果就是用户仍然可以点击 B2:C4 输入内容但点其他单元格时虽然能看到选区却无法编辑内容。注意set-range-style这条命令在 Univer 中也用于改字体颜色、背景色、边框等style对象里不止lock一个字段。你只需要设置lock: false不必把整份样式都传进去否则会把之前的格式覆盖掉。4.2 完整代码示例可直接跑我写一个更完整的 React 示例把创建表格 写入表头 解锁填写区 开启保护四件事放到一起import { Univer, defaultTheme } from univerjs/preset-sheets; import { UniverInstanceType } from univerjs/core; import { useEffect, useRef } from react; import univerjs/preset-sheets/lib/styles/index.css; const WORKBOOK_ID wk-baoming; const WORKSHEET_ID sheet-1; export default function App() { const containerRef useRefHTMLDivElement(null); useEffect(() { const univer new Univer({ theme: defaultTheme }); univer.createUnit(UniverInstanceType.SHEET, { id: WORKBOOK_ID, name: 培训报名表, rowCount: 30, columnCount: 10, cellData: { 0: { 0: { v: 姓名 }, 1: { v: 联系电话 }, 2: { v: 报名项目 }, 3: { v: 备注 }, }, }, }); const commandService univer.getCommandService(); (async () { await commandService.executeCommand({ id: sheet.command.set-range-style, params: { workbookId: WORKBOOK_ID, worksheetId: WORKSHEET_ID, ranges: [ { startRow: 1, endRow: 3, startColumn: 1, endColumn: 2 }, ], style: { lock: false }, }, }); await commandService.executeCommand({ id: sheet.command.set-worksheet-protect, params: { workbookId: WORKBOOK_ID, worksheetId: WORKSHEET_ID, protection: { selectLockedCells: true, selectUnlockedCells: true, }, }, }); })(); return () univer.dispose(); }, []); return ( div style{{ width: 100%, height: 600px, display: flex, justifyContent: center, alignItems: center, background: #f0f2f5, }} div ref{containerRef} style{{ width: 1000px, height: 500px }} / /div ); }跑起来后你切换到浏览器中点击表格B2、B3、B4、C2、C3、C4 这几个单元格可以正常输入其他单元格即使弹出选区也无法输入文字和数字。4.3 保护范围与动态控制补充上面是静态设置实际业务往往需要动态控制。比如用户身份不同填写的字段不同。这时候建议把权限配置下沉到后端由接口返回哪些区域可编辑前端拿到后动态执行命令。我常用的一种方式是在后端返回如下 JSON{ workbookId: wk-baoming, editableRanges: [ { startRow: 1, endRow: 10, startColumn: 1, endColumn: 2 } ] }前端拿到后先对所有单元格设置锁定或让所有单元格保持默认lock: true再遍历editableRanges设置lock: false最后统一开启保护。这样即使表格结构变化只需要改配置不需要改代码逻辑。如果还要区分管理员可编辑所有单元格、填表人只能编辑指定区域还可以通过切换保护状态实现管理员进入时关闭保护或另开一个不带保护的工作表填表人进入时开启保护。注意保护状态要在服务端保存最好持久化到数据库字段里否则刷新页面就失效了。比如在 React 中你可以用一个isAdmin变量来控制if (isAdmin) { await commandService.executeCommand({ id: sheet.command.set-worksheet-protect, params: { workbookId: WORKBOOK_ID, worksheetId: WORKSHEET_ID, protection: null, // 示意关闭保护 }, }); } else { await commandService.executeCommand({ id: sheet.command.set-worksheet-protect, params: { workbookId: WORKBOOK_ID, worksheetId: WORKSHEET_ID, protection: { selectLockedCells: true, selectUnlockedCells: true, }, }, }); }这样权限模型就非常清晰了后端决定你这个角色能看什么、能填什么前端只负责把策略翻译成 Univer 的解锁和保护命令。5. 实测中的坑与经验速查5.1 高频问题排查表现象可能原因处理建议设置lock: false后仍然不能编辑工作表保护没有开启或保护开启时间早于解锁命令先解锁区域再开启保护命令执行顺序不要反开启保护后整个表都无法输入没有先设置任何区域为lock: false至少把开放填写的区域在开启保护前设为未锁定粘贴、拖拽填充仍能修改锁定区域粘贴和拖拽走的命令与直接输入不同部分版本对保护检查不完全覆盖同时在命令层监听相关命令命中锁定区域时阻止执行样式覆盖导致之前设置的格式丢失set-range-style传了整个 style 对象覆盖原样式只传需要变更的字段或先读取当前样式再合并多实例时编辑器错乱创建多个 Univer 实例未指定容器初始化时显式指定容器销毁时调用dispose()初始化数据量大时页面卡顿一次性写入大量 cellData 导致渲染压力大分批写入或使用批量命令合并执行5.2 几个值得记录的细节第一保护工作表在某些版本中仅拦截修改值这件事不会拦截格式调整。也就是说用户可能仍然可以拖动行高列宽或者给自己能编辑的单元格加颜色。如果你的业务要求版面完全固定要额外监听set-row-height、set-cols-width这类命令按需拦截。第二锁定的单元格只是不能编辑内容但默认是可以选中的。如果你希望用户根本看不到选区需要修改保护参数里的selectLockedCells。我建议根据实际体验决定多数数据收集场景保留选中是更好的因为用户能看清有哪些格子不能填如果担心误操作再把选中权限关掉。第三Univer 的命令名在不同版本之间发生过变化。我刚上手 0.x 早期版本时照着旧文档写完的代码在升级后 command id 对不上运行没报错但就是不生效。我的习惯是在每个项目里先做一次console.log观察命令执行记录开了保护后手动点击一个锁定单元格验证而不是只看控制台有没有异常。这套先跑通最小命令再叠加业务逻辑的方法帮我避免了很多文档滞后带来的麻烦。第四如果表格要交给填表人使用建议在初始化模板时就写好所有表头和提示信息不要等前端渲染完再一条条命令插入。因为模板写入属于建表阶段做完后再开启保护整个交互流程会简单很多。5.3 命令层拦截作为兜底如果你想绝对确保任何入口都改不了锁定区域还可以在命令执行前做统一拦截。逻辑大概是监听命令执行事件如果命令是修改单元格数据就遍历要修改的 range判断是否包含锁定单元格包含则取消命令。// 示意图在命令执行前拦截 univer.getCommandService().beforeCommandExecute$.subscribe((command) { if (command.id sheet.command.set-range-data) { const { ranges } command.params; if (rangesContainsLockedCell(ranges)) { command.preventDefault?.(); } } });注意事件订阅的具体写法和preventDefault的调用方式以你使用的 Univer 版本为准。这里要传达的思路是不要只依赖 UI 层的禁用状态命令层拦截才是权限控制的最终兜底。尤其当你接手的是一个老项目里面可能有很多历史操作按钮没有经过权限判断统一拦截会比逐个按钮排查安全得多。这套组合拳做下来univer 支持用户定义表格开放特定单元格其他区域锁定的需求就完整落地了。方案稳定之后我甚至把同一套逻辑封装成了一个独立的权限配置模块后端只需下发一份可编辑区域清单前端就自动完成解锁和保护操作后续其他项目接进来也就两三天的事。Univer 这个项目仍在快速迭代中我写这篇文章时使用的版本与最新版之间可能存在细微差异。如果你在实操中发现某个命令在官方文档里变了名字强烈建议顺手给项目提一个 issue 或 PR这本身就是开源社区互相成就的过程。对我个人而言真正把 Univer 玩顺的转折点就是不再把它当作一个表格组件而是当成一个有渲染外壳的数据模型。所有功能都围绕数据和命令展开问题就变得简单了。
返回列表