
最近在做一个内部数据填报系统撞上一个很典型的诉求管理员要能在线上自定义一张表格模板比如项目周报、报销明细、设备巡检表。模板里会有表头、固定列、填数区域普通用户进来后只能往某些格子里填内容其他格子包括表头、说明、公式列一律不允许碰。说白了这就是 Excel 里的“工作表保护”但要在浏览器里实现还要让用户用得顺。我最后选了 Univer 这个开源在线表格框架来落地。Univer 本身是 TypeScript 写的一套 Web 办公套件目前最成熟的就是 Spreadsheet能直接嵌入到前端项目里交互上非常接近本地的桌面表格软件。这篇文章我会把“用 Univer 做一张可编辑区域受控的表格”这条路上的方案选型、权限设计、实操步骤和踩坑经验都展开讲一讲。适合谁看前端开发、低代码平台搭建者、想给业务系统塞在线表格的人都适用。就算你对 Univer 还不熟按着下面的思路也能少走很多弯路。核心就一件事怎么把 Excel 式在线表格变成一张“只让用户填白名单区域”的数据录入控件。1. 项目背景为什么要基于 Univer 做在线填表1.1 需求拆解用户定义表格 指定单元格可填写 其余锁定把标题这句话拆开其实是三个独立需求而且每个需求都不好随意简化第一“用户定义表格”。这里的“用户”往往是业务侧的人不是开发。他们需要在线新建一张表自己写列名、加行、合并单元格、设置列宽甚至预置几个计算公式而不是每次改模板都来提工单找开发改页面。这就决定了我们不能用写死的 HTML 表格而是要把“表格本身”做成一个可配置对象。第二“让用户去填写一些单元格”。这是一张白名单比如 B2:C20、E3:F5 是录入区只有这些区域对填写人生效。这里还隐含一个要求填写人可以通过键盘、鼠标正常输入能复制粘贴能回车换行交互体验要像在 Excel 里干活。第三“其他单元格用户无法修改”。表头不能被删固定列不能被改公式列不能被清空整张表格的行列结构不能被调整。不是靠“前端把按钮藏起来”这种软限制而是要从表格引擎层面禁止编辑行为。这三个需求拼在一起恰好对应 Excel 的“工作表保护”模型默认整表锁定然后显式开放若干可编辑区域。这也是我在整个项目里最核心的设计决策。1.2 在线表格比自绘 HTML 表单强在哪可能有人会问这种需求用普通表单不就完了一个输入框一个字段提交完事。但实际业务里“批量录入”和“模板化数据”的需求远比想象中复杂自绘 HTML 表格会遇到几个绕不过去的坎行列结构与数据脱节。需求方要的是“一张表”表头、分组、合计、备注都要在同一张表里展示HTML 表单很难表达这种视觉密度。操作习惯问题。业务人员已经用惯了 Excel复制一列数据过来、双击单元格粘贴、Tab 键跳格是肌肉记忆。普通table contenteditable根本接不住这些动作。附加功能缺失。公式、数据校验、条件格式、撤销重做这些在自研表格里全是深坑每一项都够开发几周。重复造轮子不划算。就算硬着头皮做出来后面还有导入导出 Excel、打印样式、公式计算这些需求等着几乎没有尽头。所以用现成的表格引擎做交互层几乎是这类需求的唯一合理答案。1.3 Univer 的生态定位Univer 是当前开源社区里比较活跃的 Web 表格引擎核心用 TypeScript 编写整体按插件拆分包括核心表格能力、UI 外壳、公式引擎、数据校验等模块可以按需加载。嵌入方式对前端框架基本无要求React、Vue 都用得上。我选择 Univer 还有一个实际原因它在权限保护这一块沿用了 Excel 的工作表保护模型天然支持“锁定单元格”和“允许编辑范围”的语义不用自己去 Hack 一套交互拦截。加上它的社区示例和 GitHub 仓库都比较活跃遇到问题能在 Issues 里找到不少同类场景。开源表格库里Luckysheet 维护节奏放缓Handsontable 商业授权严格x-data-spreadsheet 偏向轻量对比下来 Univer 在当前阶段更适合做“以表格为基础的业务系统”。2. 方案选型与核心设计思路2.1 表格引擎横向对比选型阶段我把市面上能嵌入浏览器的主流方案都过了一遍重点考察三个维度是否支持行级/区域级权限、是否开源可定制、嵌入成本高低。方案开源状态单元格保护能力嵌入成本适合场景自研 HTML Table完全自控需自己实现极高简单展示表Handsontable商用授权严格支持中企业内商业化项目Luckysheet开源有保护但生态偏旧中轻量表格展示SpreadJS商用付费完整中预算充足的企业项目Univer开源有保护模型支持自定义中现代前端项目、填报类系统不是说 Univer 各方面都碾压而是在“开源、现代、可深度定制”这个交集里它目前是最贴近业务需要的。尤其这类填表系统后期一定会叠加协作、公式、权限细化Univer 的插件化架构给我留了空间。2.2 核心思路默认拒绝显式开放做权限控制有一个经典原则默认拒绝Deny All显式允许Allow List。放到表格场景里就是——先把整张工作表视为不可编辑再把需要填写人的区域做成白名单放出来。这套思路的好处是防漏。如果反过来默认全表可编辑再挨个去锁表头、锁说明、锁公式总有漏网之鱼而且一旦用户在模板里新增了一列新增的列很可能自动变成可编辑等于绕过了所有限制。打个比方整栋楼门禁默认锁死谁想去哪个房间就给那张门禁卡开通对应的权限。而不是每个房间单独配一个锁管锁的人迟早疯掉。Univer 的单元格保护设计正好支持这种模型先把工作表保护打开再注册若干个允许编辑的区域填写人只会命中白名单其他操作全部被引擎挡住。2.3 双页面、双身份的应用架构实际项目中“定义模板的人”和“填写表格的人”基本不是同一拨人权限差异也很明显。所以我把应用拆成了两个页面模板编辑页管理员身份。用 Univer 打开一张全功能表格自由设计表头、合并单元格、设置列宽、预写公式然后把“哪些区域允许填写”保存成配置。数据填写页普通用户身份。从服务端拉模板用 Univer 重新渲染同时把工作表保护规则打上去用户只能碰白名单区域填完提交。模板和填写记录必须分开存。模板是一份 JSON描述表格结构、样式、白名单范围填写记录是另一份数据按单元格坐标保存用户输入值。这两个东西混在一起会非常痛苦——业务上改一次模板历史数据就对不上了。我吃过这个亏后面会展开说。3. 实操过程从零搭一个可写不可改的在线表格3.1 初始化 Univer 项目Univer 的版本迭代比较快包名和入口在不同版本里有调整我下面给的是我实际用的版本形态。你们安装的时候先看官方 Quick Start以 node_modules 里的类型定义为准。npm install univerjs/core univerjs/sheets univerjs/sheets-ui初始化代码大概是这样的import { Univer } from univerjs/core; import { UniverSheetsPlugin } from univerjs/sheets; import { UniverSheetsUIPlugin } from univerjs/sheets-ui; const univer new Univer({ locale: zhCN, }); univer.addPlugin(UniverSheetsPlugin); univer.addPlugin(UniverSheetsUIPlugin, { container: document.getElementById(app), });这一步跑通之后页面上就会出现一个完整的在线表格工具栏、菜单栏、工作表标签都有。如果你只想保留表格区域UI 插件里还有配置项可以裁剪这一步先不用管。3.2 加载“用户定义”的工作簿模板模板怎么来两种方式第一种是管理员在编辑页里操作后把整个工作簿数据结构化保存。Univer 内部的工作簿包含多个 sheet每个 sheet 有行数、列数、单元格数据、合并信息、样式整体可以序列化成 JSON。第二种是直接读一个 Excel 模板文件。把 .xlsx 解析成 Univer 的数据结构再渲染适合已经有线下模板的业务。这里要注意 Excel 里一些特殊格式和公式在转换时会有损耗建议上线前做一轮模板兼容测试。我实际用的是第一种把模板格式抽象成类似这样的结构const template { sheetName: 巡检登记, rowCount: 40, columnCount: 10, mergedCells: [ { startRow: 0, startColumn: 0, endRow: 0, endColumn: 9 } ], cellData: [ { row: 1, column: 0, value: 设备名称, style: { bold: true, backgroundColor: #f2f2f2 } }, { row: 2, column: 0, value: 设备编号, style: { bold: true } } ], locked: true, allowEditRanges: [ { name: area1, range: { startRow: 1, endRow: 39, startColumn: 1, endColumn: 3 } } ] };cellData 的具体字段名在不同版本会有差异可能是cellData也可能是celldata核心逻辑都一样注意跟着官方类型定义走。模板加载完之后浏览器里的表格就已经长成业务方要的样子了。3.3 实现“整表锁定 白名单可编辑”这是重头戏。模板渲染出来后要做的第一件事是执行工作表保护命令。Univer 里面命令都走ICommandService类似 Excel 的“保护工作表”功能。我用到的命令形态是SetWorksheetProtectionCommand参数里的关键字段就是lock和allowEditRangesimport { ICommandService } from univerjs/core; import { SetWorksheetProtectionCommand } from univerjs/sheets; const commandService univer.__getInjector().get(ICommandService); await commandService.executeCommand(SetWorksheetProtectionCommand.id, { unitId: workbook-01, subUnitId: sheet-01, rule: { lock: true, password: , allowEditRanges: [ { name: fillArea, range: { startRow: 1, endRow: 39, startColumn: 1, endColumn: 3 } } ] } });执行完这条命令除了fillArea声明的区域表格里其他地方在 UI 层面全部不可编辑。键盘输入、删除、格式化、拖拽填充这些常规操作都会被保护机制拦下来。这里有个提示Univer 不同版本对保护命令的命名和参数结构有变化。如果你的版本里找不到SetWorksheetProtectionCommand去包里搜Protection相关命令一般都有对应实现。重点不是记住我的写法而是抓住“整表 lock allowEditRanges 白名单”这两个核心概念。整个逻辑跑通之后普通用户打开填报表单看到的就是一张“大部分灰色、只有少数格子能点进去输入”的 Excel。这种感觉很接近需求方要的效果。3.4 用高亮和提示引导用户填写光有保护还不够用户第一次打开页面往往不知道到底该填哪儿。我的做法是给白名单区域加一层醒目的背景色比如浅黄色同时模板顶部固定一行说明文字“黄色区域为可填写项其余内容请勿修改。”实现高亮有两种方式一种是在模板数据里直接把可编辑区域单元格的背景色写进cellData的样式里另一种是用 Univer 的条件格式功能按区域统一设置样式。我建议用后者因为条件格式和allowEditRanges用的是同一套区域描述配置一处样式跟着走不会出现“能编辑的格子没标黄、标黄的格子不能编辑”的错位。3.5 把用户填写的数据取回服务端用户填完格子数据还在表格引擎里要拿到服务端得做一次快照读取。Univer 提供工作簿和单元格的读取 API概念上就是遍历当前 sheet 的行列把每个单元格的值提出来。我这里给一段思路演示const workbook univer.getActiveWorkbook(); const sheet workbook.getActiveSheet(); const result {}; for (let row 0; row sheet.getRowCount(); row) { for (let col 0; col sheet.getColumnCount(); col) { const cell sheet.getCellRaw(row, col); if (cell cell.value ! undefined cell.value ! null) { result[${row},${col}] cell.value; } } }拿到数据后可以按业务需要转成 JSON 提交也可以让后端直接渲染回表格做二次确认。注意一个细节只提交白名单区域的单元格值其他区域即使有人故意通过接口塞值后端也要按白名单过滤一遍不能信任前端。4. 常见问题与排查技巧实录4.1 保护命令设了格子还是能改这是最常踩的坑我遇到过一次。排查方向按顺序来第一检查命令是否真的执行成功。executeCommand返回 false 或抛出异常多半是unitId或subUnitId不对。Univer 的工作簿 ID 和 sheet ID 不一定是你自己起的名字最好在控制台打出来确认一下。第二检查保护规则是否被后续操作覆盖。比如加载模板之后又触发了一次数据初始化把保护状态重置了。建议在模板渲染并执行保护命令后主动读取一次当前保护配置打印到控制台确认lock: true。第三确认你的操作是不是绕过了命令层。直接改内部数据、通过扩展钩子改单元格这些路径不受 UI 保护约束。如果业务里必须走底层那你需要自己在自定义命令里增加权限判断。4.2 粘贴、拖拽仍然能覆盖受保护区域保护开启后正常键盘输入会被挡但粘贴和拖拽填充属于另一条路径。我实测下来在个别版本里粘贴行为有绕过保护的现象。处理方式有两种一是从交互层限制监听粘贴事件如果目标区域不在白名单内直接preventDefault并给出提示二是从引擎层配置看看当前版本是否支持把粘贴行为也纳入命令校验。更稳妥的方案是后端在提交时做强校验前端只保证体验不把前端保护当成安全边界。4.3 用户把表格“玩坏”删除行列、改格式、动结构填报表单里普通用户不应该拥有任何结构化操作权限。我在生产环境里遇到过一个用户不小心把整列删了导致表头和填写区错位提交的数据全乱。解法是把工具栏和右键菜单大幅裁剪去掉插入删除行列、隐藏取消隐藏、拖动排序这些菜单项只保留复制、粘贴、撤销、重做等基础操作。Univer 的 UI 插件支持自定义菜单这块值得单独花时间调。宁可用户少几个功能也别给他们破坏模板的机会。4.4 数据校验规则设置范围前面说的是“能不能编辑”数据层面还得管“填得对不对”。Univer 支持数据校验可以对指定区域设置数字范围、日期格式、下拉枚举等规则。我强烈建议把校验规则的范围写成和白名单一致不要只做全局校验。举个例子某列要求填日期如果校验范围比白名单大用户可能把日期填到旁边的只读格里被引擎挡回来后一脸懵。反过来如果校验范围小于白名单那超出范围的格子虽然能编辑但提交时后端校验不过体验同样很差。两侧范围对齐是最省心的做法。4.5 多人同时填写同一张表怎么办如果业务要求多人并发填写同一张表前端保护只是基础真正的权限控制要下沉到协作层。Univer 本身有协作方向的能力但多人填写同一区域时规则需要自己设计比如“每个人只能编辑自己负责的行”或“白名单按用户动态分配”。我目前的项目阶段是单人填报还没上多人实时协同所以只做了简单处理提交时带上用户身份后端按“用户名 区域白名单”过滤防止串改。等真正需要协同的时候再引入更完整的服务端权限合并。5. 经验总结与后续扩展5.1 模板和数据分开存越早越好这个教训我很早就踩过。最初我把模板结构和用户填写数据放在同一个表里结果业务调了一次表头历史数据全要手动迁移。后来改成“模板表 数据表”两张表模板表存结构、样式、白名单数据表只存单元格坐标和值。模板升级时旧数据能按坐标重新映射到新模板迁移成本低很多。5.2 从“填表”到“业务流”的扩展路径一旦你有了 “Univer 受控表格” 这套底座后面能长出来的东西很多审批流可以基于表格行做单据流转数据提交后可以转成 Excel 导出可以把多个模板串成一个周报系统甚至可以做跨表汇总。关键是别把 Univer 当成“页面上嵌一个 Excel”来用要当成“一张可以被系统控制读写的表格画布”。5.3 几个值得保留的实现习惯最后分享几个我现在还在用的习惯所有对表格的改动尽量走命令层不要直接改内部数据否则权限拦截会失效每次版本升级后把保护命令和校验规则的测试用例完整跑一遍Univer 迭代快API 变动经常发生后台上线一个“模板验证工具”在管理员保存模板时自动检查白名单是否与公式区域重叠避免公式格被当成填写格。在线表格这种交互形态做起来容易做好很难。最容易犯的错误是把 Excel 的完整能力一股脑塞给普通用户而正确的姿势恰恰相反——给用户一个看起来像 Excel 的表格但这个表格的“住址权限”完全在你的掌控之下。Univer 帮我把这层“受控表格”的底子打好了剩下拼业务逻辑就是水到渠成的事。