ARTICLE DETAIL

资讯详情

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

Univer 表格受控填报实战:插件化架构与单元格权限控制

Univer 表格受控填报实战:插件化架构与单元格权限控制 1. 从一张“只能填指定格子”的表格说起第一次接触 Univer 是在一个内部数据填报系统的需求评审上。业务方的诉求很朴素给一张固定格式的表格模板让一线人员只填其中几列其余单元格锁死防止误改公式和表头。当时团队第一反应是用 Excel 模板加保护工作表但很快就撞墙了——Excel 文件在浏览器里没法直接嵌入用户还得下载、填写、再上传体验割裂得厉害。后来有人提了一句“要不看看 Univer”这才有了后面这一整套折腾。Univer 是一个开源的表格与文档协同引擎核心能力是把电子表格、文档、幻灯片这类办公套件的能力做成可嵌入的 SDK跑在浏览器里底层渲染靠 Canvas逻辑层跑在 Node.js 生态里整体是插件化架构。它解决的问题很明确你不需要从零实现一套表格内核也不用被某个商业在线表格服务绑死直接把它当成一个前端 SDK 集成进自己的系统就能拥有公式计算、单元格样式、选区、协同编辑这些能力。适合谁来参考前端工程师、全栈开发者、以及需要做数据填报、报表配置、在线文档类产品的团队。哪怕你只是想在页面里嵌一个能算公式的表格Univer 也是目前开源方案里比较能打的一个。这篇文章不打算复述官方文档而是围绕“用户只能填指定单元格”这个真实场景把 Univer 的架构、集成方式、权限控制的实现思路、以及我在实操中踩过的坑完整地摊开讲一遍。看完你应该能自己动手搭出一个可用的受控填报表单。2. Univer 到底是什么架构与核心概念拆解2.1 一句话定位与它解决的问题Univer 的官方定位是“可嵌入的办公套件 SDK”。拆开来看“可嵌入”意味着它不是给你一个成品网站而是给你一堆 npm 包你装进自己的项目里按需组合“办公套件”意味着它覆盖表格、文档、幻灯片三大块其中表格Univer Sheets是最成熟、用得最多的部分。它解决的核心痛点是在线表格这件事自己写太难用别人的又太被动。自己写难在哪公式引擎、选区模型、撤销重做、协同冲突处理每一项都是深坑。用别人的被动在哪数据在别人服务器上样式和交互改不动按量收费还贵。Univer 走的是中间路线——内核开源、可自托管、可深度定制。2.2 插件化架构为什么它敢让你随便改Univer 最值得说的设计就是插件化。整个引擎被拆成一个个插件比如公式插件、条件格式插件、协同插件、UI 插件每个插件负责一块能力通过依赖注入的方式挂载到核心容器上。这种设计带来的直接好处是你不需要的功能可以不装包体积可控你需要定制的功能可以自己写插件或者覆盖已有插件的行为。打个比方Univer 的核心像一块主板插件像各种扩展卡。主板只负责插槽和通信具体能力全靠卡。你要做受控填报本质上就是写一个“权限插件”在用户编辑单元格之前拦截一下判断这个格子允不允许改。这个思路后面会详细展开。2.3 渲染层为什么选 Canvas热词里反复出现 canvas 绘图、canvas 绘图引擎这不是偶然。Univer 的表格渲染没有用 DOM 表格而是用 Canvas 绘制。原因很实际一张十万行、上百列的表格如果用 DOM光是节点数量就能把浏览器拖垮滚动和选区都会卡。Canvas 把整张表画成一张位图只渲染可视区域性能上限高得多。代价是 Canvas 里没有 DOM 节点你没法用 CSS 选中某个单元格也没法直接给单元格绑事件。所有交互——点击、拖拽、输入——都要靠坐标换算和事件代理自己实现。Univer 已经把这层封装好了但理解这一点很重要因为后面做权限控制时你操作的是数据模型不是 DOM。2.4 Node.js 在其中的角色很多人看到 Node.js 会疑惑这不是前端表格吗跟 Node.js 有什么关系关系有两层。第一层是工程层面Univer 的包管理、构建、本地开发服务都跑在 Node.js 上你得先装 Node.js 才能把项目跑起来。第二层是服务端层面Univer 支持服务端渲染和协同官方提供了基于 Node.js 的服务端方案用来做文档持久化、多人协同的冲突合并。如果你要做的是纯前端单机版Node.js 只出现在开发环境如果要做协同Node.js 就是后端的一部分。3. 环境搭建从装 Node.js 到跑起第一个表格3.1 Node.js 安装与版本选择Univer 对 Node.js 版本有要求太老的版本会在装依赖时报错。我实测下来Node.js 18 以上比较稳热词里提到的 22.12 也没问题。安装方式看系统Windows 直接去官网下载安装包一路下一步macOS 用 Homebrew 一行命令搞定Linux 服务器上建议用 nvm 管理版本方便切换。装完怎么确认终端里敲node -v npm -v两条命令都能输出版本号就说明装好了。如果提示 command not found多半是环境变量没配好Windows 上重装一次勾选“Add to PATH”Linux 上检查 nvm 有没有正确 source。提示不要用系统自带的旧版 Node.js尤其是某些 Linux 发行版预装的版本往往低于 16装 Univer 依赖时会各种报错。用 nvm 装一个干净的 18 或 20 版本最省心。3.2 创建项目与安装 Univer 依赖我习惯用 Vite 起项目快。命令如下npm create vitelatest univer-demo -- --template react-ts cd univer-demo npm install然后装 Univer 的核心包。Univer 的包名以 univerjs 开头最小可用集合大概是这几个npm install univerjs/core univerjs/design univerjs/engine-formula univerjs/engine-render univerjs/sheets univerjs/sheets-ui univerjs/ui这里解释一下每个包的作用方便你按需裁剪。core是内核提供容器、插件机制、数据模型design是设计系统和基础组件engine-formula是公式引擎engine-render是 Canvas 渲染引擎sheets是表格的数据逻辑sheets-ui是表格的界面交互ui是通用 UI 框架。做受控填报这几个基本都要。3.3 初始化一个最小可运行的表格初始化代码不复杂核心是创建一个 Univer 实例注册插件然后挂载到 DOM 上。下面是我常用的最小模板import { Univer, LocaleType, merge } from univerjs/core; import { defaultTheme } from univerjs/design; import { UniverFormulaEnginePlugin } from univerjs/engine-formula; import { UniverRenderEnginePlugin } from univerjs/engine-render; import { UniverSheetsPlugin } from univerjs/sheets; import { UniverSheetsUIPlugin } from univerjs/sheets-ui; import { UniverUIPlugin } from univerjs/ui; const univer new Univer({ theme: defaultTheme, locale: LocaleType.ZH_CN, }); univer.registerPlugin(UniverRenderEnginePlugin); univer.registerPlugin(UniverFormulaEnginePlugin); univer.registerPlugin(UniverUIPlugin, { container: app, }); univer.registerPlugin(UniverSheetsPlugin); univer.registerPlugin(UniverSheetsUIPlugin); univer.createUnit(UniverInstanceType.UNIVER_SHEET, { id: report-sheet, name: 数据填报表, sheetOrder: [sheet-01], sheets: { sheet-01: { id: sheet-01, name: Sheet1, rowCount: 100, columnCount: 20, cellData: { 0: { 0: { v: 姓名 }, 1: { v: 部门 }, 2: { v: 本月工时 }, }, }, }, }, });跑起来之后页面上就会出现一张能编辑、能算公式的表格。到这一步基础环境就算通了。注意container参数对应的是页面里一个元素的 id别忘了在 HTML 里放一个div idapp/div否则表格没地方挂载控制台会报找不到容器。4. 核心需求实现让用户只能填指定单元格4.1 需求拆解什么叫“受控填报”回到最初的需求。业务方要的其实是这样一张表表头、公式列、汇总行都是固定的用户只能改中间几列的数据。这在 Excel 里叫“保护工作表 锁定单元格”在 Univer 里没有现成的开关得自己实现。拆解下来有三个层次第一层是视觉层让用户一眼看出哪些格子能填、哪些不能填通常用背景色区分。第二层是交互层用户点到锁定格子时要么光标进不去要么输入被拒绝。第三层是数据层就算用户通过粘贴、拖拽填充等方式绕过交互限制数据模型层也要兜底拦截。三层缺一不可。只做视觉层用户照样能改只做交互层粘贴能绕过只做数据层用户会一脸懵不知道为啥改不了。下面逐层说。4.2 视觉层用条件格式标记可编辑区域Univer 支持条件格式但更直接的做法是在初始化数据时给锁定单元格设置样式。比如给表头加灰色背景、加粗字体const lockedStyle { bg: { rgb: #f0f0f0 }, bl: 1, cl: { rgb: #333333 }, };然后在 cellData 里给对应单元格挂上s字段引用这个样式。可编辑区域则用白色背景形成对比。这一步纯属体验优化但很关键——用户看到灰底就知道不能动减少无效点击。4.3 交互层拦截编辑命令Univer 的所有用户操作最终都会变成命令Command走命令总线。编辑单元格对应的是SetRangeValuesCommand之类的命令。要拦截有两种思路。思路一是监听命令执行前的事件判断目标单元格是否在允许列表里不在就阻止。Univer 提供了命令执行的拦截机制可以注册一个 mutation 或 command 的监听器。思路二是直接禁用编辑相关的 UI 入口比如让双击单元格不进入编辑态。第一种更彻底第二种更简单。我实际用的是第一种核心逻辑是维护一个“可编辑区域”的坐标集合在命令执行前做判断const editableRanges [ { startRow: 1, endRow: 50, startColumn: 2, endColumn: 2 }, ]; function isEditable(row: number, col: number): boolean { return editableRanges.some( (r) row r.startRow row r.endRow col r.startColumn col r.endColumn ); }然后在命令拦截器里调用isEditable返回 false 就中断命令。这样无论是键盘输入、粘贴还是拖拽填充只要走命令总线都会被拦下来。4.4 数据层兜底校验与回滚交互层能拦住绝大多数操作但有个别场景会绕过比如程序化调用 API 直接改数据或者某些插件的内部 mutation。所以数据层还要有一道校验。做法是监听数据变更事件变更发生后检查被改的单元格是否合法不合法就回滚。Univer 的数据模型是可观察的可以订阅变更。回滚的实现稍微麻烦一点需要记录变更前的值。我的做法是在拦截器里提前快照发现非法变更就用快照覆盖回去。虽然有点笨但稳。实操心得三层拦截里交互层是主力数据层是保险。不要指望只做一层就万事大吉我见过用户用粘贴功能把整列数据覆盖掉的案例只做交互层根本拦不住。5. 插件架构实战写一个自己的权限插件5.1 为什么建议封装成插件上面的拦截逻辑如果直接写在业务代码里会跟 Univer 的初始化代码混在一起难维护也难复用。更好的做法是封装成一个 Univer 插件。插件的好处是生命周期由框架管理可以在 onStart 里注册拦截器在 onDispose 里清理可以配置化把可编辑区域作为插件参数传进去可以复用到其他项目。5.2 插件的基本结构一个 Univer 插件本质上是一个类继承自Plugin实现几个生命周期方法。骨架大概长这样import { Plugin, ICommandService, CommandType } from univerjs/core; export class EditableRangePlugin extends Plugin { static override type editable-range-plugin; constructor( private readonly config: { ranges: EditableRange[] }, ICommandService private readonly commandService: ICommandService ) { super(); } override onStarting(): void { this.commandService.interceptCommand({ getMutations: (command) { // 判断命令是否涉及非法单元格 // 返回需要拦截的 mutation id 列表 }, }); } }interceptCommand是 Univer 提供的命令拦截入口你可以在里面检查命令携带的参数决定放行还是拦截。具体拦截哪些命令取决于你要控制的范围。编辑类命令、粘贴类命令、填充类命令都要覆盖。5.3 注册插件与配置化插件写好后在初始化时注册univer.registerPlugin(EditableRangePlugin, { ranges: [ { startRow: 1, endRow: 50, startColumn: 2, endColumn: 2 }, { startRow: 1, endRow: 50, startColumn: 4, endColumn: 5 }, ], });这样可编辑区域就变成了配置项业务方改需求时不用动代码改配置就行。这一点在多个报表场景下特别有用——不同报表的可编辑列不一样共用一套插件传不同配置即可。5.4 插件之间的依赖与顺序Univer 的插件有依赖关系注册顺序有讲究。渲染引擎和 UI 框架要先注册表格逻辑和表格 UI 要在它们之后自定义插件要等依赖的插件都就绪后再注册。如果顺序错了插件初始化时会拿不到依赖的服务报注入失败。我踩过一次坑把自定义插件注册在 sheets 插件之前结果拦截器里拿不到表格数据模型排查了半天才发现是顺序问题。6. 常见问题与排查技巧实录6.1 表格不显示或白屏最常见的原因是容器没找到或者尺寸为零。检查三点容器 id 是否和配置一致容器元素是否有明确的宽高Univer 不会自动撑开一个零高度的 div初始化代码是否在 DOM 挂载之后执行React 里要放在 useEffect 里。6.2 公式不计算公式引擎插件没注册或者注册顺序不对。UniverFormulaEnginePlugin必须在表格插件之前注册。另外公式要以开头且单元格的数据结构里公式存在f字段而不是v字段写错了不会报错只是不计算。6.3 拦截器不生效先确认命令是否真的走了命令总线。有些操作是直接改数据模型的不经过命令。可以在拦截器里打日志看用户操作时有没有触发。如果没有说明这个操作绕过了命令层得换一种拦截方式比如监听数据变更。6.4 粘贴绕过限制这是最典型的绕过场景。用户复制一段数据粘贴到锁定区域如果只拦截了编辑命令粘贴命令没拦数据就进去了。解决办法是把粘贴、填充、清除内容这些批量操作命令都纳入拦截范围。下面这张表是我整理的常见绕过路径和对应拦截点绕过方式触发的命令类型拦截要点键盘直接输入编辑类命令检查目标单元格是否可编辑粘贴批量设值命令检查粘贴区域是否全部可编辑拖拽填充填充类命令检查源和目标区域清除内容清除类命令检查清除范围程序化 API 调用直接 mutation数据层兜底校验6.5 性能问题表格数据量大时拦截器里的判断逻辑如果写得低效会拖慢每一次操作。可编辑区域用数组存、每次遍历区域多了就慢。优化办法是把区域预处理成区间树或者按行索引的 Map查询从 O(n) 降到接近 O(1)。我实测过一百个可编辑区域的情况下优化前后单次操作耗时差了好几毫秒操作频繁时体感明显。6.6 版本升级导致的 API 变更Univer 还在快速迭代不同版本之间 API 有变动。我遇到过一次升级后interceptCommand的参数结构变了拦截器直接失效。建议锁定版本号升级前先看 changelog别盲目npm update。7. 一些延伸思考与个人体会Univer 这套东西上手门槛不算低但一旦理解了它的插件架构和命令总线很多需求都能找到优雅的解法。受控填报只是其中一个场景同样的思路可以扩展到单元格级别的权限控制、按角色显示不同列、审计日志记录谁改了哪个格子等等。我在实际项目里还做过一件事把可编辑区域和用户角色绑定不同角色看到不同的可编辑范围。实现上就是在插件配置里传入角色信息拦截时结合当前用户角色判断。这套逻辑跑下来很稳业务方也很满意因为再也不用为每个角色维护一份 Excel 模板了。最后分享一个小技巧调试拦截逻辑时把每次拦截的决策过程打成结构化日志包含命令类型、目标坐标、是否放行、命中的规则。出问题时翻日志一目了然比断点调试高效得多。这个习惯帮我省下了大量排查时间。
返回列表