ARTICLE DETAIL

资讯详情

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

Univer接入实战:在线表格白名单填写权限与只读保护实现

Univer接入实战:在线表格白名单填写权限与只读保护实现 如果你手上有个需求给用户开放一个在线表格让他们在指定格子里填数据其余区域只能看不能动。你可能下意识会想自己做一个“假表格”结果光选区、复制粘贴、撤销重做就够喝一壶。我当初也是这么被折磨过来的后来才转向开源表格内核把目光落在 Univer 上。Univer 是一个用 TypeScript 实现的开源在线表格方案界面接近 Excel又保留了很深的扩展点。用户定义表格模板、锁定不可编辑区域、只开放指定单元格供填写这类需求正好是它的主战场。这篇不打算把官方文档复述一遍而是想重点聊聊我为什么在众多表格库里选了 Univer、怎么把它接入项目、白名单式的单元格填写权限如何落地以及接入过程中踩到的几个坑。无论你是只想快速给业务挂一个在线表格还是正在纠结“模板表格 用户填写”怎么设计照着这个思路走至少能帮你少绕半天弯。1. 为什么我选Univer做“可填写在线表格”绕不开的选型账1.1 三种技术路线的成本对比接到这类需求团队通常会走三条路我逐一算过账。第一条路是自研表格。用 div 加 contenteditable 拼出一个假表格看起来简单做起来全是细节单元格选区怎么画鼠标框选怎么处理复制粘贴跨单元格的数据格式怎么统一撤销重做栈要怎么维护公式算了要不要自动联动这些功能加起来是一个完整的桌面级软件工作量。除非你团队有大把时间否则我不建议走这条。我见过不少项目在“假表格”上迭代了半年最后还是一堆交互 bug 没擦干净。第二条路是商业控件。像 SpreadJS、Handsontable 这类产品功能确实全公式、数据透视表、导出 Excel 样样齐。但授权费要算进成本闭源边界也意味着你后面想深度定制时只能按厂商给的接口来碰到冷门需求会非常被动。第三条路就是开源表格内核代表有 Luckysheet、x-spreadsheet 和 Univer。Luckysheet 早年社区活跃后续部分核心成员转向了 Univer 项目。Univer 继承了那一代开源表格的积累在架构上做了更彻底的重写把表格、文档、幻灯片做成了一套可插拔的引擎。对我这种想把“在线表格”嵌进自家业务系统的人而言Univer 是当前开源阵营里最值得认真评估的选项。三门路线的差异我用一张表总结过给团队参考方案上手成本权限/可编辑定制深度长期维护成本适合场景自研假表格看着低实际极高理论上无限实际做不动极高非核心场景的简单展示商业控件低中受厂商API限制授权费持续支出预算充足、需要全家桶功能Univer开源接入中高命令层和插件机制开放中需跟进版本迭代需要嵌入业务系统、定制交互1.2 Univer的定位不只是一个表格组件我最初以为 Univer 就是个“长得像 Excel 的组件”真正用起来才发现它的架构思路很不一样。它分了 Core 引擎和 Sheet/Docs/Slide 等模块命令机制和数据层是分离的。什么意思呢所有编辑动作在 Univer 里都是命令比如改一个单元格的值、插入一行、删除一列都会走统一的命令通道。这对“用户填写模板”这件事极其重要。因为你可以不用依赖某个具体 UI 控件是否暴露了 readonly 属性而是在命令分发这个更底层的位置做拦截和校验。用户不管是敲键盘、复制粘贴、拖拽填充最终都会落到命令层你只要在那一层把规则卡住就基本锁死了所有篡改路径。而 Canvas 渲染的表格界面也让大数据量下的滚动和交互比一堆 DOM 撑出来的表格稳定得多。还有一点是它开箱即用的公式、条件格式、冻结行列、筛选这些能力。填表模板里常见的“总价 单价 × 数量”“状态列红绿灯显示”之类的需求不用自己写代码去算、去渲染直接在模板里配好就行用户的填写体验会专业非常多。1.3 什么时候不该用Univer我不希望这篇文章变成“无脑推荐”。Univer 也有它不适合的场景。如果你的需求只是收集五六个字段比如姓名、手机号、公司名那老老实实用普通页面表单就够了杀鸡不用牛刀。如果业务要求非常复杂的打印排版、严格的纸张分页、精细到毫米级的版式控制在线表格引擎不是做这个的料该用真正的文档报表产品还得用。如果团队里完全没有前端资源后续也没人维护那引入一个持续迭代的框架级开源项目反而会成为负担。Univer 的灵活是建立在你要读得懂它的模块、命令、插件这套概念之上的。2. 从npm安装到页面渲染Univer的接入全流程2.1 版本与包管理是第一个大坑Univer 目前还是 0.x 版本模块拆分和 API 都处在快速演进期。今天网上搜到的教程可能三个月后就不适用了。我接入时遇到过两次糟糕经历第一次是照着老教程用了univerjs/sheets-ui的老初始化方式启动直接报模块找不到第二次是 npm 更新后小版本升了右键菜单状态异常。所以第一件事就是锁版本。你用 npm 也好、pnpm 也好在 package.json 里写成精确版本号不要写^0.x.x这种允许小版本浮动的写法。否则某天 CI 重新构建拉到新版本一份看起来没改动的代码就莫名其妙挂了。Univer 相关的核心包一般是这几个univerjs/core核心引擎必装、univerjs/preset-sheets官方准备的表格预设包能省一大堆模块配置、univerjs/sheets和univerjs/sheets-ui不同版本暴露方式有差异。如果你不想折腾模块组合直接用预设包是最快的起步方式。2.2 最小初始化代码与界面解读我用的 React 项目最小接入大概长这样import { useEffect, useRef } from react; import { Univer, LocaleType } from univerjs/core; import { presetSheet } from univerjs/preset-sheets; function SheetContainer() { const containerRef useRefHTMLDivElement(null); useEffect(() { const univer new Univer({ locale: LocaleType.ZH_CN, presets: [presetSheet()], }); // 这里可以继续创建 workbook、加载模板数据 // 后续所有操作都建议挂在 univer 实例上便于统一管理 return () { univer.dispose(); }; }, []); return div ref{containerRef} style{{ width: 100%, height: 640px }} /; } export default SheetContainer;注意两个点。第一useEffect里的清理逻辑别省。Univer 实例如果反复创建不销毁在热更新场景下会出现两个表格叠加的诡异渲染问题。第二容器必须给一个确定的高度height: 0或者父容器塌陷会让 Canvas 渲染出空白区域这是很多人接入后“明明初始化了却没看到界面”的常见原因。初始化之后一个完整的表格界面就出来了顶部工具栏、公式栏、行列标、底部 sheet 切换标签。这些默认样式已经足够应付内部工具场景后期再逐步换肤定制。2.3 加载模板数据JSON优先xlsx次之模板数据怎么进到表格里我建议优先用 Univer 自己的 workbook JSON 格式。流程是管理员在 Univer 里把模板做好包括列名、公式、默认值、合并单元格、单元格样式然后导出一份 JSON 存到后端。用户打开时后端把这份 JSON 返回前端直接重建同一个 workbook。用 JSON 而不是 xlsx 的好处有三点加载速度快不依赖文件解析库样式、公式、合并单元格、单元格锁定这类属性可以完整保留结构可控权限配置可以跟它放在一起下发。如果你手里只有 xlsx 模板可以先在 Univer 界面里打开一次再另存为 JSON 模板。代码上加载 JSON 大概是调用当前版本 workbook 的初始化或导入接口把 JSON 对象传入。不同版本 API 名字略有出入核心点是你得在创建 workbook 时就给到数据或者通过实例方法刷新整个 workbook而不是局部 setValue 一个个塞。3. 核心需求落地白名单单元格可写、其他区域只读的实现链路3.1 理解保护模型Excel思维迁移到Univer热搜里那句话——“支持用户定义表格让用户去填写一些单元格其他单元格用户无法修改”——要落地核心不是 UI 上禁用整个表格而是要做“部分区域可编辑”的权限控制。熟悉 Excel 的同学应该知道Excel 保护工作表的核心逻辑是先给默认单元格一个 locked锁定属性启用工作表保护后所有 locked 单元格都不能编辑。如果某个区域要允许别人填就提前把这个区域单元格的 locked 置为 false。Univer 沿用了同一套思路。你需要在模板 JSON 里把允许填写的单元格设置为不锁定其余保持锁定然后启用工作表保护。写法上单元格的样式对象里通常会有protection字段大概是这样{ s: { protection: { locked: false } } }如果你是从 Univer 界面手动做模板Excel 用户在“设置单元格格式 - 保护”里看到的那个复选框在 Univer 里就是单元格样式里的保护属性。管理员在编辑器里把可填写区域勾选为“不锁定”导出模板 JSON这个信息就带上了。拿到这份 JSON下一步就是在加载后用接口开启整表保护。之后用户打开能点的格子只有白名单区域其他单元格点上去是只读态输入、粘贴、拖拽填充都会无效。3.2 三道校验UI、命令层、服务端各管一段单元格保护这种模式很好用但它依赖模板文件本身设置正确。一旦模板是别人手工改的或者用户通过某种途径修改了内存中的 workbook保护可能被绕过。所以我在项目里上了三道校验。第一道是界面层。Univer 开启保护后用户点锁定区域工具栏里的编辑按钮、单元格输入框都会进入不可编辑状态体验上很直观用户一看就知道哪里能填、哪里不能动。第二道是命令层也就是这个方案真正的兜底。Univer 所有操作都是命令我可以在命令执行前钩住它。比如当命令类型属于单元格赋值、区域替换、行列插入这类写操作时先判断目标区域是否落在白名单里不在就阻止或提示。示意逻辑如下const univer getCurrentUniverInstance(); univer.getCommandService().beforeCommandExecuted((command) { if (isEditCommand(command)) { const range extractRangeFromCommand(command); if (!isInEditableWhiteList(range)) { // 直接阻止或者抛出提示 return false; } } return true; });不用纠结这段代码在你的版本里是否逐字可跑重点是这个思路拦截编辑命令而不是反复检查鼠标事件。因为键盘输入、快捷键粘贴、鼠标拖拽最终都会被命令层统一接管。第三道是服务端。前端所有权限都只是体验和保护真正的安全边界必须在后端。用户最终提交的数据到了接口后端仍然要按照白名单配置重新校验一遍比如用户提交了某个超出范围的字段直接丢弃或报错。3.3 白名单配置的数据结构与下发流程白名单本身应该是一份后端可理解、前端可执行的配置。我在项目里定义成了这个样子const editableWhiteList { sheetId: sheet-01, ranges: [ { name: supplierName, range: B2:D10 }, { name: quoteAmount, range: F2:F10 } ], protected: true };sheetId指定哪个工作表ranges记录可填写的区域和业务字段名protected表示是否启用整表保护。这套配置和模板 JSON 一起存到后端的模板表里。完整的流程是这样的管理员进入模板编辑模式做好表格样式和公式圈选允许用户填写的区域保存后生成模板 JSON 白名单配置存在后端。用户端打开填写页面时后端一次性下发“模板 JSON 白名单配置”前端初始化 workbook、打开保护、应用白名单。这样管理员不需要碰代码改一个区域重新发布即可。4. 数据回传与整体验证从配置到提交的完整闭环4.1 拿到用户填写的内容模板给用户填完了下一步是怎么把数据拿出来。Univer 页面里肯定不止有需要提交的数据还可能存在其他辅助列、公式列、隐藏列。直接遍历整个 sheet 不太合适更好的做法是只读取白名单区域的数据。大致的读取思路是通过当前激活的 workbook 找到 sheet再根据区域范围拿到二维数组。示意代码如下const workbook univer.getActiveWorkbook(); const sheet workbook?.getSheetById(sheet-01); const rangeData sheet?.getRange(B2:D10).getData();拿到rangeData之后按行转换成业务对象。比如 B 列是供应商名称C 列是联系人D 列是电话那就逐行映射成const rows rangeData.map((row) ({ supplierName: row[0], contact: row[1], phone: row[2], }));这里有一步容易被忽略单元格里拿到的值是原始值还是显示值公式计算的结果可能和界面显示不完全一致。如果你配置了金额格式、日期格式记得在读取时确定好你用的是数据层的真实值避免用户看到的是两位小数提交上去却是一长串浮点数。4.2 提交前校验与错误提示拿到数据之后不能在用户什么都不填的情况下就提交。我一般会自定义一套字段校验规则const rules [ { field: supplierName, required: true, message: 请填写供应商名称 }, { field: phone, required: true, validator: (v) /^1\d{10}$/.test(v), message: 请填写正确的手机号 }, { field: quoteAmount, required: true, validator: (v) Number(v) 0, message: 报价必须大于0 } ];校验不通过时我建议把错误提示直接定位到对应单元格而不是用一个弹窗列出所有问题。因为用户面对的是表格一个错误一个格他才知道去哪里改。具体落地可以在校验失败时通过 Univer 的选区接口把当前选区定位到出错单元格同时页面侧边提示错误原因和表单校验的交互逻辑很像。4.3 完整Demo供应商报价模板的全流程串联我用一个“供应商报价单”的模板把所有环节串一遍方便理解。管理员先定义模板A 列是物料名称B 列是历史参考价只读带底色C 列是本次报价可编辑白名单D 列是供货周期天数可编辑E 列是总价E 列的公式是C2 * D2自动计算。管理员圈选 C2:D10 作为白名单其余区域锁定保存发布。用户打开页面看到一份漂亮的报价单。B 列的历史参考价看得到但改不了C 列和 D 列可以正常输入。用户填完数量E 列总价立刻联动出来不需要额外写前端计算逻辑因为这是模板里预置的公式在工作。用户提交后前端读取 C2:D10 的数值做必填和合理性校验然后 POST 到后端接口。后端收到数据后拿着白名单配置重新校验一遍确认没有篡改字段再写入数据库。这套流程做出来后你会发现一个很爽的点业务方自己就能维护模板不用每次改格式都找开发。模板和权限配置都是数据开发和运维成本都会低很多。5. 接入Univer后踩过的坑与后续扩展空间5.1 坑一图省事给整个sheet开readOnly等于把表格废了我第一次做权限控制时第一反应是找一个readOnly属性直接把整个表格设为只读再想办法放开白名单。结果发现全局只读会连滚动选区、框选高亮、右键菜单这些交互全部卡住。表格变成了一张“能看的死图”用户操作体验极差。后来想明白一件事我们要锁的是“编辑能力”不是“交互能力”。用户需要能滚、能选、能看甚至最好能复制内容只是不能改数据。这就是前面说的保护模式的意义——它禁用的是写命令而不是整个表格的渲染和交互。5.2 坑二API版本漂移文档别随手抄Univer 官方文档的示例确实一直在更新但你在搜索引擎里找到的二手资料很可能已经过时。我遇到过addPlugin改成addModule、包名从univerjs/sheets拆分成多个细粒度包这类变化。应对方式很简单第一锁死版本第二以官方仓库 examples 目录作为第一手教程来源第三升级版本时老老实实读 release notes别直接跳大版本。另外遇到编译报错时多看一下报错信息里提到的模块名Univer 的模块化程度比较高缺哪个包通常都会在报错里直接告诉你。5.3 坑三前端做了权限服务端不做等于白做这个问题不是 Univer 特有的而是“前端权限”这个方案共同的坑。用户如果懂一点前端完全可以直接用控制台改掉内存里的数据再提交。所以服务端的白名单二次校验绝对不能省略。校验的时候不要信任前端传来的字段名而是由后端重新取模板配置再对提交数据做范围判断。5.4 还可以继续扩展的方向白名单填表只是第一层。用 Univer 的命令层和插件机制你还能继续扩展不少东西。单元格数据校验可以做。比如给 C 列设置下拉选项只能在“已通过、待审核、已拒绝”里选填表错误率能大幅下降。Univer 本身对数据验证有支持配置好之后用户直接在格子里看到下拉箭头体验接近原生 Excel。敏感数据的防泄露可以考虑加水印。Univer 页面是 Canvas 渲染可以在容器层叠加上 SVG 水印或者 Canvas 水印即使截图外发也能追溯到来源。我做过一版在容器上盖一层绝对定位的 pointer-events: none 水印图层成本不高效果却比较明显。多人协同填写也是官方路线图里的重点。如果业务需要多个用户同时填同一份模板的不同区域Univer 的协同能力是可以作为统一基座的。具体到应用层你仍然要用白名单机制区分“谁可以填哪块”权限模型不会变。最后给想上手的同学一个建议别一上来就对着 GitHub 仓库硬啃源码直接跑官方 demo。先把 Univer 在自己的项目里渲染出来然后用我上面说的“整表保护 白名单解锁 命令层校验”做一个最小闭环。这个闭环跑通之后你再去看它的模块设计和插件开发思路会清晰很多。这套组合我已经在实际项目里稳定跑了一段时间至少对我来说它把“让用户填指定的几个格子”这个看似简单的需求真正打磨成了可配置、可维护、可扩展的线上功能。
返回列表