先聊个真实场景。前阵子有个做内部运营系统的朋友找我说他们的需求听起来很简单给业务部门做一个“项目信息收集表”表格由管理员定义好表头和结构提交人只能在几个指定的白色单元格里填内容其余区域雷打不动。他一开始打算用普通表单但字段太规整、不够灵活后来又想在 Excel 文件里做权限结果线上协作和回收数据变成灾难。最后他换成了 Univer前后大概一个下午就搞定了。Univer 是一个开源、基于 TypeScript 的在线电子表格引擎支持表格、文档和幻灯片但大家用得最多的还是它的表格能力。你可能在 GitHub 上见过这个名字star 涨得很快。它的核心价值不是“能画出一张表”而是替你解决了一整套在线表格的底层问题单元格模型、公式引擎、渲染、撤销重做、协同编辑、样式、工具栏、国际化全都内置你只需要在上层做自己的业务逻辑。这篇文章就围绕一个非常典型的需求展开基于 Univer 做一个模板型表格允许用户自定义模板结构但提交者只能填写指定单元格其他单元格全部锁定不可修改。我会讲清楚它背后的数据模型和权限模型再给出一套可以直接落地的实操路径以及我踩过的几个坑。前端开发同学可以直接照着做产品经理或技术负责人看完也能大概判断这套方案是否适合你的场景。1. Univer 到底解决了什么问题1.1 为什么不是自己做一张“假表格”先说个很多人会踩的弯路一开始想用 HTML 表格 contenteditable或者直接在 Canvas 上画格子。这两种方案在小范围内看起来都没问题一旦需求变成“要支持复制粘贴、公式、合并单元格、撤销重做、多端协同”你会发现自己其实是在重复做一个电子表格软件——从零开始做这件事按周计的开发量根本打不住。Univer 出现的意义就是把这个“重复造轮子”的过程砍掉了。它给你提供了一整套已经跑通的表格基础设施你只需要关心业务层模板什么样、谁可以编辑哪里、提交的数据怎么回收。底层渲染用的是 Canvas 架构 上层 React 组件交互手感接近传统桌面 Excel又天然适配浏览器。这里插一句对比。老牌开源方案里Luckysheet 用的人不少但项目活跃度已经明显下滑遇到 bug 基本只能自己修SheetJS 更适合做纯解析和生成 Excel 文件交互界面不是它的强项x-data-spreadsheet 则偏轻量功能相对有限。Univer 的社区活跃度、架构现代化程度和官方文档完整度在同类项目里是比较突出的一个。方案是否需要渲染 UI协同公式持续维护度适合场景自研 canvas 表格需要难做难做完全自己扛只有极轻量需求SheetJS不需要无解析为主商业维护导出 Excel 文件Luckysheet需要有有活跃度下滑老项目维护x-data-spreadsheet需要弱弱一般简单表格展示Univer已内置有有高在线编辑、模板、协同1.2 Univer 的核心数据模型要想操控 Univer先得理解它的数据分层。从上到下大概是这样的Univer 实例全局对象负责插件管理、调度、入口创建。Workbook对应一个表格文件包含多张工作表。Worksheet对应一张表包含行、列、单元格数据、样式和保护配置。Range单元格区域比如 A1:D10几乎所有操作都以 Range 为入口。Cell最小单元包含值v、样式s、公式、富文本等属性。这个模型你在后面设计模板时天天都要用。比如说“锁定单元格”本质上是改某个 Cell 的样式属性或某个工作表的保护配置而不是像操作 DOM 那样去设 disabled。理解这点后思路就清晰多了。Univer 的命令系统也是它很值得称道的一部分。几乎每一次操作例如“设置单元格值”、“改变样式”、“锁定区域”都会变成一条命令。这意味着撤销、重做、协同同步、操作录制都被统一管起来了。你不需要自己在每个按钮回调里写一套撤销栈这个底层已经替你处理好了。1.3 “模板填写”到底考验的是什么能力回到开头的需求管理员定义表格、提交人只能填指定单元格。这件事拆开看包含四个子问题表格内容由谁来定义答案是通过 Univer 的 UI 和 API用户可以直接创建行列、拖拽调整宽度、合并单元格、写表头文本。哪些单元格可编辑这个需要保护模型来约束。用户会不会绕过前端限制前端锁定的本质是“防误操作”不是“防恶意攻击”服务端必须做二次校验。填完的数据如何回收通过监听修改事件或者在提交时统一读取数据快照。Univer 的保护机制和 Excel 的传统模型很像先把整张工作表的保护打开默认所有单元格都不可编辑再把允许填写的区域单独放进“可编辑白名单”里。你只要懂得这个思路后面看代码就跟看说明书一样。2. 需求拆解从“一句话需求”到“三行保护配置”2.1 把需求翻译成技术约束标题里的这句“支持用户定义表格让用户去填写一些单元格其他单元格用户无法修改”我建议把它拆成三个必须满足的技术约束约束一模板创建者拥有完全编辑权限可以改任何单元格。约束二普通填写者进入表格后只能修改被标记为“可填写”的区域。约束三表格结构本身不允许填写者随意改动包括插入行、删除列、合并单元格等操作。第三条很多人会漏掉。你以为只要锁定单元格就够了结果用户用工具栏“插入一行”就能绕过限制。所以完整的方案里要么直接隐藏工具栏中的结构性操作要么通过保护机制限制工作表结构变更。Univer 的保护模型里protect如果开启不只是单元格不可编辑插入删除行列这类结构性操作同样会被限制住。这个特性正好能覆盖约束三属于是一石二鸟。2.2 方案 A工作表保护 允许编辑区这是最推荐的方案也是我实际项目里在用的方案。它的思路是开启工作表保护然后在保护配置里声明哪些范围Range是例外允许用户编辑。用伪代码表达大概是这样的const sheet workbook.getActiveSheet(); sheet.setProtection({ protect: true, password: , // 可选设置后需要密码才能取消保护 selections: [ { range: { startRow: 1, startColumn: 0, endRow: 20, endColumn: 1, }, type: UNLOCK, }, { range: { startRow: 1, startColumn: 3, endRow: 20, endColumn: 5, }, type: UNLOCK, }, ], });这段代码的意图是工作表整体锁定但 A2:B21 和 D2:F21 这两个区域是白名单用户可以在这里输入内容。其余区域即便被点击也不会有编辑光标出现。这里有几个细节要说明。protect等于 true 是开关本体selections是允许编辑的地址簿type: UNLOCK表示这些范围解锁。不同版本的 Univer 在字段命名上可能略有差异有的版本叫allowEditRanges有的版本把type省略掉直接表示解锁但核心逻辑是全网一致的。你使用时一定以当前版本官方文档为准不要照抄旧代码。2.3 方案 B单元格 locked 样式 全局保护如果你用过 Excel一定知道它的“保护工作表”功能。Excel 的底层逻辑是每个单元格有一个 locked 属性默认为 true当你开启工作表保护后所有 locked 为 true 的单元格就全部不可编辑只有被改成 unlocked 的单元格能填内容。Univer 沿用了同样的模型。所以你完全可以把模板里需要填写的单元格的样式设置为{ locked: false }然后统一开启工作表保护。// 把允许填写的区域解锁 const editableRange sheet.getRange(A2:B10); editableRange.setStyle({ locked: false }); // 再开启整表保护 sheet.setProtection({ protect: true });这个方案的好处是即使你没配置复杂的选择区只要样式正确保护开启后自然就形成了“未 locked 单元格可编辑、locked 单元格不可编辑”的效果。缺点也明显你得额外维护一份样式状态如果某个区域的样式被用户或脚本覆盖覆盖锁定就会失效。所以如果你不是特别熟悉 Univer 的样式系统我还是建议优先用方案 A 的白名单模型。2.4 两种方案的选型建议判断维度方案 A保护白名单方案 Blocked 样式核心逻辑声明哪些区域可用声明哪些单元格被锁定配置直观度更接近 Excel“允许编辑区域”需要逐个区域设置样式防御性强白名单之外一律拒绝中容易受样式覆盖影响实现成本低一个配置对象搞定中需要额外设置样式适合场景模板提交、回填表单Excel 文件导入模拟我个人的习惯是模板类需求一律用方案 A因为它把“什么能改”放在一个地方统一管理排查问题特别快。方案 B 更适合你已经从 Excel 文件里导入了大量 locked 样式、不想再额外维护一套白名单的场景。3. 实操从零搭一个可填写的模板表格3.1 依赖准备与初始化在正式开始前先看看需要哪些依赖。以当前版本的 Univer 为例最省事的方式是使用官方预设包npm install univerjs/preset-sheets这个包已经把核心、表格、公式、渲染、UI 等插件都打包好了适合不想深钻插件机制的快速起步。如果你想按需加载可以改用npm install univerjs/core univerjs/sheets univerjs/ui univerjs/engine-render univerjs/engine-formula这里多一句嘴Don’t 看到什么包就全都装一遍Univer 的包名非常多只装 preset 包能少走一半弯路。等后面真的遇到包体积瓶颈了再去拆细粒度插件。然后创建一个最简单的容器并初始化实例div idapp stylewidth: 100%; height: 600px;/divimport { Univer } from univerjs/core; import { UniverPresetSheets } from univerjs/preset-sheets; const univer new Univer({ locale: zhCN, user: { id: template-admin }, }); univer.addPlugin( UniverPresetSheets({ container: app, layout: compact, }) );初始化完之后页面上应该就能看到一个完整的表格界面。这一步如果没渲染出来先看两个地方一是容器有没有高度很多 UI 不显示是因为外层 div 高度为 0二是插件是否重复注册重复注册会直接报错。3.2 创建模板工作簿Univer 提供了createUniverSheet方法可以一次传入工作簿的完整结构。这里我先创建一个“项目信息收集表”包含项目名称、项目编号、负责人、预算等列并预设一行空数据。const workbook univer.createUniverSheet({ id: project-collect-demo, name: 项目信息收集表, sheetOrder: [sheet-01], sheets: [ { id: sheet-01, name: 基本信息, cellData: { 0: { 0: { v: 项目名称 }, 1: { v: 项目编号 }, 2: { v: 负责人 }, 3: { v: 预算万元 }, 4: { v: 备注 }, 5: { v: 填写说明 }, }, 1: { 0: { v: }, 1: { v: }, 2: { v: }, 3: { v: }, 4: { v: }, 5: { v: 仅需修改白色区域 }, }, }, }, ], });从代码里你能看到数据存储是按“行索引”对应“列索引”来的0: { 0: { v: 项目名称 } }意思是“第 1 行第 1 列的内容是项目名称”。这个索引是 0 基数的也就是第一行是 0第一列是 0写代码时千万别混。到这里模板的结构就已经出来了。你可以直接在上面改表头、加说明、调列宽所有修改都会进入 Univer 的命令系统开发者无需再额外维护状态。3.3 锁定表头以外的结构区域接下来是重头戏设置保护。按照方案 A先打开工作表保护再声明哪些区域是允许填写的白名单。假设这张表的“可填写区域”是 A2:B10 和 D2:E10其余全部锁定const api univer.__getFacade(); const workbook api.getActiveWorkbook(); const sheet workbook.getActiveSheet(); sheet.setProtection({ protect: true, selections: [ { range: { startRow: 1, startColumn: 0, endRow: 10, endColumn: 1 }, type: UNLOCK, }, { range: { startRow: 1, startColumn: 3, endRow: 10, endColumn: 4 }, type: UNLOCK, }, ], });结合前面的数据模型这句配置的意思就是第 2 行到第 11 行的 A、B 两列以及第 2 行到第 11 行的 D、E 两列是可以编辑的其他地方只能看不能动。用户即使双击被锁定的区域也不会进入编辑状态。如果你想更进一步把密码加上那么普通用户连取消保护的机会都没有sheet.setProtection({ protect: true, password: your-password, selections: [/** ... */], });需要注意这个密码在前端代码里是完全暴露的只能防普通用户通过界面操作解锁不能防懂技术的人看源码。真正的强校验要靠后端权限这个话题我在下一节展开。3.4 填写端的交互控制锁定了单元格之后下一步是让“填写者”以受限的身份进入页面。一般做法有两种。第一种给管理员和提交者各分配一个入口。管理员页面初始化 Univer 时不去设置保护或者额外传入一个“解锁”指令提交者页面初始化完成后立刻执行上面的setProtection代码。第二种同一个页面里根据登录用户角色判断是管理员就跳过保护逻辑。实操时我通常这样写function initForSubmitter() { const api univer.__getFacade(); const workbook api.getActiveWorkbook(); const sheet workbook.getActiveSheet(); sheet.setProtection({ protect: true, selections: editableRanges }); } function initForAdmin() { // 不需要设置保护或者设置一个空的保护 }两种方式都可以核心是“保护”是动态的不要写死在创建模板里。这样以后想临时开放某个区域让人改或者收回权限都不需要重新发布模板只需调一次接口。3.5 回收用户填写的数据用户填完表格之后你需要拿到填写结果。Univer 提供了 Facade API可以直接从当前工作表读取数据const api univer.__getFacade(); const workbook api.getActiveWorkbook(); const sheet workbook.getActiveSheet(); // 读取整张表的数据 const data sheet.getData(); console.log(data);这样拿到的数据就是一个二维结构每一行是一个数组每个元素对应一个单元格。如果你只需要回收某个区域的值也可以直接用 Rangeconst range sheet.getRange(A2:B10); const values range.getValues();如果你的后端接口需要 JSON 格式可以在这个基础上自己组装。我实际项目中习惯在“提交”按钮里写一个整体导出逻辑把getData()的结果过滤掉空行再转成业务字段提交到服务端。4. 常见问题与避坑实录4.1 问题速查表现象可能原因解决方法锁定后依然可以编辑未真正触发setProtection检查保护配置里的protect是否等于 true锁定成功后表头也锁了想填的格子也锁了白名单范围写错记住行和列都从 0 开始数仔细核对startRow等参数用户插入了一行把模板结构打乱只锁定了单元格没限制结构保护开启状态下结构类命令默认就会被阻止如果没阻止检查是否在工具栏层额外添加了自定义命令样式覆盖后锁定失效使用了方案 B 的 locked 样式但某段脚本重新设置了单元格样式给可编辑区域设置唯一样式标记并在保护逻辑里增加兜底范围数据导出时多了很多空行getData()会返回包含空值的完整行导出前过滤空行或通过 Range 精确读取界面空白不出表格容器没高度或插件重复注册给外层 div 设置明确高度检查控制台插件加载是否报错4.2 合并单元格、样式覆盖和“隐形解锁”合并单元格是模板表格里最常踩的坑。比如你把 A1:C1 合并成一个“大标题”然后只想让下面某个格子可编辑结果发现合并单元格区域的保护行为和你预想的不太一样。核心原则是合并单元格的保护遵循“区域整体判定”规则不要试图对一个合并区域里的单个子单元格做差异化配置。如果你想让合并区域整体可编辑就把整个合并范围加入白名单想让整体锁定就不要加入白名单。没有“白名单外再单独解锁一个角”这种操作别跟模型对着干。样式覆盖是另一个隐蔽问题。尤其是方案 B 的locked样式方案如果用户或脚本调用了clearStyle或批量设置样式可能会把整个区域的 locked 属性重置原本锁定的格子突然就能编辑了。排查时先打开编辑器的样式面板看目标单元格的实际 locked 值而不是只看保护配置。4.3 大数据量模板的性能体感模板场景通常不会太大但如果你做的是一张几千行的数据收集表性能就有必要关注。Univer 的保护配置本身是声明式的开销很低真正影响性能的是大批量设置样式。我之前遇到过一个“卡顿一分钟”的案例原因是代码里对每个单元格循环调用setStyle设置 locked。后来改成按 Range 批量设置时间骤降。建议所有批量操作尽量用区域 API不要在循环里逐格调用。模板初始化时最好把 cellData 直接作为初始化数据一次性传给createUniverSheet而不是建完表后一个个格子去填。4.4 前端保护只是一道“门”不是“保险柜”这一点必须单独说。Univer 的前端保护机制在真实生产环境里只是“防误操作”级别。任何用户只要打开浏览器开发者工具都能调用 Univer 的 Facade API强行修改单元格数据、取消保护、甚至绕过 UI 直接提交伪造数据。如果你的场景只是内部信息收集前端保护完全够用。但如果涉及审批、财务、对公业务后端必须做二次校验用户提交的数据到了服务端要重新核对哪些字段属于可填写范围、哪些字段被篡改了。真正的安全边界永远在服务端不在前端渲染层。这也是我不建议在保护密码上寄托太多希望的原因。密码的意义是防止普通用户用界面操作解锁而不是防御恶意用户。4.5 几个容易忽略的小细节第一工具栏里如果保留了“导入 Excel 文件”的按钮提交者可能直接导入一个新文件覆盖当前模板。解决方法是根据角色隐藏相关工具栏按钮或者在工具栏配置里剔除不需要的功能。第二如果打开了协同编辑多个提交者同时填写同一张表时保护模型依然有效但你需要关注冲突处理机制。Univer 底层协同引擎会同步操作但业务上建议按“一行一个用户”的方式分配填写区域避免两人同时改同一个单元格造成数据覆盖。第三关于模板版本管理。我在实际项目里会把模板定义保存为一份 JSON发布时传给前端初始化。每次模板需要改版就通过后台生成新的 JSON 并更新数据库记录前端不需要发版。这样运营人员也能通过简单的表格编辑器自己维护模板。最后再分享一点个人经验Univer 这套方案我实际用了大半年最直观的感受是它把“表格能力”从“做成一个产品”降级成了“只是项目里的一个模块”。过去需要前端团队花几周去啃的编辑交互、撤销重做、显示渲染现在变成了配置项和 API 调用。如果你也正在做类似的需求我的建议是别急着写业务代码先把需求里的“谁、能改哪里、不能改哪里”用一张表画清楚再对着本文的模型去配置保护区。前期多花半小时理清权限边界后面能少踩一整天的坑。模板数据最好从一开始就设计成 JSON 结构存储给每个模板定义版本号和使用状态。这样即使以后用户量大了要拆分前后端、要加审批流也只是在现有基础上加一层服务不用重构表格逻辑。这算是我做这类系统收获的最大经验了。