ARTICLE DETAIL

资讯详情

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

Univer实战:在线填报表格的锁定与权限控制

Univer实战:在线填报表格的锁定与权限控制 如果你稍微关注过开源表格类项目应该会注意到一个名字univer。在我第一次看到它的演示视频时第一反应是这不就是本地Excel被搬进了浏览器但真正深入用下来之后我发现它解决的问题远不止像Excel这么简单。尤其是热词里提到的场景——支持用户定义表格然后让用户去填写一些单元格其他的单元格用户无法修改——这其实就是在线填报、数据收集类系统最典型的痛点而univer把这块做了非常自然的支持。这篇文章我想从一个实际做项目的人的角度聊聊univer能做什么、它的底层是怎么想的、以及怎么把它变成一套真正可用的在线填写-锁定保护-数据回收工具。我不会只贴官方文档更多的是我自己在集成过程中踩过的坑和验证过的方案。1. 开源在线表格里的隐藏选手Univer能做什么先说结论Univer是一套基于TypeScript开发的、开源免费的在线文档解决方案除了电子表格之外它还有文档和幻灯片能力但最容易落地、社区讨论最多的还是它的Sheet电子表格模块。很多团队用它是为了替代在代码里拼表格HTML这种方案直接给用户一个能渲染、能编辑、能联动公式的表格界面。它的核心价值我总结成三个关键词可控、可嵌入、可扩展。可控指的是不用被特定的在线文档平台绑定。你可以把它集成到自己产品里指定哪些单元格可编辑、哪些必须只读甚至可以做到不同用户登录进来看到同一张表但编辑范围不同。热词里提到的让用户去填写一些单元格其他的单元格无法修改本质上是表单系统的需求传统做法是画一个表单页但表单页远不如表格直观——尤其当你要让用户一口气填几十行、多列数据时表格的体验优势很明显。可嵌入指的是它不挑前端框架。项目用React、Vue还是原生JS都能接。Univer的架构里核心逻辑和UI渲染层是分离的这让我可以在一个老旧的jQuery项目里也能嵌进去而不需要为了一个表格功能做技术栈迁移。可扩展指的是它有插件机制而且不是摆设。我后面会详细讲Univer允许你注册自定义命令、自定义函数、自定义菜单操作甚至能通过命名绑定去替换它内部的模块实现。这意味着它不只是现成的表格控件而是一个可以长在产品里的基础设施。如果只是在Gitee或GitHub上白嫖它的源码看热闹你会低估它真正把它当成一个可选组件去设计时你会发现市面上几乎没有第二个开源项目能同时满足界面接近Excel、可编辑范围可控、部署自由这三件事。2. 三个底层设计决定Univer的实际体验2.1 Canvas渲染为什么拖2000行不卡表格这种场景最怕的就是DOM节点爆炸。你用普通HTML表格渲染2000行×20列的单元格浏览器会开始卡顿滚动起来帧率下降明显一旦加上公式重算那基本就是灾难现场。Univer在这里走的是Canvas渲染路线。它的整体渲染层是基于Canvas的单元格的绘制、选区高亮、行列标题、网格线全部画在一块画布上。这个设计带来两个直接好处一是大数据量下滚动性能质变二是视觉表现极其统一它可以像游戏引擎那样自己控制重绘时机。实际体验下来我对比过同一个3000行数据在普通table方案和Univer中的滚动流畅度后者是明显平滑的。代价是——你没办法直接通过DOM去选中单元格里的文本因为根本没有DOM。所有交互都得走Univer的Command和Selection体系。刚开始做集成时我会下意识想能不能用document.querySelector去拿某个单元格的输入框后来发现整个思路都要转变。2.2 公式引擎可以独立使用的计算核心公式是表格的灵魂。Univer的公式能力不是简单调个eval它内部有一套独立的公式引擎支持跨工作表引用、命名范围、条件格式化里带公式、数组公式等。日常采购清单里的SUM、VLOOKUP、IF嵌套基本都没问题。这套公式引擎最让我意外的是可以被单独拿出来用。有段时间我想在服务端做一套模板校验逻辑——用户在客户端填写金额服务端要判断填的值是否满足单价×数量总价这种约束。我完全可以把Univer的公式计算逻辑编译到服务端执行同一个公式定义在前后端得到一致结果省掉了整整一套规则引擎的活。当然这里要提醒一句Univer公式引擎的兼容性和Excel比仍然有边界。超复杂的交叉引用、透视表推导、某些数组公式的边界情况可能处理得不如原版Excel那么精细。所以如果业务场景深度依赖Excel的高级公式上线前一定要做一轮兼容性摸底。2.3 协同机制CRDT与命令重放在线表格另一个绕不开的话题是多人协作。Univer的设计里协同不是靠谁后保存谁赢而是基于一套变更操作流——每次编辑会成为一个命令命令可以广播到其他端每个端按相同顺序重放命令最终保持一致的用户状态。这套机制让我在一开始使用时踩过一个很有意思的坑本地编辑其实也在走这套命令流。如果你直接修改某个内部数据结构而不派发CommandUI不会更新协同也不会同步。换句话说Univer里所有操作都必须走正规流程它不像一个DOM组件那样直接改属性就能解决问题。理解这一点之后你会发现它的架构相当统一命令、撤销栈、协同、权限控制全部建立在操作流基础上。这带来一个很好的工程特性——撤销是天然可靠的因为撤销本身就只是派发一个反向命令。相比自己维护表格状态的快照来做撤销要省心太多。3. 最关键的填报权限让部分单元格可编辑其他都锁定3.1 从工作表保护说起回到热词里最核心的那个场景用户填几个单元格其他区域完全动不了。Univer实现这个需求的方式一开始让我有点不习惯因为它没有像普通表单那样设置某个字段为只读的直观API而是通过工作表的保护机制来控制的。工作表的属性里有一个protection配置启用后整张表进入锁定状态未解锁的单元格无法编辑。然后你再针对特定Range设置unlock放行这些区域就可以被填写。这种先全锁再局部解锁的思路和Excel的保护工作表如出一辙但它被完整带到了Web端。我实际做的配置大体是这样创建一张空表先开启整表保护把需要用户填写的连续区域比如B2:F100标记为可编辑在工具栏层面禁掉insertRow、deleteRow、insertColumn等操作防止用户通过插入行列的方式绕开锁定再通过命令拦截或权限配置让普通用户即使调用了编辑命令也会在权限校验层被拒绝。如果只在UI层面控制右键菜单禁用实际上并不能彻底阻止调用命令。Univer的权限校验是可以被触发的所以最稳妥的方案是在CommandManager派发之前注册前置校验逻辑如果当前用户对此Range没有编辑权限就直接拦截命令。这样做之后即便有人用控制台手动调用API也改不了因为命令没有真正的执行权。3.2 动态权限后端决定谁能改哪一格静态锁定简单真正复杂的是不同用户看到同一张表可编辑范围不同。比如一张预算表部门经理可以编辑本部门的预算数字但不能碰其他部门财务总监全表可编辑。这种场景靠前端写死不可行因为权限判断必须跟登录用户走。Univer的方案是提供一系列PermissionPoint。每一个操作、每一类单元格改动都可以对应一个权限点。你可以注册自定义权限点或者通过拦截命令的方式在前端逻辑中动态判断当前操作是否被允许。我最终落地的方式是页面加载时根据当前登录用户调后端接口获取该用户在当前表格上的可编辑范围比如返回一个数组[{sheetId, startRow, endRow, startCol, endCol}]在Univer实例化之后遍历这些范围依次给Range设置unlock同时注册一个命令拦截器数据变化操作执行前判断目标区域是否包含在允许列表中不在就拒绝。这样一次性设置之后每个用户看到的是同一套模板填写的边界却很清晰。项目经理填进度财务填金额销售填预估大家互不干扰报表又汇总在一起。3.3 配合表单化的几点细节只有锁定还不够填报场景里还有几个细节容易被忽略。第一是提示文案。用户看到一片灰暗的锁定单元格第一反应是系统坏了而不是这不能填。我会在填报说明里明确告诉他哪些列需要填甚至用条件格式把可编辑区域标成浅黄色让哪里能填一目了然。第二是必填校验。Univer本身不做这个单元格不能不填的逻辑但你可以在提交时遍历可编辑区域逐格检查空值再通过UI提醒用户。如果你需要的是更严格的保存即校验可以让校验逻辑在CommandManager完成后触发但要做好性能控制不要每次敲一个字符就跑全量校验。第三是粘贴劫持。这是容易翻车的地方。用户可能在Excel里复制了一整块数据然后往锁定表格里粘贴——默认情况下Univer会尝试把粘贴内容映射到当前选区如果跨越了锁定区域就会产生一部分粘贴成功、一部分被拒的混乱状态。我给普通填报用户关闭了粘贴命令只保留手动输入这确实损失了一些便利但换来了数据安全。如果你是内部系统可以做成粘贴前先检查目标区域是否完全可编辑再做放行。4. 跑通一个最小可用填报系统4.1 前端工程与初始化这里我按照自己常用的前端工程方式给一个最小可跑的示例骨架。假设你已经有一个Vite项目安装所需依赖npm install univerjs/core univerjs/sheets univerjs/sheets-ui univerjs/ui univerjs/sheets-formula univerjs/sheets-numfmt初始化入口通常在main.ts里大致代码如下import { Univer } from univerjs/core; import { defaultTheme } from univerjs/design; import { UniverSheetsPlugin } from univerjs/sheets; import { UniverSheetsUIPlugin } from univerjs/sheets-ui; import { UniverUIPlugin } from univerjs/ui; import { UniverFormulaEnginePlugin } from univerjs/sheets-formula; import { UniverNumfmtPlugin } from univerjs/sheets-numfmt; const univer new Univer({ theme: defaultTheme, }); univer.registerPlugin(UniverSheetsPlugin); univer.registerPlugin(UniverFormulaEnginePlugin); univer.registerPlugin(UniverNumfmtPlugin); univer.registerPlugin(UniverUIPlugin, { container: app, layout: { toolbar: true, footer: true, }, }); univer.registerPlugin(UniverSheetsUIPlugin, { container: app, });这段代码看起来平平无奇但有一个容易被新手卡住的地方插件的注册顺序。UniverSheetsPlugin必须比UniverSheetsUIPlugin先注册因为UI插件依赖核心模块的已注册状态。我刚开始把顺序写反界面就是渲染不出来控制台报了一堆看不懂的依赖错误——其实本质是插件先跑核心模块还没有实例化。初始化之后得到的是一个空的Univer实例。下一步是创建工作簿并填充模板数据。4.2 锁定单元格、设置填报项在我使用的版本里创建工作簿并设置权限的路径大概是先在univer实例上获取univerAPI然后创建sheet并写入表头数据。示例逻辑如下const univerAPI univer.createAPI(); const workbook univerAPI.createWorkbook(); const worksheet workbook.getActiveSheet(); // 写表头 const headers [项目名称, 负责人, 开工日期, 计划产值, 实际产值]; headers.forEach((header, colIndex) { worksheet.getRange(0, colIndex).setValue(header); }); // 设置A列到E列第0行到第100行的范围为填报区 const fillRange worksheet.getRange(1, 0, 100, 5); fillRange.setUnlock(); // 允许编辑 // 其他范围保持锁定默认是锁定的这里需要解释一下setUnlock和setLocked的区别。官方语义中setUnlock是把某个Range的保护开关打开允许在sheet被保护时继续编辑setLocked则是重新锁定。默认情况下新单元格的锁定状态是继承工作表保护的所以你新建工作表后先启用保护再对指定Range执行setUnlock效果最清晰。光有锁定还不够我把工具栏里那些插入行、删除行、删除列、排序、筛选也一并从普通用户的UI上抹掉。如果只看锁定动作用户仍可能通过插入行在填报区下方插出一行不受保护的空白行。虽然权限拦截能防住底层调用但最好的策略是底层拦截UI简化双管齐下。4.3 从填表到收集数据数据回收让我考虑了很久。Univer提供了一套onChange监听机制但如果你每个单元格的变化都触发一次后端请求网络开销会非常大。填报场景最优解是本地积累、统一提交。我的做法是在提交按钮里遍历全部需要填报的Range一次性读取所有值然后组织成JSON或表格行数据打到后端。极端情况下上万行数据一次性提交会触发HTTP请求体积上限所以如果填写量真的很大建议按批次提交但普通填报表单控制在几百行内完全没问题。这里要提一个非常有用的APIworksheet.getRange().getValues()。它能按行列一次性把区域数据打成二维数组。相比逐个单元格取数性能提升是数量级的。而且Univer内部对此做了优化一次API调用能返回整个Range的矩阵。采集到数据后后端可以按下发的模板ID进行落库。如果需要生成审批流或Excel导出直接基于这份数据拼装即可不再依赖前端渲染。5. 服务端部署别被前端迷惑5.1 必需的基础组件很多人以为Univer是个纯前端库部署起来就是静态文件一放就行。这个认知对了一半——如果只是做单机Demo确实只需要静态前端但凡是多用户、要做协作、要能保存编辑历史就必须有服务端支撑。Univer的后端组件分为几种能力Univer Server端核心处理工作簿的创建、保存、读取操作提供REST API协同服务基于WebSocket实现多人实时同步如果用不到多人同时编辑一张表可以简化对象存储与数据库保存文档快照和操作日志Redis等中间件缓存热数据与协同状态。这不是一套轻量部署的组件。如果你只是要做填报场景后端不需要协同服务只需要模板下发-数据回收两条路径那完全可以自己写一个轻服务把Univer前端当成渲染引擎把最终数据落进自己的业务库。这样部署压力小很多。5.2 普通业务场景的部署经验如果确实需要服务端官方提供了Docker镜像我建议直接用编排工具走通官方示例然后再替换自己的业务逻辑。需要准备的容器大致有univer服务端容器一个PostgreSQL或MySQL一个Redis一个对象存储可以先用MinIO模拟后续再切换云厂商的S3服务。这里我踩过一个非常实际的坑前端通过WebSocket连不上服务端。排查一番后发现Univer前端的协同接入地址走的是内置配置如果你的服务端部署在/univer-server这种子路径下前端需要同步修改API基础路径和WebSocket路径两处。只改一个会出保存成功但同步无响应的诡异故障。实际上对于大部分业务系统我不会建议上完整的协同服务。原因是协同带来的复杂度远大于收益——普通填报/查询场景里真正需要多人同时并编辑同一份数据的情况很少。Univer前端本身已经是很好的表格体验层服务端我按业务自己设计反而更可控。5.3 一个真实的坑域名和跨域如果你把Univer嵌到一个子域名而这些接口又被部署在另一个子域名CORS配置是绕不开的。我遇到过前端页面打开正常但所有请求被浏览器拦截的问题——原因就是服务端没有正确配置Access-Control-Allow-Origin。Univer服务端的配置里通常有两种模式同域部署最简单推荐跨域部署需要配置白名单且WebSocket连接也需要处理跨域。我最终的建议是在公司的网关层直接把/univer-api这类路径反代到Univer服务端让前端以为它是同域服务所有跨域问题一次性解决。这比逐个接口配CORS省事也安全。6. 扩展能力当默认功能不够用的时候6.1 自定义操作和插件路由Univer的插件机制是它区别于普通Table组件的地方。你注册一个插件后可以向CommandManager注册自定义命令这些命令可以在撤销栈里生效。比如我给填报系统做了个提交当前区块按钮点击后走自定义命令校验当前区块数据合法性快照当前样式和值供撤销向后端提交数据UI上显示提交状态。这样做的直接好处是撤销还原也能覆盖这个动作不会出现填了数据提交了但CtrlZ只能撤销最后一次键盘输入的割裂感。6.2 给表格增加自定义函数业务系统里经常需要算一个业务专用指标比如项目进度百分比。与其在外部JS里计算好再塞回表格不如注册成Univer里一个函数让用户在单元格里直接写PROJECT_PROGRESS(B2, C2)。Univer支持自定义公式的注册走一遍它们的生命周期即可。这个能力在花名册、项目排期、成本核算之类的表格里很实用。它让表头的语义和单元格的计算都统一在表格内部业务人员可以自主调整计算逻辑而不需求固定的代码版本。当然函数注册需要公式引擎加载完毕后再进行不然会报Function not defined之类的问题。6.3 命名绑定与注入机制更深层的扩展是用命名绑定替换Univer内部的默认实现。这相当于Spring里的依赖注入你可以告诉Univer这个服务不用你的默认实现用我的。我在项目中用这个能力替换了默认的鉴权逻辑把用户信息和菜单权限做了一次统一适配。命名绑定是所有扩展机制里最重的一种优点是极强的可定制性缺点是要理解它的源码层级。如果你只是做表格填报建议先通过命令拦截和自定义函数满足需求命名绑定留到后期需要深度集成时再研究。7. 落地Univer的持续困扰与兜底方案7.1 渲染性能的极限虽然Canvas渲染让大数据量表的表现大幅改善但Univer并非无上限。当单元格数量到几十万、公式链很复杂时输入响应仍然会变慢。我的经验是真正到这种量级应该考虑聚合层处理让用户看到的是过滤后的结果而不是把所有明细一股脑丢到前端。滚动性能只是第一层重算性能和内存占用才是深层压力。遇到复杂嵌套公式时要多做压测尤其是那些跨工作表引用数组公式的组合计算成本可能在毫秒级和秒级之间剧烈波动。7.2 权限之外还需要兜底的行为控制权限拦截解决的是能不能改但拦不住改了之后提交脏数据。填报场景里我会再加两道兜底后端的业务校验不能完全信任前端Univer的前端模板规则要同步一份到后端操作日志必须有。一旦数据异常能从操作流中定位是谁、什么时间、从哪一秒改成了什么样。Univer保存操作流的能力在这方面是加分项因为单元格每一次改变都是可复盘的。它天然具备审计需要的原始轨迹。7.3 退出成本与控制成本说实话Univer目前的上手成本不算低。它的接口迭代速度快文档更新没完全同步社区里很多问题的答案还是旧版本的写法。集成时最好锁定一个稳定版本别追着最新版跑否则每次升级都可能有断崖式变更。我的做法是把Univer封装成项目内的一个适配层外部只暴露创建填报表格、锁定区域、导出数据等窄接口。这样即使底层升级业务代码也不需要大面积改动把控制成本控制在一个模块内部。从我自己的实操体验来看Univer最适合的场景始终是产品中需要一个像Excel一样的交互层但数据安全和权限边界你要自己掌控。它不是开箱即用的业务系统更接近一个高质量的通用组件。用好的关键是理解它的命令流、权限机制和插件模型并且从一开始就设计好哪些数据给用户写、哪些只能由后端写入的边界。找准这个边界你能省下大量的表格与表单开发工作量还不会失去系统自己的控制力。
返回列表