Univer表格引擎实战:从零实现可编辑区域与单元格锁定
发布时间:2026/10/1 18:05:37来源:尧图网络
1. Univer 是什么解决哪一类问题1.1 一个让我换了三次方案的收数需求上个月我在给内部系统做一个在线收数模块运营同事需要发一张表格出去业务方自己填写指定几列其他单元格一律禁止改动。我第一反应是直接上表单引擎但字段一多表单项的体验就变得支离破碎第二版改成手写表格加弹窗编辑结果运营看了直接摇头——他们要的是打开就像一张正经 Excel能看清楚上下文还能自动汇总。后来我又试了几款商业表格组件要么交互不像在线表格要么权限控制要按 SaaS 席位收费折腾几轮之后我开始认真盯上了 Univer 这个开源项目。Univer 是 TypeScript 写的开源表格引擎交互风格接近常见的在线表格产品支持公式、图表、数据校验、条件格式和协同编辑。它最戳我的一个能力正好对应这类用户定义表格然后让用户填写部分单元格其他单元格无法修改的场景官方叫可编辑区域Editable Range本质就是 Excel 里保护工作表加解除部分区域锁定的思路。如果你也在找能在自己系统里嵌入在线表格、并且精细控制哪些格子允许用户填写的方案这篇文章应该能帮你省掉不少试错时间。1.2 为什么它和普通表格组件不一样我在选型时把常见的方案摆在一起对比过下面这张表基本还原了我当时的判断依据方案类 Excel 交互开源维护状态公式引擎可编辑区域二次开发成本Handsontable一般偏数据网格商业授权为主弱需自己写逻辑中Luckysheet好社区活跃度起伏明显有需自己封装中高x-spreadsheet中等功能更新较慢弱需自己封装低Univer完整活跃版本迭代快完整原生支持中大部分传统表格组件解决的是展示和编辑一堆数据的问题而不是让用户在一个表单化表格里按规则填数。它们的行列模型、单元格对象、命令体系都是为开发者准备的面向普通用户的那层体验要自己搭。Univer 不一样的地方在于它把像在线 Excel 一样的 UI和可编程的数据内核做成了两层上层是现成的表格界面下层是命令驱动的数据操作所以拦截编辑、放行指定区域、给单元格加校验这些事不是靠黑魔法补丁而是架构上就留了口子。1.3 从架构上理解为什么能锁单元格Univer 的数据改动基本都走命令Command体系UI 上的每一次输入、复制、粘贴、拖拽填充最终都会变成一条条命令提交到数据层。这意味着当你想做权限限制时可以在命令真正生效之前插入一道判断符合规则的放行不符合的直接拒绝而不需要去监听各种纷繁的鼠标键盘事件。另外它的功能都是插件化的核心包只负责文档模型表格插件负责行列和单元格UI 插件负责工具栏和编辑界面公式、数据校验、协同各自独立。所以锁定单元格这件事天然拆成了两条线索——一是设置可编辑区域来限制哪些格子可写二是通过权限和 UI 配置让用户感知不到不该出现在他面前的编辑入口。下面我就按这个思路把从搭建到落地的完整过程过一遍。2. 最小可运行工程把 Univer 跑进自己的项目2.1 依赖安装注意锁定版本我用 Vite 加 TypeScript 搭的最小工程Node 版本建议保持较新的 LTS。安装命令如下npm create vitelatest sheet-collect -- --template vanilla-ts cd sheet-collect npm i univerjs/core univerjs/sheets univerjs/sheets-ui univerjs/ui univerjs/engine-formula univerjs/sheets-formula univerjs/theme univerjs/locale这里有一个非常重要的经验Univer 的小版本之间接口变动相当频繁尤其是插件注册写法、命令参数结构这类基础 API。我一开始装的是当时最新的几个包结果文档示例跑不通最后不得不挨个看类型定义非常痛苦。后来我学乖了把package.json里所有univerjs/*包固定到同一个大版本并且每次升级前都先看官方 changelog再拿一个 demo 分支验证。2.2 初始化实例与注册插件注册插件的最小代码如下按顺序把核心、公式引擎、表格、UI 注册进去import { LocaleType, Univer } from univerjs/core; import { defaultTheme } from univerjs/theme; import { zhCN } from univerjs/locale; import { UniverSheetsPlugin } from univerjs/sheets; import { UniverSheetsUIPlugin } from univerjs/sheets-ui; import { UniverUIPlugin } from univerjs/ui; import { UniverFormulaEnginePlugin } from univerjs/engine-formula; import { UniverSheetsFormulaPlugin } from univerjs/sheets-formula; const univer new Univer({ locale: LocaleType.ZH_CN, locales: { [LocaleType.ZH_CN]: zhCN }, theme: defaultTheme, }); univer.registerPlugin(UniverSheetsPlugin); univer.registerPlugin(UniverFormulaEnginePlugin); univer.registerPlugin(UniverSheetsFormulaPlugin); univer.registerPlugin(UniverSheetsUIPlugin, { container: app, }); univer.registerPlugin(UniverUIPlugin);页面上放一个带高度的容器即可div idapp stylewidth: 100%; height: 600px;/div注册完成之后插件会自动创建一个默认的空工作簿并渲染到容器里。如果你发现有版本在初始化时需要手动指定 CSS 入口文件检查一下对应包的样式是不是被自动注入了没有的话就把样式文件手动引进来。这一小步经常被文档一句带过但项目在切换打包器或者做 SSR 预渲染时会突然冒出来卡你一下。2.3 用 Facade API 写出第一行数据Univer 提供了一个面向开发者的高级 API叫 FUniver初始化之后可以拿到操作工作簿的句柄import { FUniver } from univerjs/core; const univerAPI FUniver.newAPI(univer); const workbook univerAPI.getActiveWorkbook(); const sheet workbook?.getActiveSheet(); if (!sheet) return; sheet.getRange(0, 0, 1, 1).setValue(供应商名称); sheet.getRange(0, 1, 1, 1).setValue(联系人); sheet.getRange(0, 2, 1, 1).setValue(联系电话);这里的getRange(row, col, rowCount, colCount)是行列下标从 0 开始算的和我们在 Excel 里习惯的 A1 表示法完全不同第一天写代码就有人栽在这上面想操作第五行却写了getRange(5, ...)结果跑到了第六行。建议在项目里封装一层自己的R1C1转A1的工具函数至少把文档里那种下标的认知成本隔离掉。3. 核心场景指定可填区域其它全锁死3.1 先把模板结构定下来我做的示例是一张《供应商信息登记表》A 列放字段名B 列放待填内容C 列放填写说明第 12 行放自动汇总。需求很明确——只有 B2 到 B10 这块区域允许用户填写其他所有单元格只读防止有人把字段名、汇总公式改坏。这个需求看似简单但拆开看其实有三层第一层是数据权限即命令层面禁止编辑区域外的单元格第二层是 UI 引导用户不应该看到一堆看起来能点但点了没反应的编辑入口第三层是数据可信哪怕有人绕过前端后端也要有能力判断这份数据是否合法。下面逐层处理。3.2 第一道锁设置可编辑区域Univer 的可编辑区域思路和 Excel 的保护工作表一致默认整表不可编辑然后显式放行几个区域。部分版本的 UI 右键菜单里可以直接设置可编辑区域但我在封装业务时更倾向于用命令方式写入这样模板下发、权限变更都能做成接口控制。我当时的实现大致是这样import { CommandService } from univerjs/core; import { SetWorksheetEditableRangeCommand } from univerjs/sheets; const commandService univer.__getInjectedDependency(CommandService); commandService.executeCommand(SetWorksheetEditableRangeCommand.id, { unitId: workbook.getUnitId(), subUnitId: sheet.getSheetId(), ranges: [ { startRow: 1, startColumn: 1, endRow: 9, endColumn: 1 } ], });不同版本里这个命令的 id 和参数结构可能叫法略有差别动手前一定先打开node_modules/univerjs/sheets里的类型声明文件搜一下EditableRange关键字确认当前版本的准确写法。思路可以复用先告诉 Univer这张表默认受保护再把 B2:B10 放行为可编辑区域。设置完成后你可以自己先验证一遍在锁定的单元格里输入内容光标应该被拒绝在放行区域里输入则正常。如果发现锁定区域的单元格仍然能通过拖拽填充或者复制粘贴改掉不要慌这通常是 UI 层的漏网之鱼正好引出第二道锁。3.3 第二道锁UI 层收紧可编辑区域负责的是数据层拦截但普通用户打开表格后工具栏、右键菜单、公式栏这些编辑入口还挂在那里。我的处理方式是给填表模式做了一套瘦身配置隐藏公式栏和大部分工具栏按钮禁掉右键菜单里和编辑相关的项目把初始选中区定位到 B2同时在模板上做样式引导——可填单元格给浅黄色底只读区域用灰底用户一眼就知道该点哪里。这里有个容易被忽略的细节锁定的单元格在视觉上一定要看起来不可编辑。只靠数据层拦截用户点下去没反应会以为是系统坏了配合底色、边框、填写说明三件套之后用户会形成灰色区域本来就不该点的预期运营侧的咨询量能少一半。如果业务上有更严格的权限要求还可以利用 Univer 的命令机制做二次拦截。因为所有编辑行为都走命令你可以在命令执行前挂一层校验判断当前操作是否落在许可区域内不满足就 return false。这种方式的优点是无论 UI 怎么改底层拦截始终兜底。3.4 第三道锁提交前的后端校验前端锁得再死也只能保证正常用户不被误导真要防止数据被篡改提交接口必须有独立的校验逻辑。我当时的做法是前端提交的不再是表格 UI 状态而是只把用户填的那几个单元格的值打包上传后端按模板规则重新校验一遍比如必填项是否为空、枚举值是否合法、编号是否重复。function validateRows(payload: Recordstring, string[], template: TemplateRule) { return payload.every((row) { template.fields.every((field) { if (field.required !row[field.key]) return false; if (field.enum !field.enum.includes(row[field.key])) return false; return true; }); }); }这样设计之后前端锁定和可编辑区域解决的是用户体验和误操作的问题后端校验解决的是数据可信的问题两者各管一段谁也不会把谁顶掉。记住这个分工做权限类功能时思路会清晰很多。4. 把填表体验做完整校验、下拉与公式联动4.1 数据校验让可填单元格知道自己该填什么只锁单元格还不够用户填什么还得有约束。Univer 有独立的数据校验插件注册后就可以给单元格加必填、数字范围、下拉枚举之类规则import { UniverSheetsDataValidationPlugin } from univerjs/sheets-data-validation; univer.registerPlugin(UniverSheetsDataValidationPlugin);对于供应商登记这个场景我给所属区域这一列配了一个下拉枚举给联系电话配了数字位数校验。填表人点进单元格时会出现下拉箭头错误输入会被直接标记出来这比提交后统一报错友好得多。关于数据校验有个实操注意点校验规则一定要绑定到具体的可编辑区域不要整列设置。我第一次图省事直接给 B 列整列加校验结果用户在锁定的说明列里也被触发了校验小红标观感很怪。区域和校验规则应该同源管理模板下发多少行可填校验就精确覆盖多少行。4.2 视觉和引导把表格做成一张会说话的问卷填表场景里用户不关心你的表格功能有多全只关心自己会不会填错。我建议做几件固定的视觉工程表头行加粗并冻结标题行合并居中必填单元格统一浅黄底色已经填写的单元格自动恢复正常底色。Univer 的 Range 接口可以批量设置边框、底色、字体这些操作组合起来就能搭出一个规范的表单模板。另一个实用技巧是把说明内容直接放到 C 列的只读单元格里比如填写 11 位手机号、下拉选择区域不要手输。这样用户不需要额外看文档所有信息都在当前行上下文里。锁定的单元格在这里反而成了优点——说明文字永远不会被填表人改掉。4.3 公式自动汇总只读区域的隐藏价值模板的汇总行用了公式比如第 12 行自动求和sheet.getRange(11, 1, 1, 1).setFormula(SUM(B2:B10));这个公式单元格也属于锁定区域用户改不了但公式引擎会随可编辑区域的数据变化自动重算。这一点非常妙用户填数的格子刷新后汇总结果实时更新整个表格看起来就像一套微型业务系统而不只是一张静态表。公式的表达式是基于单元格地址的如果模板行数会动态变化建议用OFFSET或者命名区域这类相对稳定的引用方式否则插入一行后求和范围就对不上了。我最初直接写死SUM(B2:B10)后来模板扩展成了 20 行整整找了一下午才发现是范围没跟上。4.4 数据保存与回显填表数据的保存方式会影响整个项目的复杂度。Univer 工作簿本身有快照序列化能力不同版本可能叫getSnapshot或toJSON你可以把工作簿结构、行高列宽、合并单元格、公式和可填区域一次性存下来下次打开时重建。但这种全量快照体积不小如果模板是固定的我更推荐只保存用户填写的单元格值再加一个模板版本号。回显时先加载模板骨架再把用户上一次填的值写入对应的可编辑区域。这样既保证模板升级后老数据还能对齐又避免把整张工作簿的 JSON 传来传去。模板和业务数据分开存是我在这个项目里做得最对的一件事。5. 上线后踩坑记录与调优5.1 版本一升级接口就变脸Univer 的 API 演进速度很快我项目跑了两周后想升级一个小版本结果插件初始化参数换了写法、Facade 的getActiveSheet返回值变成了可选类型连带一整片业务代码全部要跟着改。我的结论是生产项目锁定版本升级当重构做。每次升级之前拉一个分支跑一遍核心场景的自动化用例确认锁定单元格、校验、回显这条主链路没坏再考虑合并。5.2 包体积与按需加载Univer 全量功能很重公式引擎尤其占地方。如果只是填表场景不要把所有插件一次性全注册不需要图表就不注册图表插件不需要协同就把协同相关包排除。我把 Vite 的rollupOptions做了按需拆包首屏只加载核心表格和校验相关代码公式引擎延迟到模板确实包含公式时再引入。实测下来首屏体积能砍掉不少对内部系统来说体验提升非常明显。5.3 大模板与大数据的渲染性能模板行数多的时候全量写入cellData会非常慢。我第一次初始化时直接塞了一个大的二维数组页面卡了好几秒。后来改成只写有内容的单元格用稀疏数据描述模板并配合setValues批量写入而不是逐格setValue初始化耗时大幅下降。如果你要展示的数据量本身很大请务必按视图窗口做分层加载别让表格接管整个数据源。5.4 中文输入法与复制粘贴的边角问题做中文产品就绕不开输入法。Univer 在部分版本里对输入法组合状态的处理可能和普通输入框不太一样我们遇到过拼音还在组合中单元格却已经触发提交的情况。稳妥的做法是把输入事件驱动的业务逻辑尽量放在提交整张表时集中处理而不是监听每一次单元格变更去做什么实时联动。另外从 Excel 复制多行数据粘贴进来时格式类型和换行符可能和你预期不一致模板锁定区域外的粘贴行为一定要在测试用例里覆盖到。最后再分享一个小经验我们给每张下发模板都加了版本号字段前端把用户填到一半的草稿自动存进本地存储页面误关后刷新还能恢复。填表场景的用户耐心有限数据丢失一次后面就不愿意用了。把锁单元格和本地草稿放在一起做收数模块基本就不会再被业务方挑毛病了。
网站建设高端定制企业官网