Univer在线表格:用Canvas渲染与权限控制实现表格型表单

发布时间:2026/11/2 13:58:11
Univer在线表格:用Canvas渲染与权限控制实现表格型表单 做内部系统时间久了你会发现一个问题需求方几乎都把 Excel 当成默认的交互原型尤其是做预算填报、项目评审、数据补录这类页面他们开口就是要“像表格一样操作”。以前我只能用 table 或者简单的开源组件去凑数据一多界面就卡成 PPT稍微复杂的公式、合并单元格、条件格式全都得自己造轮子。后来我把 Univer 引入项目这个问题才算真正被解决掉。Univer 是目前开源社区里相当能打的在线表格框架关注度和更新速度都很快整体采用 Canvas 渲染底层是一套完整的工作簿、工作表、单元格数据模型支持公式、协同、多文档类型并且能很方便地嵌入你自己的 Vue、React 或者原生 TS 项目。很多人第一次接触它是被“网页 Excel”这个标签吸引但真正让它不可替代的场景绕不开一个高频需求做一张“用户填写表格”开放特定单元格让用户录入其他区域锁定不可修改。这篇文章我就从框架定位、核心原理讲到怎么落地这一场景如果你正打算选型或者已经被权限控制卡住都可以对照着看。1. Univer 是什么凭什么撑起一个在线表格项目1.1 它解决的不只是一个“表格组件”问题先说一个很直观的对比如果你只是要做一张几十行的报表展示用普通 UI 组件库的 Table 就够了但如果用户需要在浏览器里拖拽、缩放、填公式、设置格式、合并单元格、加筛选甚至同时多个人在上面编辑DOM 表格方案基本走不通因为每个单元格都要有独立 DOM 节点数据量一大浏览器内存和事件绑定数量都会爆炸。Univer 的核心思路是用 Canvas 画出一整块画布屏幕上只绘制当前可视区域这样单元格再多实际参与渲染的节点也只是视野里那几百个。这个定位让它和常见的表格组件不在一个维度上。Univer 把自己定义成一套“包含渲染、数据、交互、命令、协同的表格引擎”而不是单纯 UI 组件。它把能力拆分成很多个插件包比如univerjs/core提供数据模型和命令总线univerjs/sheets提供公式、数据操作等能力univerjs/sheets-ui负责交互界面。这种分层有一个很实际的好处你可以在自己的系统里只注册需要的插件减小最终打包体积也可以在官方没覆盖的场景上叠加自定义插件而不是被关在一个黑盒里。另外一个容易被忽略的价值是Univer 本身就是面向私有化、可嵌入场景设计的。很多公司不想把表格数据放到第三方在线文档平台里又希望员工体验和在线表格一致Univer 恰好给了这条路数据留在自己服务器界面交互自己定制部署和升级完全可控。它天然就是要被嵌进业务系统的不是让你去注册一个账号再上传文件的工具。1.2 Canvas 渲染与插件化架构到底强在哪理解 Univer 的技术优势可以从我们做项目时经常遇到的两个痛点出发。第一个痛点是性能。纯 DOM 表格组件在数据量到达几千行之后滚动和点击事件会明显发虚更不用说涉及合并单元格、批量填充、公式联动的时候。Univer 的 Canvas 渲染相当于把所有单元格画在同一张画布上配合虚拟滚动无论底层数据是十万行还是二十万行真实参与绘制的都是可视范围内的单元格。这个思路和地图类应用有点像地图不会一次性把全世界都画出来而是只画当前视口里的部分滑动时再补齐。第二个痛点是扩展性。一个在线表格项目越往后越会发现不可能只靠官方功能覆盖所有业务。比如你需要自定义右键菜单、需要在某个操作前弹审批框、需要把外部系统数据实时写入表格这时候最怕的是框架不开放。Univer 的命令系统把每一次修改都抽象成命令从数据层到 UI 层有一套完整的服务总线你可以在命令执行前做拦截在命令执行后做监听甚至注入自己的命令进去。这意味着权限控制、审计日志、数据同步这些企业级需求都有地方安放。插件化的另一个好处是模块解耦。Univer 同时支持表格、文档、幻灯片三类文档但你在做表格的时候完全不用关心文档和幻灯片那部分代码。甚至表格内部也可以继续拆不用公式就不注册公式插件打包体积能少一截。这对国内很多需要控制前端资源加载量的团队来说是很实际的加分项。1.3 主流开源表格方案横评Univer 的优势与代价接触过表格选型的人大概率都会把几个方案摆在一起对比。我按自己的实际感受整理了一张表方案开源情况渲染方式在线编辑交互适合场景Univer开源Canvas强企业私有化在线表格、嵌入式填报/展示Handsontable商业授权为主DOM中等中小数据量的表格编辑需注意授权Luckysheet开源但维护节奏偏慢Canvas/DOM 混合中等老项目维护、学习参考SheetJS CE开源无 UI无数据解析、导入导出不适合直接交互Univer 相对最明显的特点就是“既要又要”既要开源的自由度又要接近商业表格控件的交互和性能。当然它也有代价其中之一就是版本迭代太快后面我会专门讲这个坑。另一个不得不说的事实是Univer 在中文社区里的文档和示例都算友好出现问题时能搜到的讨论也越来越多这比很多国内开源项目“只发文章不维护 issue”的情况要靠谱得多。选型在线表格这种重型组件社区是否持续维护重要性可能比某一个具体功能还高。2. “只让填部分单元格”的核心拆解Univer 的权限与控制逻辑2.1 先拆需求这是一张“表格型表单”把“只让用户填某些单元格”这个需求拆开本质就是两件事定义哪些区域可以编辑以及把其余区域变成不可写状态。Excel 老玩家都知道这就是“锁定单元格 保护工作表”的用法先选中所有单元格设置锁定再选中允许填写的区域取消锁定最后打开工作簿保护。Web 表格里的思路类似只是实现上还要考虑命令、事件、权限、提示反馈等多个维度。举一个真实场景运维部门做服务器信息登记表表格第一行是标题第二行是填写说明接下来才是服务器名称、IP、负责人、使用部门等字段。用户要做的只是把 IP 和负责人填进去其他内容比如录入时间、审核状态、统计公式统统不许动。这时候如果你给用户一个完整 Excel他把公式删了、说明文本改了、格式弄乱了都在意料之中。所以“表格型表单”需要用表格的界面体验加上表单的规则约束。Univer 的底层工作簿数据模型里单元格本身是可以带锁定这类状态的命令系统也能在任何写入动作发生之前做拦截判断。理论上既能支持类似 Excel 的“保护工作表”逻辑也能支持更灵活的规则比如“只有有权限的人才可以改”“打开状态是终审时才允许提交”这些统一都可以落到区域权限上。2.2 方案一整体保护 白名单解锁如果 Univer 当前版本已经提供 Protection 相关 API优先用这个最省事。思路很简单先对整个工作表开启保护再用白名单的方式把允许编辑的范围放开。未配置的区域默认不可写用户点进去也不会有光标体验和 Excel 的保护工作表一致。我写一个思路示例接口名不同版本会有差异你实现时以自己锁定的版本官方文档为准// 伪代码示例接口名以你当前安装的 Univer 版本为准 const sheet univer.getActiveSheet(); // 1. 开启保护默认锁定全部单元格 sheet.protect({ name: 用户填报保护, ranges: [ { id: input-area, range: { startRow: 2, endRow: 12, startColumn: 1, endColumn: 4, }, editable: true, }, ], });这里有一个我实际踩过的坑range 坐标从 0 开始第一行是行号 0第一列是列号 0和 Excel 里“第 1 行第 1 列”的直觉不一样。我第一版配置想开放 A1:D10写成了 B1:E11测试的时候死活觉得区域不对后来才发现偏移了一格。另外保护开启之后受保护区域通常仍然可以选中、可以复制只是不能修改。如果你希望连选中和复制都不允许那就要在 UI 事件层或者工作表的选项配置里再处理一下。对大多数填报场景来说允许复制其实是有好处的用户可以把参考数据复制进来阅读只是不能改。2.3 方案二命令层拦截把编辑权完全握在自己手里如果你的业务比较特殊例如不同角色能编辑的区域不一样或者你需要在编辑前做一次审批判断光靠保护配置就不够灵活了。这时候可以退到命令层自己接管权限判断。Univer 的特点是几乎所有修改操作都会经过命令总线因此你可以挂一个拦截器检查这次要修改的范围是否在业务白名单内不在就拒绝执行。代码思路是这样的// 思路示例拦截表格单元格编辑命令 univer.getCommandService().onBeforeCommandExec((command) { // 这里只拦截写相关命令例如 setRangeValues、setRangeStyle 等 if (command.id ! sheet.feature.cell.set-range-values) { return true; } const ranges command.params.ranges; const allowed ranges.every((range) isInAllowedArea(range)); // 返回 false 时命令不会继续执行 return allowed; }); function isInAllowedArea(range) { // 这里从后端接口拉取当前用户的区域权限做相交判断 // 注意处理边界range 的 startRow/endRow 是否闭合 return assignmentAreaList.some((area) { return ( range.startRow area.startRow range.endRow area.endRow range.startColumn area.startColumn range.endColumn area.endColumn ); }); }这个方案有个必须注意的点不要只判断当前命令要改的单元格是否在白名单里还要考虑合并单元格。一个合并区域里任何一个子区域被修改都可能影响整块合并区域所以遇到合并单元格时建议把整个合并范围拉出来做校验。另一个体验问题也很重要当用户点了只读单元格不要无声无息。最好给出一个轻提示比如“当前单元格不允许编辑”否则用户会以为系统卡死了。你可以自己弹 Toast也可以在命令层拦截之后往事件总线上发一个业务事件再由 UI 层消费并提示。这种细节决定功能是“能用”还是“好用”。2.4 让表格更像表单校验、下拉、条件格式开放区域锁定以后剩下的体验问题就是把这张表做得更像表单。最常用的三个手段是数据校验、下拉选项、条件格式。比如填报单元格限定只能输入数字填了文字就标红某个字段必须从下拉列表里选不允许自己编必填项背景色统一标成浅黄等用户全部填完再检查一遍。这些能力在 Univer 的插件体系里大部分都有对应能力。即便某个能力当前版本还不支持你也可以在 Univer 之上包一层业务逻辑提交时统一校验。不管校验放哪一层我建议遵循一个原则表格是给用户一个熟悉的高效录入界面底层的校验和存储逻辑一定要业务系统自己兜住。提示前端锁定只解决体验问题真正防止恶意篡改数据必须在后端做同样的区域权限校验。千万别以为界面锁了就安全了。用户完全可以绕过页面构造请求把不该改的单元格也改掉。3. 实操用 Univer 搭一个可填写、其他区域锁死的表格3.1 初始化项目与安装依赖先说清楚Univer 的 API 版本迭代非常快社区现在上手的常用方式是用 preset 全家桶快速搭起来再做按需拆分。我这里以 Vite Vue 3 为例如果你是 React 或者原生 TS思路完全一样。npm create vitelatest univer-demo -- --template vue cd univer-demo npm install univerjs/preset-univer装好后把模板里的默认内容清掉在入口文件或者组件里引入样式。Univer 的 CSS 文件在不同版本路径略有变化比如早期版本是lib/styles.css新版可能直接并入 preset 包。最稳的方式是照着官网示例模板抄一遍跑通之后再调整版本。这里提醒一下不要用^之类的模糊版本号去装直接锁一个确定版本。原因后面“版本迭代”那一节会专门讲你先记住结论就行。3.2 创建 Univer 实例并注册表格插件初始化示例代码大致是这样的import { LocaleType } from univerjs/core; import { Univer, UniverSheetsCore, UniverSheetsFormula, UniverSheetsUI, } from univerjs/preset-univer; import univerjs/preset-univer/lib/styles.css; const univer new Univer({ locale: LocaleType.ZH_CN, }); univer.registerPlugin(UniverSheetsCore); univer.registerPlugin(UniverSheetsFormula); univer.registerPlugin(UniverSheetsUI);这几个插件分别负责什么值得理解一下UniverSheetsCore相当于表格的引擎核心负责数据模型和基础操作UniverSheetsFormula是公式计算能力如果不需要公式可以先不注册包体积会小很多UniverSheetsUI是交互层负责画布渲染、选区、菜单这些看得见摸得着的东西。创建完实例之后还需要在当前页面容器上挂载一个 sheet 单位。不同版本挂载方式略有不同常见写法类似这样// 伪代码不同版本的创建单位/挂载方法不同这里展示常见写法 const sheet univer.createUnit(UniverInstanceType.SHEET, { name: 用户填报表格, sheetOrder: [sheet1], sheets: [ { id: sheet1, name: 填报页, rowCount: 500, columnCount: 20, cellData: {}, }, ], });如果上面这段 API 和你实际安装的版本对不上直接看node_modules/univerjs/preset-univer里的类型定义或者去官网 Playground 对照一下。Univer 更新确实快但核心概念不会跑偏实例、插件、工作簿、工作表这四个层级抓住后面再看任何新版本代码都不会蒙圈。3.3 配置“可填区域”锁住其余单元格这里我建议把“保护配置”和“命令层拦截”叠在一起用。保护配置负责给用户一个正常的界面感受命令层拦截负责兜底防止某些异常路径绕过配置。const sheet univer.getActiveSheet(); // 1. 开启保护锁定全部单元格 sheet.protect({ name: default, }); // 2. 把你允许填写的区域从保护里排除 sheet.addProtectionRange({ id: fill-area, range: { startRow: 2, endRow: 12, startColumn: 1, endColumn: 4, }, permissions: { write: true, }, }); // 3. 命令层兜底拦截非白名单写入 univer.getCommandService().onBeforeCommandExec((command) { if (command.id sheet.feature.cell.set-range-values) { const ranges command.params.ranges; return ranges.every((range) isAllowedRange(range)); } return true; });第三步里的isAllowedRange要自己实现规则建议从后端接口拉而不是写死在前端因为业务上的权限往往和用户角色、提交时间、审批状态都有关系。比如一个区域在初审阶段只开放给填写人在终审阶段就改为只读这靠前端静态配置是撑不住的。配置完成后效果大概是用户能在指定区域里点进去输入内容表格其他部分即使双击或者直接粘贴也不会产生任何修改。有个容易被忽略的细节是粘贴操作同样会走setRangeValues命令所以这份拦截也会一并生效不会出现“用户一个粘贴把整张表毁了”的事故。3.4 读取填写结果落回业务系统用户填完数据最终要回到业务系统。实现并不复杂提交时遍历指定区域的单元格取值即可。function collectInputs(sheet, range) { const values sheet.getRangeValues({ startRow: range.startRow, endRow: range.endRow, startColumn: range.startColumn, endColumn: range.endColumn, }); return values.map((row) row.map((cell) cell?.v ?? ) ); }这里需要留意getRangeValues返回的 cell 对象在不同版本里字段名可能不同有的是v有的是value有的是rawValue以你锁定的版本类型定义为准。另外空单元格一般会返回 null 或 undefined转业务数据时要统一处理成空字符串否则 JSON 提交时可能出现 null 字段导致后端校验失败。如果要做自动保存而不是提交按钮触发可以在值变更命令执行成功的事件里加节流每改一批数据就保存一批。节流时间建议 500ms 到 1s 之间既能避免频繁请求也不会因为用户秒关页面丢太多数据。对于需要多人在线填写的场景还可以用 Univer 自带的协同能力不过表单类应用通常不建议开放多人同时改同一批单元格否则你还要处理字段级合并冲突复杂度会上一个台阶。4. 实战避坑性能、版本与常见问题4.1 大表性能从渲染到更新的经验Univer 性能在开源表里算不错但它不是魔法。如果你的工作表有几十万行数据初始化时把全部 cellData 都塞进去哪怕 Canvas 渲染不卡数据模型构建、公式计算、首次渲染也一样会有压力。我的建议是大批量数据用增量写入先建空表再按片区setRangeValues填充公式范围不要整列选择尽量限制到有数据的行。还有一个小坑在高 DPI 屏上Canvas 的 devicePixelRatio 适配如果不理想会出现字迹发虚。不同版本表现不一样遇到时先升级版本再排查不要一上来就改渲染层。还有一点是关于图片和富文本的。Canvas 表格里塞大量图片每次滚动重绘都很吃资源。如果业务上需要展示图片优先考虑在渲染层之外用浮层方案实现而不是把图片直接作为单元格内容塞进数据模型。这个取舍等你在生产环境被卡掉几帧之后就懂了。4.2 版本迭代别被升级折腾疯Univer 目前迭代非常快我遇到过两次升级版本后 API 大变的情况。比如插件注册方式从分散注册改成 preset 统一注册工作簿初始化参数也换过位置。对于已经在生产环境跑的项目我的建议是把版本号锁死用 package.json 里精确版本不要用^号乱跳升级前先看 changelog重点关注 breaking change升级后第一件事不是跑业务而是先跑一遍表格核心流程渲染、编辑、公式、导出。另外不要太依赖 Univer 内部没有文档的私有 API因为它的内部模块变化太频繁。能封装的尽量封装起来这样将来升级时只需要改适配层不用满项目替换调用点。4.3 常见问题速查表现象可能原因处理思路表格能看但点击单元格没有编辑光标保护已开启白名单没配好检查 protect 配置确认允许区域坐标和 editable 权限输入内容后刷新就消失只在界面层改了没有把数据同步到后端用 getRangeValues 抓取并提交保存滚动到下方数据明显卡顿一次性加载数据过多或者样式计算过重改为增量写入限制行列数关掉额外插件输入手机号/长数字后变成科学计数法列宽不够或单元格数字格式为常规设置列宽和数字格式为文本或指定格式公式显示结果不更新未注册 Formula 插件注册UniverSheetsFormula插件不同角色看到的可编辑区域不一致前端保护配置写死成了一份从后端拉取每个用户的权限规则再动态配置最后说点实在的。Univer 不是唯一选项但从我接过几个内部表格系统项目的经验看它确实是当前开源方案里综合体验比较好的一款。真正想把它用好不是把官方 demo 跑通就完而是要把它当成业务系统的一个核心模块来对待封装好版本、设计好数据出入口、把权限规则从后端拉下来再让用户在浏览器里像用 Excel 一样自然填写。这个方向带来的开发效率提升远比你在普通表格组件上修修补补来得值。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询