
最早让我认真琢磨 Univer是我接手一个小项目的时候。业务方给了一个听起来很简单的需求他们想要一张放在线表格让客户自己填报名信息但只允许填几个指定的单元格其他区域一律不能改最好鼠标点都点不动。我第一反应是这还不简单锁定一下单元格不就完了结果真正落到具体方案时才发现这里面的坑远比想象的多。Univer 是一个开源的一体化在线文档方案主仓库在 GitHub 上就能找到核心由 TypeScript 编写主要解决在浏览器里做类 Excel 表格这件事。它的优势在于可以嵌入到自己的业务系统中而不是被绑定在某一家在线文档平台上。配合工作表保护 可编辑区间白名单的思路就能实现用户只能填写我允许的单元格其他区域纹丝不动的效果。这篇文章我会完整复盘我是怎么用 Univer 实现这张只能填指定单元格的在线表格的包括为什么选它、核心实现拆解、完整落地步骤、以及我在实际开发里踩过、也帮别人排查过的一堆坑。适合前端工程师、全栈开发以及业务系统里恰好需要轻量表单化表格的团队参考。1. 这个需求到底烦在哪开放填表不等于开放编辑拿到需求的时候你可能会觉得这活儿太简单了。但让用户填一张表格和让用户在一张表格里填几个格子完全是两件事。前者是表单后者其实是对一个成熟表格引擎做权限管控。1.1 为什么不能直接用现有在线文档很多团队一开始会考虑腾讯文档、石墨、WPS 这类产品做法是建好表格后把它分享出去。但你很快就会碰到几个头疼的问题第一是体验问题。用户打开共享文档后天然会尝试去动格式、拖拽列宽、合并单元格甚至不小心删掉别人的数据。你可以把整个工作表保护起来但保护粒度在共享文档的产品里往往不够灵活。你想让用户填 A1 到 A10结果他连标题行的格式都能改或者不小心把手动输入的数据覆盖了管理成本极高。第二是归属问题。表格是别人平台上的数据流转要经过第三方和企业内部已有的业务系统对接时还要考虑 API 限制、开放平台权限、数据安全等问题。对于需要把表格嵌入到自家后台的场景这基本不可接受。第三是回收数据的链路太长。用户填写完之后你需要去平台上人工导出或者依赖回调接口整个过程总让人觉得隔了一层。1.2 为什么自己搭一张半开放表格这么麻烦自己搭的话你立刻会面对几个选择题表格引擎用谁家的填写的可编辑区域怎么做限制用户能不能复制粘贴能不能看到公式提交之后数据往哪里存还有一个很容易被忽略的点你要的不只是一张静态表格而是一个能限制读写的动态页面。用户看到的表格本质上是一个被裁剪过的编辑界面——只开放录入不开放结构。这要求在引擎层面做到能对单张工作表做全局锁定能对指定区域做放行也就是白名单能控制用户在界面上的操作范围比如禁用右键删除、禁用插入行列、隐藏工具栏上的一堆按钮能收集用户填入的数据并把它安全地交给后端。这几个能力凑齐了你才能说这活儿能接。而在开源表格引擎生态里真正能把这些能力糅合在一起的Univer 是做得比较完整的一个。2. Univer 到底是什么为什么我会选它做底子Univer 这个项目我第一次看的时候第一感觉是它把整个在线文档的框架都搬到了浏览器端。它不只做表格还有文档和幻灯片但表格是其中最成熟、社区讨论最多的模块。它支持公式、单元格样式、合并单元格、条件格式、筛选、冻结窗格、复制粘贴、撤销重做还支持多人协同编辑的扩展能力。2.1 核心模块和适合嵌入的架构Univer 在架构上是分层的。核心包univerjs/core管数据模型和命令系统univerjs/sheets管表格领域逻辑univerjs/sheets-ui管界面渲染和交互另外还有公式、条件格式、富文本等一系列插件。这种插件化架构对做二次开发的人非常友好——你不用一次性引入全部功能可以按需加载。一个比较典型的最小集成方式是前端装好依赖初始化一个 Univer 实例指定一个容器节点然后传个工作簿配置进去一张可操作的表格就出来了。它解决的是浏览器里跑一套真正能用的表格引擎这个最底层的问题。2.2 对比 Luckysheet、Handsontable 这些替代方案我最早看过 Luckysheet也试过 Handsontable。各有优势但对照用户填指定单元格、其他不能动这个需求时差别就出来了方案优势在可编辑区间需求上的问题Luckysheet老牌开源、中文资料多、上手快项目活跃度起伏较大保护工作表/权限控制能力偏弱多人在线写入依赖额外配套Handsontable商业控件成熟、性能好、表格渲染流畅开源版 API 受限深度自定义权限和业务系统融合成本高授权需要留意自研表格完全可控成本极高公式、复制粘贴、撤销、样式、选区这些能力都要重造轮子不现实Univer插件化架构、TypeScript 生态、保护/权限能力较完整、支持协同扩展版本迭代快API 有变化需要花一点时间适应选 Univer 还有一个现实理由它的核心仓库一直在活跃更新Issues 里的讨论也是真实在回答工程落地问题。对一个要长期维护的业务来说项目还活着、社区在动是很重要的一点。2.3 一个需要提前知道的事实前端权限只是体验层必须把话说在前面Univer 在前端做的单元格保护和锁定本质上解决的是用户界面上操作不到它不构成安全边界。如果用户足够懂技术完全可以绕过前端直接看原始数据或者伪造请求提交数据。所以我在架构上把权限分成了两层前端锁定是给普通用户看的门闩真正的保安是后端校验。这个思路后面会详细讲。3. 让用户只能填指定单元格核心实现思路拆解明确了选型之后我来说说核心功能的实现逻辑。你想要的用户填指定格子、其他格子不能动在表格引擎的原理里其实有一个标准的表达方式工作表保护 可编辑区间白名单。3.1 先理解锁定和保护到底在干什么Excel 类工具的权限控制普遍是一个两级结构。第一级是单元格级锁定它只是一个状态标记——每个单元格都可以被标成锁定或未锁定。第二级是工作表级保护保护开启之后所有被标记为锁定的单元格都不可编辑。而未锁定的单元格即使开了保护也仍然可以编辑。这个设计非常巧妙它让整表锁死和开放局部不冲突。你只需要记住一条规律工作表保护开启时默认你动不了任何锁定单元格你要放行的格子就把它的锁定状态去掉或者把它加入放行名单你要锁死的格子不用一个个操作因为保护开启后它们天然是锁的。在 Univer 里工作表保护的对象和 Excel 类似除了单元格编辑控制以外还有一类能力是操作级控制例如是否允许用户选择锁定单元格、是否允许插入行列、是否允许删除行列、是否允许排序筛选等。这些选项是为了防止用户通过删行、插列、移动区域等方式间接破坏表格结构。3.2 Univer 里设置保护的核心配置项我以当前版本的一个典型配置为例代码长这样const workbook univer.getActiveWorkbook(); const worksheet workbook.getSheetByName(报名表); const cellRange worksheet.getRange(2, 1, 10, 3); // 第3行到12行B列到D列 worksheet.protect({ password: null, // 不需要密码但这样只挡普通用户 options: { selectLockedCells: false, // 禁止选中锁定单元格 selectUnlockedCells: true, // 允许选中可编辑单元格 formatCells: false, // 禁止设置单元格格式 formatColumns: false, // 禁止调整列格式 formatRows: false, // 禁止调整行格式 insertColumns: false, // 禁止插入列 insertRows: false, // 禁止插入行 deleteColumns: false, // 禁止删除列 deleteRows: false, // 禁止删除行 sort: false, // 禁止排序 filter: false, // 禁止筛选 }, ranges: [ { startRow: 2, startColumn: 1, endRow: 11, endColumn: 3 }, // 放行区域 ], });这段代码的重点不是具体方法名——Univer 的 API 在不同版本里命名会有调整重点是它背后的思路工作表整体开启保护然后通过ranges列表声明可编辑区间。哪怕未来版本里方法名变了、参数位置挪了你按照这个思路去查文档或源码也能很快找到对应的能力。还有一个容易被忽略的设置是selectLockedCells: false。如果这个选项开着用户虽然改不了锁定单元格但可以选中一片包含锁定单元格的区域视觉上会误以为表格卡住了体验非常奇怪。关上之后用户点击锁定区域时不会有任何选区反馈只会感觉自己点不进去这才是我们想要的效果。3.3 把工具栏和右键菜单也收起来单元格保护解决了改不了的问题但用户仍然能看到一些创作类工具。对于只负责填表的人来说工具栏上的字体颜色、背景色、合并单元格、插入删除行列这些按钮只会增加误操作概率。Univer 的 UI 插件通常支持配置项你可以按需隐藏默认工具栏按钮或者干脆自定义一个精简工具栏只保留必要的复制粘贴、撤销重做按钮。右键菜单同理——填写场景下其实不需要插入行、删除列、清除内容这些菜单项能隐藏就隐藏。这一步做的好处是用户界面会非常收敛整个页面看起来更像一个表单而不是一张Excel几乎不会产生我要不要调一下格式的念头。4. 完整落地一张报名信息收集表的搭建过程光有原理不够我直接走一遍我当时搭建的完整流程你就知道这活儿怎么串起来了。4.1 页面整体设计我做的页面分三块左侧是操作说明区用普通 HTML 写的告诉用户哪些格子可以填、格式要求是什么右侧是一个 Univer 容器里面加载一张预先设计好的报名信息表底部是一个提交信息按钮点击后前端收集可编辑区域的数据发给后端接口。页面结构大致这样div classpage-wrapper aside classtips-panel h3填写说明/h3 p姓名、手机号、所属单位、备注为必填项/p p表格其他区域已锁定请不要尝试修改/p /aside section classsheet-container idsheet-container/section /div footer classfooter-bar button idsubmit-btn提交信息/button /footer这样设计纯粹是为了给用户强烈的这是在填表的暗示而不是让他们以为自己在用在线 Excel。4.2 初始化 Univer 并加载模板初始化这块首先要安装 Univer 相关依赖。以我的项目为例最小集合大概是这几个包npm install univerjs/core univerjs/sheets univerjs/sheets-ui然后初始化实例import { Univer, UniverInstanceType } 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: sheet-container, toolbar: false, // 这里可以根据需要关闭或自定义 rightMenu: false, }); univer.createUnit(UniverInstanceType.UNIVER_SHEET, { id: enroll-sheet, name: 报名表, sheetOrder: [sheet-1], sheets: { sheet-1: { name: 报名表, rowCount: 50, columnCount: 8, cellData: { // 模板数据标题行、表头、说明文字等 }, }, }, });这里有个小经验模板最好在服务端保存一份 JSON 或标准 Excel 文件每次用户打开时拉最新版本而不是把模板逻辑硬编码在前端。因为业务方几乎一定会调整表头字段如果模板只存在前端代码里每改一次都要发一次版本。4.3 锁定整表、放行填写区初始化完成后接下来就是核心动作开启工作表保护并把允许用户填写的区域加入白名单。我当时的表格设计是A 列是序号和说明用户不能动B 列到 D 列是用户填写区E 列以后是管理员回填的审核状态用户不可见但管理员可见。所以保护配置对标下来是这样的整表锁定放行 B2:D20同时开启selectLockedCells: false。如果表格底稿带了公式单元格也一样会被保护住用户无法改动公式区域。样式上我建议把可编辑区域设置成淡黄色或浅绿色背景这样用户一眼就能看出只有这里能填。Univer 支持单元格样式设置模板生成后一次性配置好即可。4.4 提交数据与后端回收用户填写完成后点提交前端要做的事不只是拿数据还要先做一次本地校验。检查必填项有没有空、手机号格式对不对然后才把数据组装成 JSON 发给后端。Univer 提供了读取单元格数据的能力你只需要遍历白名单区域const workbook univer.getActiveWorkbook(); const worksheet workbook.getActiveSheet(); const values worksheet.getRange(2, 1, 18, 3).getValues(); const payload values.map((row) ({ name: row[0], phone: row[1], company: row[2], remark: row[3], })); fetch(/api/enroll/submit, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify(payload), });后端拿到数据后再做一次强校验只接收白名单内的字段超出范围的一律丢弃。这一步极其重要因为前端校验可以被绕过后端必须自己守住边界。实际项目中我还建议在后端保存提交版本号或哈希防止重复提交和并发覆盖。5. 最容易踩的四个坑我全踩过一遍方案跑通之后你以为就完事了太天真了。我在这套方案上踩过的坑一个比一个隐蔽也给后来排查的人留下了宝贵的血压升高素材。5.1 复制粘贴绕过锁定用户从外部粘贴一大片第一个坑是用户从微信、Excel 或者其他网页里复制了一大片内容直接粘贴到表格里。由于粘贴目标区域可能同时包含锁定单元格和未锁定单元格有些版本的引擎在判断是否可以粘贴时是按目标区域整体判断的结果就是整块内容被粘贴进来锁定单元格的数据被覆盖了。我当时的处理办法是在提交接口做兜底后端根据模板白名单逐格校验凡是落在白名单之外的修改一律忽略或直接判定提交无效。同时在页面提示用户仅支持在黄色区域粘贴但这只是体验优化真正的防线还是后端。后来我还进一步禁用了右键粘贴菜单只在工具栏保留粘贴纯文本按钮降低格式污染的几率。5.2 撤销重做把管理员配置的模板改回去了第二个坑出现在用户操作比较多的场景。用户填了几行内容之后顺手按了 CtrlZ结果撤销的不只是自己刚填的数据连同表格里一些原本设置好的样式、公式也被回退了。更麻烦的是如果用户在可编辑区域和不可编辑区域之间切换操作偶尔会出现选区状态和实际权限状态不一致的情况。解决方式有两种。一种是干脆在填写场景里关闭撤销功能或者限制撤销栈只记录单元格值变化不记录结构和样式变化。另一种是把撤销功能整个取消用户填错了直接手动改回来反正区域就那么大。对于更严肃的业务可以每次在关键操作后向后端持久化快照管理员可以一键恢复。5.3 多人同时填同一张表后写覆盖先写Univer 本身对协同场景有扩展支持但如果只是把它当作单机表格嵌入页面多人同时填写同一张表时必然会出现覆盖问题。两个用户在同一时刻改了同一个单元格后提交的人会把先提交的人的数据覆盖掉。我的方案是在提交环节加了版本号。用户打开表格时前端会从服务端拉一个模板版本号和数据版本号提交时携带版本号后端只有在版本号匹配时才接受写入否则提示页面数据已过期请刷新后重新填写。这在业务上就变成了谁先提交谁生效后面的人看到提示赶紧刷新。5.4 改完保护配置页面就是不生效还有一次我改了服务端的模板配置给表格新增了一个可编辑列结果线上用户打开后依然点不动那个区域。排查了半天才发现Univer 实例在前端是有缓存状态的模板数据更新后已经加载过表格的用户需要刷新页面重新拉取配置才能真正生效。后来我让前端在每次打开页面时强制检查模板的更新时间戳一旦发现配置版本变了就销毁旧的 Univer 实例、重建一个。干净利落再也没出现过配置改了但不生效的诡异问题。6. 从单张填表到业务系统可扩展的方向跑通了核心流程之后这套方案的价值其实才刚刚开始。你会发现Univer 这个底子能承载的不是一张孤立的表格而是一整套和业务系统耦合的数据采集入口。6.1 模板与业务数据分离一个特别实用的做法是把模板和实例分开。业务方维护一张模板表用户每提交一次系统就基于模板复制出一个填写实例。这样每个用户都有自己的填写页面互相隔离不会出现 A 用户看到 B 用户数据的情况数据归档也清晰。模板字段的变化直接走版本管理历史填写数据不会因为模板调整而丢失。这一步做完之后你会发现业务方再也不需要每次都找你改表格了他们自己在后台改模板前端页面自动感知版本变化。6.2 与审批流、消息通知打通表格里可以增加审核状态列普通用户不可见但管理员打开同一个工作簿时可见可编辑。这就是同一个 Univer 实例在不同角色下加载不同保护配置的典型场景——权限完全是动态的由后端根据当前登录用户返回不同的模板配置。用户提交后后端触发通知给审核人员审核人员在后台表格里回填审核意见和状态用户刷新页面后看到已通过或驳回原因。整个过程都围绕这一张表格完成数据血缘非常清晰。6.3 利用事件机制做填报行为监控Univer 的命令系统和事件机制也可以用起来。用户每一次编辑都会触发事件你可以利用它统计用户填写时长、修改次数、在哪些单元格停留过甚至判断用户是否在提交前删改过关键字段。这张表到底是谁在什么时间改的这件事在业务审计场景下很有价值。我当时做了两件小事一是把用户每次提交前的完整表格快照存一份到对象存储二是把单元格级变更事件记录到明细表。后来真的碰到了用户否认自己填写内容的情况拉出快照比对问题五分钟就解决了。写在最后的几句真心话这套方案做完之后我最大的体会有两点第一权限控制的核心从来不是锁界面而是锁数据边界——前端保护做得再好也只是让普通用户顺着规矩走后端校验才是真正的安全线第二不要把业务逻辑过多地塞进表格里Univer 表格负责的是呈现、录入、限制这三件事至于审批、校验、通知、数据归档全部应该由外围业务系统承担。最后分享一个我的小习惯Univer 的版本迭代速度不算慢升级依赖前我会先去官方仓库看 changelog再把保护配置相关的代码单独抽成一个版本化模块。这样即使 API 变了改动范围也能控制在很小的一片区域。大家在实际项目里如果遇到权限配置不生效、复制粘贴绕过保护、多人覆盖这些问题建议按我上面的排查链路走一遍大概率能省下半天到一天的时间。