ARTICLE DETAIL

资讯详情

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

Univer实战:嵌入Web系统的在线表格与单元格级权限控制

Univer实战:嵌入Web系统的在线表格与单元格级权限控制 最近我在做内部数据收集系统需求一上来就卡了壳要把一张员工信息登记表嵌到 Web 应用里普通用户只能填写自己的那几个格子姓名、部门、手机号以外的区域统统不能改。我第一时间想到 univer——这个开源在线表格项目我在社区关注很久了正好趁这次需求把它从 demo 拉进了生产环境。Univer 给我的第一印象是不像传统开源表格库那样自嗨。它不只是一个能画单元格的组件而是一整套基于 TypeScript 的在线表格/文档/幻灯片引擎尤其适合需要嵌入业务系统、控制单元格编辑权限、做数据收集和汇总的场景。这篇文章我会从选型开始逐步拆解 Univer 的初始化、权限控制实现、验证链路和进阶玩法把我实际踩过的坑也一并交代。如果你正在犹豫到底该不该把 Univer 用到项目里或者已经决定用了但不知道单元格权限怎么做这篇应该能帮你省不少时间。1. 为什么是 Univer数据收集场景下的表格选型思考1.1 需求本质一张局部可编辑的数据收集表业务方给我的需求其实很朴素人事每周要收集一次员工的项目经历归档每人填一行但行里只有本周工作内容遇到的问题两列是本人能填的工号、姓名、部门、日期这些列由系统自动带出其他人不许改。这类需求一旦细化就会发现它并不是做一个表格那么简单而是单元格级权限控制 在线录入 数据汇总三件事的组合。用户不能看到完整表格后自由发挥也不能绕过前端直接改数据否则汇总出来的表就是一张废纸。我当时把需求拆成了几个核心点表格必须嵌入现有 Web 系统不能跳转到第三方在线文档。普通用户只能编辑指定单元格其余区域从交互层面就要锁死。管理员要能预览完整表格、修改表头或调整可填写范围。提交的数据要能被后端服务读取并做二次校验不能只靠前端展示。这些点单独拎出来都不难但合在一起手写一个看起来像表格的界面会非常痛苦尤其是在要处理公式、合并单元格、复制粘贴、键盘导航这些细节的时候。1.2 我认真对比过的几条技术路线先说说我淘汰掉的方案思路可能对你有参考价值。第一个自然是成熟的在线文档产品比如飞书表格、腾讯文档这些。它们开箱即用权限功能也确实细但问题在于数据沉淀在第三方平台接入自家业务系统要做一堆开放接口对接而且单元格级权限在部分产品里不一定能覆盖到每个格子。对 ToB 系统来说把核心数据放到别人的服务器上这件事本身就要走很长时间的合规评审。第二个是手写表格也就是用 HTML 表格加一堆输入框。这种做法前期很快但凡是做过的人都知道一旦用户开始提要能复制粘贴、要能自动求和、要能合并单元格、要能撤销,手写的表格就会膨胀成一个无法维护的怪物。第三个是开源表格库。我重点对比了 Handsontable、Luckysheet 和 Univer情况大致是这样的方案单元格级锁定公式能力维护活跃度接入成本授权风险Handsontable支持但偏商业版较弱一般中商业授权需购买Luckysheet有保护概念但接口偏旧较好社区版停滞明显中开源但迭代慢Univer有完整的 protection 语义较好高迭代快中Apache 2.0看完对比心里基本有数了。Luckysheet 我也实际跑过 demo功能确实全但社区版本给我的感觉是能跑但不太敢往生产上放尤其是我需要的权限保护相关接口文档和代码的匹配度一般。1.3 我决定用 Univer 的三个理由第一单元格级权限控制是真需求而 Univer 在引擎层面就有 protection 概念。它不是靠给输入框加 readonly 样式来糊弄而是通过保护工作表、锁定单元格的机制从交互层面阻止用户进入编辑状态。这一点非常重要因为看起来不能改和真的不能改是两码事。第二数据驱动的工作簿模型很契合业务系统。Univer 创建表格时直接传入一个包含行、列、单元格数据的 JSON 结构服务端返回什么前端就渲染什么。这让我可以把表格结构和业务数据完全解耦后端同学改配置就能调整表格不用前端发版。第三插件化和命令机制让二次开发有清晰路径。我后面要做的开启保护解锁区域读取数据这些动作都可以通过命令或者直接操作快照来完成而不是去 hack 一个表格组件的内部 DOM。遇到问题还能顺着源码查社区活跃度也够。就这样我定了 Univer。2. Univer 的核心脉络从实例化到第一个可编辑表格2.1 安装和初始化最简链路Univer 的版本迭代比较快不同版本的 API 有差异。我用的版本是 0.6.x 系列的 preset-sheets 套件下面的代码逻辑在这个版本下是能直接跑的。如果你用的是更新的 1.x 版本建议以官方文档为准但整体思路是一致的。安装依赖很简单npm install univerjs/preset-sheets创建一个 Univer 实例并注册一个工作簿核心代码如下import { Univer } from univerjs/preset-sheets; import { UniverInstanceType } from univerjs/core; const univer new Univer({ locale: zhCN, }); univer.createUnit(UniverInstanceType.UNIVER_SHEET, { name: 员工信息收集, sheetOrder: [sheet1], sheets: [ { id: sheet1, name: 员工信息, rowCount: 100, columnCount: 10, cellData: { 0|0: { v: 姓名 }, 0|1: { v: 部门 }, 0|2: { v: 本周工作内容 }, 1|0: { v: 张三 }, 1|1: { v: 研发部 }, }, }, ], });注意看cellData的结构它是用行|列作为 keyvalue 是一个单元格对象其中v是显示值。这种键值对描述单元格的方式让我从服务端拼数据时非常舒服因为不需要预定义一个二维数组而是可以按需填充任意格子的内容。实例化之后Univer 会自动把表格挂载到页面容器里。容器需要一个固定高度否则表格高度会塌陷。实际项目中我通常会包一层 div 并设置height: 600px避免在弹窗或 Tab 切换时出现高度计算异常。2.2 Univer 的命令机制和插件机制刚开始接触 Univer 的人往往会被它的命令、Mutation、Operation、插件这些概念绕晕。我用一个生活化类比来解释命令就像你去餐厅点菜服务员命令服务把订单传给后厨后厨做完菜再让传菜员上桌Mutation整个过程都可以被记录下来随时可以回放或撤销。Univer 之所以把一次改动拆成多层是为了支撑两个关键能力协同编辑和 undo/redo。每一次对工作簿的修改无论是最初的创建、中间的数据填入还是后面的权限保护都是通过命令或 Mutation 完成的。这样在多人协作时每个人的操作都能变成有序事件流谁做了什么、改了什么一目了然。我在实际使用中并不需要深入实现一个自定义命令但必须理解一件事不要绕过 Univer 的 API 去直接改内部状态。比如有的人为了让某个单元格不可编辑会直接操作渲染层 DOM 加一层遮罩这在前端确实能达到视觉效果但一旦用户通过快捷键、粘贴、拖拽填充等方式操作遮罩根本拦不住。正确做法是走 Univer 提供的保护接口让引擎本身就不进入编辑流程。2.3 第一个可编辑表格上线之前要注意的细节preset-sheets 套件已经包含了完整的基础功能包括 UI、编辑、样式、公式、合并单元格等。因此你不需要像拼积木一样手动引入十几个插件只要按上面的方式创建实例就能得到一个功能完整的在线表格。但有几个点我在上线前踩过提前给你提个醒第一必须设置容器的宽高否则表格会产生诡异的渲染高度第二初始化数据里的单元格最好带上样式对象比如表头背景色、字体加粗因为用户第一眼看到的表格应该已经像样而不是白底黑字一片第三确定好 locale。Univer 的默认语言是英文我初始化时直接传了zhCN这样右键菜单、工具栏提示都是中文体验会好很多。3. 核心需求实现让用户只能填写指定单元格这一节是整个项目的核心也是你真正关心的部分。Univer 实现用户只能填写指定单元格的思路和 Excel 的工作表保护 单元格锁定非常像先把整张工作表保护起来默认所有单元格都不可编辑然后单独把允许用户填写的区域设为不锁定这样保护状态下只有这些格子能进入编辑。3.1 先从保护概念讲起单元格、工作表两层语义在 Univer 的模型里每个单元格对象可以带一个pprotection 缩写属性用来描述这个格子的保护状态。工作表本身也有一个保护状态。两者组合的规则是工作表未保护时不管单元格的p怎么设置所有格子都可以编辑。工作表保护开启后只有那些被显式标记为不锁定的单元格才能编辑。这个设计非常关键它不是设置某个格子 readonly而是反过来的默认全锁局部开放。我在做权限控制时最喜欢这种白名单模式因为漏配的风险更小——某个格子忘了解锁用户顶多是不能填要是哪天我忘了锁掉一个敏感区域数据就乱了。Univer 还允许在开启保护时设置密码用于后续解锁工作表。在业务系统里我一般不会用密码因为前端保护更多是防误操作真正的数据安全要靠服务端做这个后面细说。3.2 正向实现全表锁定再解禁可填区域在 0.6.x 版本里开启工作表保护和设置区域保护需要借助ICommandService来执行对应命令。完整实现代码如下import { ICommandService } from univerjs/core; import { SetWorkSheetProtectedMutation, SetRangeProtectedMutation, } from univerjs/sheets; async function enableFillMode(unitId, subUnitId, editableRanges) { const commandService univer.__getInjectedDependency(ICommandService); // 第一步开启工作表保护 await commandService.executeCommand(SetWorkSheetProtectedMutation.id, { unitId, subUnitId, isProtected: true, password: , }); // 第二步把允许填写的区域设置为“不锁定” await commandService.executeCommand(SetRangeProtectedMutation.id, { unitId, subUnitId, ranges: editableRanges.map((r) ({ startRow: r.startRow, endRow: r.endRow, startColumn: r.startColumn, endColumn: r.endColumn, })), isProtected: false, }); }这个editableRanges就是可编辑区域的数组。比如员工可以填写B2:E2这一行的空白格那 ranges 就是[{ startRow: 1, endRow: 1, startColumn: 1, endColumn: 4 }]。第一次跑这段代码时我还担心保护整个工作表 解禁部分区域会不会造成性能问题实测下来没有任何卡顿因为 Univer 对保护状态的管理是在数据模型层完成的不涉及大批量 DOM 重绘。3.3 配置驱动把可填写区域做成 JSON 配置上面代码里的editableRanges不应该是前端写死的否则每次调整可填写区域都要发一次版本。我把它做成了服务端下发的配置结构大概是这样的{ unitId: employee-survey-2024, sheetId: sheet1, editableRanges: [ { name: 工作内容, startRow: 1, endRow: 50, startColumn: 2, endColumn: 2 }, { name: 遇到的问题, startRow: 1, endRow: 50, startColumn: 3, endColumn: 3 } ] }前端拿到配置后把unitId、sheetId、editableRanges直接传给enableFillMode函数就能动态控制哪些格子可编辑。后端同事想开放新的填写列只需要在配置中心改一条记录前端完全不用动。这个设计还有一个额外的好处同一套配置可以用来做服务端的数据校验白名单。用户在哪些格子填了数据提交时后端会拿这份配置逐一比对凡是不在允许范围内的单元格数据一律拒绝入库。这对防止有人通过浏览器开发者工具篡改数据非常重要。3.4 设计一个编辑状态开关查询编辑模式/填表模式在真实业务里光有全表锁定还不够。人事要维护这张表得能编辑表头、调整列宽、修改校验规则这就需要在两种模式之间切换管理编辑模式工作表不启用保护管理员可以任意编辑。填表模式工作表启用保护普通用户只能填写白名单内的格子。我用一个布尔变量配合两段函数来控制async function setMode(isAdminMode) { const unitId univer.getCurrentUnitId(); const subUnitId sheet1; if (isAdminMode) { await commandService.executeCommand(SetWorkSheetProtectedMutation.id, { unitId, subUnitId, isProtected: false, password: , }); } else { await enableFillMode(unitId, subUnitId, currentEditableRanges); } }切换模式只涉及对保护状态的修改不会影响已有单元格数据因此往返切换很安全。我一直觉得状态开关 配置驱动这种组合是处理这类权限需求最直观的方案既好理解又不容易出 bug。4. 验证与演示一个真实的用户填写链路4.1 模拟一次完整操作流程代码写完只是第一步我更关心的是真实用户操作链路是否顺畅。我搭了一个模拟页面里面有几个核心步骤页面初始化时请求服务端拿到表格结构和可编辑范围配置。用 Univer 创建工作簿渲染出完整表格。自动切换到填表模式全表保护 白名单解锁。普通用户打开页面只能点击和编辑高亮的可编辑区域。提交时前端把cellData快照发送给服务端服务端根据同一份白名单配置校验数据。我现在把第 4 步的细节展开。普通用户看到表格后如果想去编辑一个被锁定的单元格鼠标点击上去只会出现选中状态不会出现编辑光标双击也不会进入编辑。这个交互是 Univer 引擎层面控制的不是我用 CSS 遮罩挡住的所以非常可靠。当用户点击到可编辑区域时输入流畅度和普通在线表格没有区别。有一次我拿这个 demo 给人事同事体验她在被锁定的单元格里试了半天以为系统卡了。我告诉她这些格子本来就不让你改她才恍然大悟。这说明从产品层面来说锁定区域的视觉提示还需要更明显一些。4.2 通过 API 验证单元格的保护状态光靠看起来不能编辑还不够我在自动化测试里还需要直接断言某个单元格的保护状态。Univer 提供了命令服务可以查询当前工作簿的信息。我当时用一个辅助函数来检查import { IUniverInstanceService, UniverInstanceType } from univerjs/core; function isCellLocked(unitId, subUnitId, row, col) { const instanceService univer.__getInjectedDependency(IUniverInstanceService); const workbook instanceService.getUnit(unitId, UniverInstanceType.UNIVER_SHEET); const worksheet workbook.getSheetBySheetId(subUnitId); const cell worksheet.getCellRaw(row, col); // 这里根据实际版本的 protection 字段做判断 return cell cell.p cell.p.locked true; }实际版本里 protection 相关字段的嵌套结构可能不太一样但思路是对的单元格对象会带一个有保护信息的字段判断它的锁定状态即可。我在做端到端测试时会遍历白名单内外的几个关键格子断言白名单内 locked 为 false白名单外的格子 locked 为 true跑通后基本就能安心。4.3 锁定单元格的用户体验增强交互层面的保护做到了接下来是体验优化。默认情况下被锁定的单元格和可编辑单元格长得一样用户根本分不清。我做了几件事来提升辨识度给可编辑区域设置浅色背景或者添加一个明显的边框标记。给锁定区域设置灰色背景但文本保持可读让用户知道这里有内容但不归我管。在页面顶部给出一段提示文案请在黄色区域填写其他区域不可编辑。样式怎么做呢我直接用 Univer 的单元格样式对象来设置背景色。function buildEditableStyle() { return { backgroundColor: #fffbe6, border: { top: { color: #faad14, style: thin }, left: { color: #faad14, style: thin }, bottom: { color: #faad14, style: thin }, right: { color: #faad14, style: thin }, }, }; }把可编辑区域的初始单元格对象加上这个样式视觉上就很清楚。另外我还做了一个改进在用户用 Tab 或方向键移动选中单元格时跳过锁定区域。这个功能 Univer 不一定自带我在键盘事件里做了过滤处理虽然代码不多但对录入效率的提升非常明显。用户填完一个格子按 Tab下一个可编辑单元格会直接获得焦点而不是被锁定区域截住。4.4 提交后的数据读取与服务端比对数据提交这块很多人会忽略但它恰恰是权限控制里最重要的一环。前端再怎么锁定单元格懂技术的人还是可以通过浏览器控制台直接修改 Univer 内部数据。因此我在提交时做了一层完整的服务端校验。前端的提交逻辑很简单读取整张表的cellDataPOST 给后端接口。const snapshot univer.getActiveSheet().getSnapshot(); // 这里把 snapshot.cellData 提交到后端后端拿到数据后会对照配置中心的下发配置逐格检查该单元格是否在允许编辑的白名单内。该单元格的数据格式是否符合要求。该单元格是否为自动计算或系统填入的字段比如工号、日期。凡是白名单外的单元格发生变化后端统一拒绝并返回一条错误信息。这个逻辑虽然多写几行代码但它才是这整个权限方案的真正底线。前端保护是方便用户后端校验才是保障数据。5. 进阶玩法多角色权限、按行动态解锁、服务端校验5.1 多角色权限不同用户看到并填写不同区域单一全表锁定 白名单解锁的模式很快就不够用了。在更复杂的业务里一张表可能同时被多种角色填写。比如一张研发项目周报开发人员只能填写工作内容列。组长可以填写工作内容和风险项两列。项目经理可以看到整表能编辑结论列。这个需求用 Univer 做起来并不算难。核心思路是前端根据当前登录用户的角色从服务端获取不同的 editableRanges 配置然后用这套配置去执行保护和解锁操作。前端代码没有太大变化差异只是配置来源async function initTableForUser(user) { const config await fetchEditableConfig(user.role); await createWorkbook(config.data); await enableFillMode(config.unitId, config.sheetId, config.editableRanges); }同样的表格结构、同一个页面入口不同用户打开后看到的可编辑区域完全不同。管理员可以在一个页面里预览所有角色的配置效果方便核对权限是否有遗漏。我在做这块时还发现一个细节白名单配置不要只传行列范围最好带一个用途标签比如财务填写HR 填写。这样将来排查权限问题时一眼就能看出某块区域是为哪个角色开放的不用拿着行列坐标去对数。5.2 按行动态解锁审批流场景还有一类场景是按行控制常见于审批表格、巡检记录。比如一张巡检表有 10 条记录每条记录由不同的巡检员填写用户只能填写属于自己的那一行。实现方式也很直接根据当前用户的数据权限动态计算出可编辑的行列范围。比如用户张三对应的是第 5 行到第 7 行那 editableRanges 就是[ { startRow: 5, endRow: 7, startColumn: 0, endColumn: 4 } ]然后照旧执行enableFillMode即可。这里有个实践建议不要把多个连续范围拆成多个命令一次次执行。我在早期版本里犯过这个错误循环调用SetRangeProtectedMutation每次都会触发一次保护状态的重算数据量大时页面会有明显卡顿。正确做法是把所有需要解锁的区域合并成一个数组一次性传给命令服务执行。如果确实遇到解锁 A 区域的同时锁定 B 区域的需求也尽量合并成一次保护设置来完成。5.3 权限的最终闸门是服务端前端权限方案做得再好也挡不住恶意用户。Univer 的数据模型在前端是完全暴露的我可以通过控制台直接读取或者修改数据甚至调用内部命令绕过 UI 界面。因此任何涉及生产数据的权限控制都必须把服务端校验作为最终闸门。那服务端到底要校验哪些东西我的经验是至少要覆盖三点第一校验谁可以编辑哪个单元格。这和前端白名单共用一套配置后端在做数据写入前遍历提交的每个单元格 key不在白名单内的直接拒绝。第二校验数据内容是否合法。比如某单元格要求填入数字类型的工时用户却填了加班两小时服务端要能根据配置里的类型定义做校验。第三校验提交的数据完整性。有些单元格是系统自动带入的比如工号、创建时间、登记人用户根本不需要填。前端会把这些格子锁定但服务端要反过来检查这些字段是否被篡改尤其要防止用户把系统自动填充的值改成其他内容。我的建议是前端权限做好用户体验后端权限做好数据安全两者各司其职缺一不可。6. 我在 Univer 实践里踩过的坑6.1 版本升级让 API 变了Univer 的迭代速度真的很快从 0.6 到 1.x一些命令的 ID、参数结构都有调整。我第一次踩坑是在项目进行到一半时想升级一下小版本结果发现SetWorkSheetProtectedMutation的导入路径变了参数也从isProtected变成了protect配置对象。从那以后我养成了一个习惯项目锁版本不跟风升级。Univer 这种还在快速演进的开源项目升级前一定要先跑一遍完整 demo把所有用到的 API 列个清单逐个验证。光看 release note 是不够的source break 往往藏在细节里。6.2 保护设置和视觉样式是两回事我最初只设置了保护没改任何样式。结果用户打开表格后看到了一个看似完全正常的表格点半天发现有些格子进不了编辑还以为是浏览器卡了。后来我在可编辑区域加了明显的边框和背景色再加上页面顶部的引导文案用户就再也没有疑惑了。这个经验给我一个启发权限的交互反馈要直接、明显。用户在可编辑区域输入应该感受到这是专为他开放的输入区锁定区域则应该用一种低调但清晰的视觉语言告诉他这里不需要你操作。不要指望用户会去猜测。6.3 单元格数据格式数字别当字符串写我在初始化 cellData 时一开始图省事把所有值都写成{ v: 123 }这样的字符串。结果测试统计功能时SUM 公式算出来永远是 0排查了半天才发现是因为单元格值类型不是数字。Univer 的单元格对象里其实有类型字段数值应该写成数字类型或者用t字段显式声明。我后来在构建后端返回的数据时做了类型转换确保数字字段是v: 123而不是v: 123。这个问题看起来很小但一旦上线会让所有统计类公式哑火。6.4 大数据量的渲染性能Univer 自带虚拟滚动几万行数据渲染起来没问题但我在做全表保护 区域解锁时发现一个性能瓶颈连续给多个区域设置保护状态如果区域数量过多前端会有明显的卡顿。应对方式前面提到过尽量合并 ranges。另一个做法是不要一次性加载整个超大数据表可以按需加载部分行或者在初始化时用一个较小的rowCount等用户滚动到底部再加载更多。这个思路对所有在线表格都适用Univer 也不例外。6.5 自定义工具栏按钮和提交逻辑用户填完表格后需要一个明确的提交动作来把数据发到后端。Univer 默认的工具栏没有这个按钮我通过它的扩展机制注册了一个自定义按钮放在工具栏右上角点击后触发数据提交流程。我在实际项目中的体会是与其让用户自己去点浏览器里的某个外部按钮不如把提交融入到表格界面里。用户在这个表格里完成了所有录入自然期望在这里结束整个流程。自定义按钮这个能力 Univer 是支持的文档里有扩展工具栏的示例照着做就行。个人经验小结Univer 解决了我最初那个局部可编辑数据收集表的难题而且比预期顺利。它真正打动我的点是权限控制不是靠前端 hack而是引擎层面原生支持数据模型是干净的 JSON和后端服务可以很好地配合自定义能力足够强能把它揉进复杂的业务系统里而不是反过来让业务迁就一个僵硬的表格组件。如果你只是需要一张静态展示表格完全没必要上 UniverHTML table 就够了。但如果你要做的是嵌入系统、带单元格权限控制、还要能处理公式和数据汇总的在线表格Univer 值得一试。最后分享一个我自己的小技巧遇到 Univer 的问题不要只搜文档直接去 GitHub 仓库看它自带的使用示例和源码往往比任何博客都更快让你找到答案。
返回列表