ARTICLE DETAIL

资讯详情

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

Univer在线表格实践:用表格引擎实现可填区域与单元格锁定

Univer在线表格实践:用表格引擎实现可填区域与单元格锁定 1. Univer 到底是什么不只是又一个在线表格1.1 拆开代码层看Univer 更像一个表格引擎说实话Univer 第一次进入我的视野是在一份开源项目周报里。当时只觉得是一个“能在线显示 Excel 的 JavaScript 库”没有特别上心。直到后来做一个内部填报系统被表格控件折腾得够呛我才认真把它从仓库里拉下来跑了一遍才意识到它和市面上那些“表格组件”根本不是一回事。Univer 是一套基于 TypeScript 的跨端开源办公套件核心形态是可嵌入的在线表格引擎官方把它定位为“下一代跨端协同办公解决方案”。简单说它不是一个给终端用户直接使用的成品应用而是一个能把“类 Excel 的表格能力”嵌入到你自己业务系统里的基础设施。它支持工作表、文档、幻灯片三种文档类型但在国内被讨论最多的还是它做在线表格的部分——也就是大家常说的“univer在线表格能跑通什么样的业务场景”。把它看成“表格引擎”而不是“表格组件”是因为它的架构和普通 UI 组件有本质区别。Univer 的核心层不依赖浏览器 DOM可以在纯 Node 环境运行UI 层和核心逻辑层是分离的。所有针对表格的操作比如改单元格、合并区域、调整行高列宽都会被封装成一条一条的“命令”再交给核心层执行。正因为有这一层命令封装后面实现撤销重做、协同同步、权限拦截才成为可能——这一点在后续做单元格锁定时会非常重要。上手之后你会发现Univer 能做的远不止“展示一张表格”。公式计算、条件格式、数据校验、筛选排序、冻结窗格、合并单元格、跨表引用等能力都有而且工作簿、工作表、单元格这套结构基本对齐 Excel 心智模型。对于要自建“在线填报系统”“管理后台列表编辑器”或者“类 Excel 业务表”的团队来说它的可定制空间非常大。1.2 热词里的刚需场景一张“能填但锁不住”的表这次搜索里有一句很典型的话Univer 支持用户定义表格让用户填写一些单元格其他的单元格用户无法修改。这句话看起来很简单实际上是一个特别普遍、也特别容易做砸的需求。我见过太多团队在做这种“填写表”时第一反应就是遍历单元格在用户输入之前做一遍判断哪些行哪些列是可编辑的哪些必须只读。理想状态是“非白名单区域全部灰色锁定”实际做出来却经常是表头能被拖拽修改、合并单元格被误删、公式区域被用户一条粘贴覆盖掉甚至用户复制粘贴还会绕过校验。说白了一个在线表格如果只是 UI 上做只读限制那它从头到尾都是漏的。Univer 解决这个问题的思路是支持把“编辑内容”“编辑结构”“选择区域”这类权限拆开控制。你可以定义某些单元格区域受保护禁止别人改内容、改格式、改结构也可以反过来只开放一小块白名单区域给用户填写。再配合命令拦截层和服务端提交校验基本能做到“该锁的锁死该填的放开”。这也是 Univer 相比纯“表格展示组件”的核心优势它把权限控制能力做在了引擎这一层而不是靠前端一个个事件去补。1.3 选型对比为什么我最终没选 SpreadJS 和 Luckysheet在做在线表格选型时绕不开几个同行SpreadJS、Luckysheet、Handsontable、x-spreadsheet。我用自己的真实需求对照过一圈列在下面供参考。对比项UniverSpreadJSLuckysheetHandsontablex-spreadsheet开源协议开源商业授权开源开源/商业双轨开源Excel 还原度优秀极强中上一般偏数据表格简单协同能力底层支持 CRDT 协同商业方案社区协同不完整无内置协同无二次开发自由度高核心与 UI 分离受限低中低更新活跃度高商业维护基本停滞较高低集成成本中等中高低低低典型适合场景在线协同办公、嵌入式编辑、自主可控业务强 Excel 兼容、离线桌面端简单在线表格展示数据录入型页面原型验证你可能会问那直接用 SpreadJS 不是更省心吗如果预算充足、不希望自己深入改造商业方案确实值得考虑。但如果你要的是“能融入自己技术体系、能改权限逻辑、能自己控制协同方案”的底层引擎Univer 的开源属性和命令架构会更合适。Luckysheet 最大的问题就是社区更新慢很多当年的 bug 到现在还挂着不太敢用在长期维护的业务里。2. 最小落地把 Univer 塞进你的 Web 项目2.1 初始化工程与安装 Univer 相关依赖这里用一个 Vite TypeScript 项目做最小验证。Univer 对现代浏览器要求不高TypeScript 支持很友好做集成时类型提示能省掉很多猜字段的时间。npm create vitelatest univer-demo -- --template vanilla-ts cd univer-demo npm install接下来安装 Univer 相关依赖。Univer 现在采用多包架构核心包、工作表插件、UI 插件、渲染引擎是分开的官方把它们叫做“Univer 插件体系”。至少需要下面几个包才能跑起来一张在线表格。npm install univerjs/core univerjs/sheets univerjs/sheets-ui univerjs/ui univerjs/design univerjs/locale univerjs/engine-render这里有个值得注意的点Univer 的版本更新很快API 在不同大版本之间不保证完全兼容。我在实际项目中碰到过 0.x 时代的createUniver函数签名到了 2.x 变成了new Univer老文章里的代码直接拿过来大概率跑不通。所以安装完依赖后最好先确认一下package.json里锁住的版本号再到官方 GitHub 的 demo 目录里对照示例代码。2.2 创建 Univer 实例并渲染在线表格在页面里放一个容器Univer 会把整个表格渲染到这个 DOM 节点里。容器的高度必须显式设置否则表格会出现“高度为 0”的诡异现象这是新手最容易踩的坑。!DOCTYPE html html langzh-CN head meta charsetUTF-8 / meta nameviewport contentwidthdevice-width, initial-scale1.0 / titleUniver 在线表格示例/title style html, body, #univer-container { margin: 0; height: 100vh; } /style /head body div iduniver-container/div script typemodule src/src/main.ts/script /body /html然后在main.ts里初始化 Univer 实例import { LocaleType, Univer, defaultTheme } from univerjs/core; import { UniverRenderEnginePlugin } from univerjs/engine-render; import { UniverSheetPlugin } from univerjs/sheets; import { UniverSheetUIPlugin } from univerjs/sheets-ui; import { UniverUIPlugin } from univerjs/ui; import { zhCN } from univerjs/locale; // 创建 Univer 实例 const univer new Univer({ theme: defaultTheme, locale: LocaleType.ZH_CN, locales: [[zhCN]], }); // 注册渲染引擎插件 univer.registerPlugin(UniverRenderEnginePlugin); // 注册工作表插件 univer.registerPlugin(UniverSheetPlugin); // 注册 UI 插件指定容器 univer.registerPlugin(UniverUIPlugin, { container: univer-container, header: true, toolbar: true, footer: false, }); // 注册工作表 UI 插件 univer.registerPlugin(UniverSheetUIPlugin); // 获取 Univer API 入口 const univerAPI univer.__getAPI();这一步跑起来页面上应该就会出现一张带工具栏的空白在线表格。Univer 渲染出来的交互是完整的单元格点击、编辑、拖选、右键菜单这些基础能力都默认可用。有两点实践经验可以分享第一footer这个配置项在部分版本里控制的是底部 Sheet Tab 栏业务系统内嵌时通常建议开header关闭footer让页面更干净第二如果你发现工具栏和单元格都是英文检查locales参数是否传对了中文语言包常见原因是没有把zhCN放进locales数组。2.3 用 createUnit 定义可填写的表格初始数据Univer 中“创建工作簿”的概念在老版本里叫createUniverSheet新版本统一成createUnit。我们可以在初始化之后直接创建一份带有表头和数据格式的表格。univer.createUnit(UniverSheetPlugin, { name: 部门经费预算填报单, sheetData: { default: { rowCount: 60, columnCount: 10, cellData: { 0: { 0: { v: 项目名称, s: { bg: #f5f5f5, bl: 1 } }, 1: { v: 预算科目, s: { bg: #f5f5f5, bl: 1 } }, 2: { v: 预算金额, s: { bg: #f5f5f5, bl: 1 } }, 3: { v: 实际支出, s: { bg: #f5f5f5, bl: 1 } }, 4: { v: 备注, s: { bg: #f5f5f5, bl: 1 } }, }, 1: { 0: { v: }, 1: { v: }, 2: { v: }, 3: { v: }, 4: { v: }, }, }, }, }, });sheetData的结构和 Fax 引擎内部数据结构一致row和column用数字索引单元格对象{ v: 值, s: 样式 }表示内容与样式。你可以调用univerAPI.getActiveWorkbook()拿到当前工作簿再通过getActiveSheet()操作活动工作表。这一阶段不用急着做权限先把一张表格跑起来、数据能渲染、单元格能编辑就已经完成 80% 的集成工作了。3. 核心实现可填写区域 其余单元格锁定3.1 需求拆解权限控制只有“编辑内容”还不够我看到很多项目在实现“其他单元格用户无法修改”时只是把输入框里的内容disabled了这远远不够。用户依然可以复制粘贴、拖拽填充、插入行、删除列、改样式。所以做单元格锁定前要明确表格层级里有哪些操作会破坏数据修改单元格内容输入、粘贴、公式计算产生的新值修改单元格样式字体、颜色、背景修改区域结构插入行、删除列、合并单元格移动单元格内容拖拽填充、剪切粘贴Univer 的权限模型把这些操作拆成了不同的“权限点”可以针对某个工作簿、某张工作表、某个单元格区域分别设置。最常用的是对 Range 设置保护把“编辑内容”和“编辑结构”同时关闭让目标区域变成纯只读。再加上命令拦截层做兜底基本能覆盖上面所有破坏路径。3.2 用保护区 API 锁定禁止编辑的区域我按当前主推的 2.x API 风格写一个锁定示例。具体方法名和字段在不同版本可能有出入跑之前先到官方示例里对一下。const workbook univerAPI.getActiveWorkbook(); const sheet workbook?.getActiveSheet(); // 锁定第一行表头区域从第 0 行第 0 列开始共 1 行 5 列 if (sheet) { const headerRange sheet.getRange(0, 0, 1, 5); headerRange.setProtection({ editContent: false, editStructure: false, hintText: 该表头区域已锁定不可修改, }); }这个操作的意思是把第一行 A 到 E 列的表头区域设成受保护状态用户点击这个区域时无法进入编辑会显示提示文本。如果后续要放开某一块区域可以重新调用setProtection传入新的权限配置或者直接移除保护。要注意的是setProtection在某些版本里会要求传一个完整的保护配置对象缺字段可能直接抛错有些老版本则需要通过univerAPI.getCommandService().executeCommand()发送“添加保护区”命令来实现。不同版本差距较大使用时优先看官方示例里 “Protection” 的 demo。3.3 用命令拦截做最终兜底保护区 API 是在引擎层对用户操作做了拦截但在我看来真正要保证“绝对锁死”单靠保护区还不够。因为某些批量操作、粘贴行为、程序化调用会走不同路径可能在保护区判断之外执行。更稳妥的方式是在命令执行前统一拦截一遍判断当前命令是否涉及“写入操作”再结合业务白名单决定放行还是阻止。Univer 的onBeforeCommandExecute可以注册一个全局前置拦截器返回false表示阻止该命令执行。可以参考下面的代码const editableRanges: Array{ row: number; col: number } [ // 这里存一张“允许填写”的格子清单 // 例如第 2 行第 0 列、第 2 行第 1 列... ]; univerAPI.onBeforeCommandExecute((command: any) { const commandId command?.id || ; // 只有涉及到修改单元格值的命令才需要拦截 if (commandId.includes(set-range-values) || commandId.includes(paste)) { const params command.params || {}; const row params.range?.startRow; const col params.range?.startColumn; // 判断目标区域是否在可编辑白名单里 const isEditable editableRanges.some( (range) range.row row range.col col ); if (!isEditable) { return false; // 阻止命令执行 } } return true; });这个方案的好处是所有针对单元格值的修改操作都必须经过命令层即使某个具体编辑器组件忘记做 UI 判断这里也会兜住。但要注意一点——onBeforeCommandExecute拦截覆盖范围比较广调试时容易影响正常的工具栏操作建议只在“锁定场景”下挂载页面退出时及时移除监听。3.4 读取用户填写数据并回传业务后端锁定只解决了“不能改哪里”的问题还有另一半是“用户填了什么”。实际业务里通常两件事会一起做监听单元格变化以及提供“保存/提交”按钮主动收集数据。监听单元格变化官方提供事件订阅 APIimport { UniverInstanceType } from univerjs/core; univerAPI.getEventManager().on( UniverInstanceType.SHEET, univer:sheet:on-cell-change, (change: any, state: any) { console.log(用户修改了单元格, change.row, change.column, change.value); // 这里可以把变化实时同步到后端草稿接口 } );如果是批量提交我更推荐在“提交按钮”触发后直接读取整个工作表的有效数据一次性交给后端。这样后端拿到的是一份完整快照而不是一堆离散的变更事件。function collectFormData() { const workbook univerAPI.getActiveWorkbook(); const sheet workbook?.getActiveSheet(); if (!sheet) return; // 读取从第 0 行第 0 列开始到第 59 行第 4 列结束的数据 const values sheet.getRange(0, 0, 60, 5).getValues(); const payload values .filter((row) row.some((cell) cell?.v ! )) // 去掉空行 .map((row) row.map((cell) cell?.v ?? )); // 提交到业务后端 fetch(/api/fill/save, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify(payload), }); }这里有个小陷阱getValues()返回的单元格值类型取决于单元格设置可能是字符串、数字、对象甚至公式对象。提交前最好先做类型清洗避免把{ v: 123 }这种对象结构直接扔给后端。4. 在线协同与动态权限一张表多人同时填4.1 Univer 在线协同的基本思路热词里强调“univer在线”其实就是指它天然适合做成多人协同填写的在线表格。Univer 底层通过 CRDT 数据结构做协同常用的是 Yjs 这套方案。多个用户在各自客户端操作同一份文档操作产生的是增量命令通过协同协议分发给房间里的其他客户端再合并到本地状态。这和“CtrlC / CtrlV 到服务端再广播”的老式方案完全不一样。CRDT 的好处是不存在传统“最后写入覆盖”问题哪怕两个人同时编辑同一列相邻单元格最终也能收敛到一致状态。Univer 的命令系统本身很适合这种协同模型所有编辑都是命令命令可以在网络中传输不需要一帧一帧同步屏幕截图。如果你打算自建在线协同需要准备三个部分一是 Univer 客户端协同插件二是协同房间服务三是数据持久化。官方仓库里有协作示例可以参照collaboration相关 demo。4.2 与业务权限联动不同人打开同一张表看到的锁区不同单元格锁定和“在线协同”遇到一起时真正的难点不是技术而是业务策略。同一个填报单部门经理打开应该能改预算科目普通员工打开只能填实际支出这是很典型的动态权限需求。我的建议是前端权限只负责展示层和输入体验服务端必须再做一层数据提交校验。换句话说锁区的控制要让用户“根本进不去编辑”但这并不能替代后端的最终审核。因为用户可以直接打开浏览器调试工具绕过前端限制发送修改请求。一个成熟的填报表业务至少要保证后端有能力验证“提交的数据是否越过了允许范围”。动态权限在客户端可以做成一查表配置当前用户角色 → 可编辑单元格列表 → 页面加载时批量设置保护区。比如普通员工的可编辑范围只有 C、D 两列管理员的可编辑范围是全部区域这种配置放在数据库里最好页面初始化时再由前端拉取。4.3 协同填报表实时同步与提交校验的几个注意点多人同时填写一张表还有一个很容易被忽略的问题用户 A 填写的单元格可能和用户 B 提交的数据发生冲突。虽然 CRDT 能保证最终一致但业务上的“覆盖错误”并不会因为底层一致就自动消失。我在实际项目中加了这样几层防护单元格级监听只在可编辑区内监听变化把用户输入实时同步到草稿状态提交前校验遍历提交数据检查必填项、数字格式、金额上限服务端版本校验提交时带上表格版本号如果服务端发现版本已经变化要求用户刷新后重新确认。协同不是“把表格变成聊天室”而是把编辑体验平滑地交给多人。如果用户同时只有三五个人填报其实也不需要上全量实时协同定期拉数据、提交时合并也够用。技术选型上先想清楚业务规模再决定要不要上 Yjs 那套重量级方案。5. 实操中踩过的坑与排查经验5.1 版本差异是集成时最大的坑Univer 版本迭代速度非常快很多 API 在 2024 年内就发生了调整。我最初参考的博客代码还是createUniver()老写法当时直接用报错找不到函数。后来对照官方 example 目录才发现新版已经全面改成new Univer()registerPlugin()的写法。如果你发现复制过来的代码报错优先做两件事打开node_modules/univerjs/core/package.json确认实际安装版本到官方 GitHub 仓库的examples目录里找当前版本对应的初始化代码。还有一个常见问题Univer 不同插件的版本必须一致如果univerjs/core是 0.5.x而univerjs/sheets-ui是别的版本运行时会报各种奇怪的模块错误。建议安装时统一用npm install univerjs/xxxlatest并且一次性安装同一批避免新旧混用。5.2 容器、样式与高度问题表格渲染成“一条线”症状是页面里明明调用了插件但表格区域几乎看不见只有很窄的一条线或者完全空白。排查思路很简单Univer 需要容器具备确定的高度。如果父元素是height: auto子元素高度计算为 0渲染引擎会导致表格无法撑开。解决办法是在 CSS 里给容器设置固定高度比如height: 600px或height: calc(100vh - 50px)。在后台管理系统里如果容器在 Tab 页内还要注意 Tab 页刚渲染时容器是否对引擎可见不可见时可能需要手动触发一次 resize。样式污染是另一个隐性坑。Univer 内部样式会直接注入页面如果项目里同时使用 Tailwind 的全局 reset 或者其他 UI 库的全局样式可能会出现按钮错位、菜单样式冲突。稳妥做法是把 Univer 放在一个相对隔离的区域内必要时使用 iframe 承载。虽然 iframe 不优雅但在“先保证不出问题”的前提下它是最可靠的隔离方式。5.3 性能与内存页面卸载后表格还在运行在 Vue 或 React 项目里使用 Univer最容易出现的问题是路由切换后Univer 实例仍然被引用监听器、命令拦截器、渲染引擎都没有销毁。结果就是用户来回切换页面后浏览器内存一路飙升甚至出现多个表格实例互相干扰。正确的做法是在组件卸载时主动销毁 Univer 实例。onBeforeUnmount(() { univer.dispose(); });如果你的业务是“后台常驻一张主子表”也可以不销毁但要保证全局只有这一个实例。我早期犯过的错是把 Univer 实例挂到某个局部变量里页面切走又切回时重新初始化了一份结果两张表格叠加在一起单元格错位。后来改成了“先销毁再重建”或者“全局单例复用”的策略这个问题就彻底消失了。5.4 常见问题速查表现象可能原因处理方式表格不显示或高度异常容器高度为 0给容器设置显式高度Tab 切换后触发 resize中文界面变成英文locale 配置缺失在构造参数里注册zhCN语言包单元格无法编辑保护区配置误设或权限拦截器生效检查setProtection参数确认可编辑白名单工具栏按钮报错插件版本不一致统一升级到相同版本重新安装依赖复制粘贴后数据异常粘贴命令绕过 UI 校验在命令拦截层额外校验目标区域权限路由切换后内存上涨实例未销毁组件卸载时调用univer.dispose()createUniver函数找不到版本变化导致 API 更名改用new Univer() 插件注册方式最后说一点个人体会。Univer 确实解决了我很长时间以来的痛点以前做在线填报总是不得不用“一堆 div 拼表格”或者“花钱买商业控件”前一种体验差后一种改不动。Univer 把完整表格能力还给了开发者但它不是一个开箱即用的成品业务逻辑、权限边界、提交策略这些都要自己一层层设计和实现。如果你只是为了一个三五行的表单没必要为它引入整套引擎但如果你想做的是真正长期迭代的在线表格类业务在它身上投入的时间大概率是值得的。
返回列表