尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

Univer 表格引擎实战:单元格级权限控制与 Node.js 服务端计算

Univer 表格引擎实战:单元格级权限控制与 Node.js 服务端计算 1. 从一张“只能填指定格子”的表格说起第一次接触 Univer 是在一个内部数据填报系统里。业务方的需求很朴素给每个部门发一张表格他们只能填自己负责的那几列其他列要么是公式自动算出来的要么是别的部门填的要么是系统预置的谁都不能动。听起来简单但真做起来Excel 的权限控制粒度不够Google Sheets 的 API 又太重自己用 Canvas 从零画一个表格引擎那基本等于重新造轮子。Univer 就是在这个场景下进入视野的。它是一个开源的表格与文档协作引擎核心能力是把电子表格、文档、幻灯片这类“办公套件”的能力做成可嵌入的 SDK。你可以把它理解成一块乐高底板它提供了单元格渲染、公式计算、协同编辑、插件扩展这些基础能力你只需要在上面拼自己业务需要的模块。热搜词里出现的“univer 支持用户定义表格然后让用户去填写一些单元格其他的单元格用户无法修改”恰好就是它最典型的用法之一——通过插件和权限模型把一张通用表格变成受控的业务表单。这篇文章面向的是需要在前端或 Node.js 环境里嵌入表格能力的开发者尤其是那些被“在线表格权限控制”折磨过的人。我会从整体设计思路讲到具体实现包括怎么用插件架构做单元格锁定、Canvas 渲染层怎么工作、Node.js 侧怎么跑服务端计算以及我在实际项目里踩过的坑。如果你正在选型一个可嵌入的表格 SDK或者已经用了 Univer 但卡在权限控制上下面的内容应该能直接抄作业。2. Univer 的整体设计与插件架构拆解2.1 为什么是“插件架构”而不是“大单体”很多表格库的设计思路是提供一个庞大的 API 表面所有功能都挂在上面。Univer 走的是另一条路内核极小功能全部以插件形式注册。这个选择背后有很实际的考量。表格类产品的需求差异极大。有人只要一个只读的数据展示表格有人要完整的公式引擎有人要协同编辑有人要打印排版。如果内核把所有能力都塞进去包体积会失控而且你没法按需裁剪。Univer 的做法是内核只负责生命周期管理、插件注册、事件总线和依赖注入具体能力由univerjs/sheets、univerjs/formula、univerjs/sheets-formula这些独立包提供。这种设计带来的直接好处是当你要实现“部分单元格可编辑、其余锁定”时不需要去改内核而是写一个权限插件监听编辑相关的事件在合适的时机拦截操作。我在项目里就是这么做的一个SheetPermissionPlugin注册到 Univer 实例上它不关心表格怎么渲染只关心“这次编辑请求该不该放行”。2.2 核心模块的分层关系从依赖关系上看Univer 大致分成四层。最底层是univerjs/core提供Univer类、Plugin基类、ICommandService、IEventBus这些基础设施。往上一层是渲染与数据层包括univerjs/sheets表格数据模型、univerjs/sheets-uiCanvas 渲染与交互。再往上是能力层比如公式、条件格式、数据验证。最上层是业务插件也就是你自己写的那部分。理解这个分层很重要因为权限控制可以发生在不同层。最粗暴的做法是在 UI 层禁用编辑但用户还能通过 API 改数据更稳妥的做法是在命令层拦截因为 Univer 里所有对数据的修改最终都会走命令Command通道。我选择的是命令层拦截这样无论用户是点击单元格输入还是通过粘贴、拖拽填充都会被同一套规则管住。2.3 与 Node.js 的关系不只是前端 SDK热搜词里出现了 Node.js这不是偶然。Univer 的核心计算逻辑公式引擎、数据模型是平台无关的可以跑在 Node.js 里。这意味着你可以做服务端计算前端只负责渲染和交互真正的公式重算、数据校验、权限判定放在 Node.js 服务里。我在一个项目里就这么干过表格里有大量跨表引用的公式如果全放前端算低配机器会卡。于是把 Univer 的公式引擎跑在 Node.js 侧前端每次提交变更后服务端重算受影响的范围再把结果推回前端。这样前端只做展示计算压力转移到了服务端。当然这要求你对 Univer 的数据模型有足够理解知道哪些变更会影响哪些单元格。3. 单元格级权限控制的核心实现3.1 权限模型的设计从“区域”到“规则”“用户只能填指定单元格”这句话翻译成技术语言就是需要一套单元格级的权限模型。最直接的做法是维护一个可编辑区域列表比如[{ startRow: 1, endRow: 10, startColumn: 2, endColumn: 2 }]表示第 2 列的第 1 到 10 行可编辑。但实际业务往往更复杂可能还要区分“可编辑但不可粘贴”“可编辑但值必须符合校验规则”“某些行根据状态动态锁定”。我的做法是定义一个权限规则对象包含range作用区域、editable是否可编辑、validators值校验器数组、condition动态条件函数。规则按优先级排序匹配时取第一个命中的规则。这样既能处理静态区域也能处理“当 A 列值为‘已提交’时B 列锁定”这类动态逻辑。const permissionRules [ { range: { startRow: 0, endRow: 99, startColumn: 2, endColumn: 2 }, editable: true, validators: [value value ! !isNaN(Number(value))], condition: (rowData) rowData.status ! submitted }, { range: { startRow: 0, endRow: 99, startColumn: 0, endColumn: 1 }, editable: false } ];3.2 命令拦截在数据变更前做判断Univer 里所有会改变表格状态的操作最终都会触发一个命令。比如用户输入内容会触发SetRangeValuesCommand粘贴会触发SetRangeValuesCommand的批量版本拖拽填充也有对应命令。权限插件的核心就是监听这些命令在beforeCommand阶段判断是否放行。具体实现上我注册了一个ICommandInterceptor在intercept方法里拿到命令参数解析出目标区域然后逐单元格检查权限规则。如果任何一个单元格不可编辑就抛出错误或静默拦截。这里有个细节Univer 的命令拦截支持异步所以你可以把权限判定做成异步的比如去服务端查一下当前用户的权限。但异步拦截会带来延迟用户体验上要权衡。class PermissionInterceptor { intercept(command, options) { if (command.id ! sheet.command.set-range-values) return true; const { range } command.params; const cells expandRange(range); for (const cell of cells) { if (!isCellEditable(cell.row, cell.column, this.currentUser)) { return false; // 拦截 } } return true; } }3.3 视觉反馈让用户知道哪里不能改光拦截还不够用户得知道哪些格子是锁定的。Univer 的 Canvas 渲染层提供了自定义绘制的能力你可以通过插件在渲染前修改单元格样式。我的做法是给不可编辑的单元格加一层浅灰色背景鼠标悬停时显示“此单元格由系统维护”的提示。这里要注意性能。如果每次渲染都去遍历权限规则表格大了会卡。我的优化是在表格初始化时把权限规则预计算成一个二维的布尔矩阵渲染时直接查矩阵。当规则动态变化时比如某行状态变了只重算受影响的行。这个优化把渲染帧率从 20fps 拉到了 55fps 以上。4. Canvas 渲染与交互的实操细节4.1 为什么表格引擎偏爱 CanvasUniver 的表格渲染用的是 Canvas不是 DOM。这个选择在表格场景下很合理一张表可能有几十万单元格如果用 DOM每个单元格一个元素浏览器直接崩。Canvas 把所有单元格画在一张画布上只维护一个 DOM 节点性能上限高得多。但 Canvas 的代价是你失去了 DOM 自带的事件系统和可访问性。点击一个单元格浏览器不会告诉你点的是哪个格子你得自己根据鼠标坐标反算行列号。Univer 在sheets-ui里封装了这套坐标换算逻辑但如果你要自定义交互还是得理解它的坐标系。4.2 坐标换算从鼠标位置到单元格Univer 的表格视图维护了行高、列宽的数组以及滚动偏移量。给定鼠标的clientX、clientY换算过程大致是先减去画布在页面中的偏移加上滚动偏移然后遍历行高数组做累加找到落在哪一行。列同理。这个遍历如果线性做列多了会慢。Univer 内部用了前缀和加二分查找把复杂度降到 O(log n)。我在自定义一个“点击表头排序”的功能时直接复用了它的IRenderManagerService拿到getCellAtPosition方法省了自己写换算。4.3 自定义单元格渲染以“锁定态”为例Univer 允许你注册自定义的单元格渲染器。渲染器是一个对象包含draw方法接收 Canvas 上下文和单元格信息。我给锁定单元格写了一个渲染器在默认绘制之后再叠一层半透明遮罩和一个小锁图标。class LockedCellRenderer { draw(ctx, cell, style, layout) { // 先调用默认渲染 defaultRenderer.draw(ctx, cell, style, layout); // 再叠加锁定视觉 ctx.fillStyle rgba(0, 0, 0, 0.05); ctx.fillRect(layout.x, layout.y, layout.width, layout.height); drawLockIcon(ctx, layout.x layout.width - 16, layout.y 4); } }这里有个坑Canvas 的绘制是逐帧的如果每帧都画锁图标图标多了会掉帧。我的做法是把锁图标预渲染到一个离屏 Canvas 上绘制时直接drawImage比每次画路径快很多。5. Node.js 侧的服务端计算与部署5.1 在 Node.js 里跑 Univer 公式引擎Univer 的公式引擎包univerjs/formula不依赖浏览器 API可以直接在 Node.js 里 import。我搭了一个简单的服务接收前端传来的表格快照和变更操作在服务端重建 Univer 实例应用变更触发重算然后把受影响的单元格值返回。const { Univer, FormulaPlugin } require(univerjs/core); const { SheetsPlugin } require(univerjs/sheets); async function recalculate(snapshot, changes) { const univer new Univer(); univer.registerPlugin(SheetsPlugin); univer.registerPlugin(FormulaPlugin); const workbook univer.createUniverSheet(snapshot); for (const change of changes) { await univer.getCommandService().executeCommand(change); } return extractChangedCells(workbook); }这个方案的关键是快照的序列化和反序列化要高效。Univer 的 workbook 可以直接toJSON但大表格的 JSON 可能几 MB每次请求都传不现实。我的优化是只传变更的增量服务端维护一份最新的快照定期持久化。5.2 Node.js 环境准备与常见安装问题热搜词里有“node.js安装教程”“centos 7.9 node.js安装部署”说明不少人在环境准备阶段就卡住了。Univer 的 SDK 对 Node.js 版本有要求建议用 18 LTS 以上。CentOS 7.9 自带的 Node.js 版本太老需要先升级。在 CentOS 7.9 上我一般用 NodeSource 的仓库装curl -fsSL https://rpm.nodesource.com/setup_18.x | bash - yum install -y nodejs node -v # 应该输出 v18.x如果公司网络受限也可以下载官方二进制包解压后配 PATH。装完后验证npm是否可用有时候node有了但npm没装上是因为安装方式不对。另外Univer 的一些包依赖canvas这个原生模块在 CentOS 上需要先装cairo-devel、pango-devel这些系统库否则npm install会编译失败。5.3 服务端权限校验不能只信前端前端拦截命令只是用户体验层面的控制真正的安全边界在服务端。我的做法是前端提交变更时服务端用同一套权限规则再校验一遍。如果前端被绕过比如用户直接调 API服务端会拒绝并返回错误。服务端的权限规则可以比前端更严格比如加上“只能修改自己部门的数据”这类需要查数据库的规则。这里要注意服务端校验会增加延迟所以我把规则做了缓存并且只对写操作校验读操作放行。6. 常见问题与排查技巧实录6.1 命令拦截不生效的几种原因最常见的问题是拦截器注册了但没生效。排查顺序是先确认插件是否在Univer实例创建后、表格加载前注册再确认命令 ID 是否写对Univer 的命令 ID 是字符串常量拼错一个字母就静默失效最后确认拦截器的优先级如果有多个拦截器可能被前面的放行了。我遇到过一次拦截器写的是sheet.command.set-range-values但实际命令 ID 是sheet.command.set-range-values-batch粘贴操作走的是后者所以粘贴能绕过锁定。解决办法是同时监听两个命令或者用通配符匹配。6.2 Canvas 渲染模糊与高分屏适配在 Retina 屏上Canvas 如果不做 devicePixelRatio 适配画出来是模糊的。Univer 内部处理了这个问题但如果你自定义渲染器时直接按 CSS 像素画还是会糊。正确做法是拿到 Canvas 的width、height后乘以window.devicePixelRatio设置实际像素再用ctx.scale(dpr, dpr)缩放坐标系。6.3 大表格下的性能优化清单问题现象可能原因解决方向滚动卡顿每帧重绘全部单元格开启虚拟滚动只绘可视区域输入延迟每次输入触发全表重算公式引擎开启增量计算内存暴涨快照未释放定期销毁不再使用的 workbook 实例权限判定慢每次遍历规则列表预计算权限矩阵按行缓存6.4 公式循环引用的处理Univer 的公式引擎默认会检测循环引用并报错但如果你的公式是跨表引用循环可能跨 workbook检测会失效。我的经验是在服务端重算时加一个依赖图记录每个单元格依赖哪些单元格重算前先做拓扑排序发现环就中断并标记错误。这个依赖图也可以用来做增量重算一举两得。7. 一些实操心得与后续扩展方向权限控制这块我踩过最大的坑是“以为前端拦截就够了”。早期版本只在前端拦结果有用户用浏览器控制台直接调 Univer 的 API 改数据绕过了所有限制。后来加了服务端校验才堵住。所以如果你要做严肃的业务系统服务端校验不是可选项是必选项。另一个心得是关于规则的设计。一开始我把权限规则写得很细每个单元格一条规则结果规则表比数据表还大。后来改成“区域条件”的模式规则数量降了两个数量级。规则引擎的设计要克制能用区域表达的就不要用单元格。Univer 的插件架构还留了很多扩展空间。比如你可以写一个审计插件记录每个单元格的修改历史或者写一个工作流插件当某列填完后自动锁定并通知下一环节。这些都不需要改内核注册一个插件监听命令事件就行。我最近在试的是把 Univer 和低代码表单结合让业务人员自己拖拽生成带权限的填报表底层还是这套命令拦截和 Canvas 渲染。等跑通了再另开一篇聊。
返回列表