
老板扔过来一句话做一个在线填报表让用户在我们自己的页面里填数据只能填我指定的那几格其它单元格谁都不许动。这个需求听起来不复杂真正落地才发现一堆细节怎么锁定区域、怎么让别人只改输入区、填完的数据怎么回收。我最后选的是 Univer——一个开源、可以嵌入到任意前端框架里的在线表格引擎。这篇文章把从集成、配置保护、到收集数据的完整过程写下来包含我踩过的坑和一段可以直接抄的代码。Univer 的优势在于它不是一个在线表格网站而是一个表格引擎。你可以把它理解成一套能跑在你自己网页里的Excel 内核用 TypeScript 操作单元格、公式、样式、保护规则渲染则基于 canvas交互速度比常规 DOM 表格方案快很多。对于用户定义表格、锁定部分单元格、收集填写结果这个场景它可以说是目前最省力的一条路。适合谁看前端工程师、产品经理以及任何要在自己系统里嵌入一张业务表格表单的团队。1. 从锁定单元格这个需求说起Univer 的定位和选型理由1.1 我要的其实是一个有格式的表单先交代背景。我要做的是给公司内部一个预算填报页面运营人员打开网页填写一组数字提交后汇总到总表。表面上是张表格本质上是个表单——表格要像一个可以自由定义的东西表头、说明文字、公式、样式都由我定义用户只能编辑我给出来的那几个空格填完的数据要能导出。试过几类方案都差口气。第一类是表单生成器比如各类在线收集表。优点上线快但表格感太弱。用户要填十几行明细时体验非常割裂样式、计算、跨单元格联动基本做不了。第二类是自研表格。回头一算光是一个可编辑网格、撤销重做、格式刷、公式引擎团队一个季度都不一定做得完。第三类是商业在线表格产品的开放接口。能力全但权限模型是人家定的嵌入自家系统的定制成本高数据也要走对方服务后期想深挖都受制于人。Univer 正好卡在中间开源表格引擎能嵌进自己的页面单元格是否允许编辑通过 API 精确控制Canvas 渲染性能也够用。这篇就是我在落地模板定义 锁定单元格 回收数据这个场景的过程中把验证过的做法和踩过的坑整理出来的完整记录。1.2 先对齐几个基本概念后面代码才不晕动手之前把术语先聊清楚后面看代码会顺很多。术语含义对应到 Excel 的概念Workbook工作簿一个 Univer 实例一个 xlsx 文件Worksheet工作表工作簿里的一个 sheet文件底部的一个标签页Range区域连续单元格区间选中的一块格子ICellData单元格数据对象一个格子里的值和样式Univer 的核心是一个叫 Facade API 的东西代码里的univerAPI就是它。它把底层的命令、事件、插件封装成简洁方法业务逻辑基本都跟它打交道。要理解锁定单元格关键是它有两层保护概念工作表保护Worksheet Protection给整个 sheet 设一个总开关控制能否编辑能否插入行能否筛选这些全局权限。范围保护Range Protection针对某个区间单独设规则可以覆盖工作表保护的默认表现用来解锁或加锁特定区域。这两层组合起来就是全表锁定、指定区域开放的核心机制。1.3 为什么我不推荐自己写一个只读表格有人可能会说锁定单元格而已自己写个 div 表格不就完了数据量小于几十格确实可以。但一旦涉及公式联动总金额 单价 × 数量、单元格样式表头背景、边框、冻结首行、批量粘贴 Excel 数据、撤销重做自研网格的成本立刻失控。Univer 把这些能力都做成了现成插件我们要做的只是把保护规则配置好。这就是我最终选它的关键原因不重复造网格只写业务规则。2. 五分钟把 Univer 跑进现有页面集成方案与版本选择2.1 最省事的方案官方预设集Univer 的插件体系很细早期版本要自己拼十几个包对第一次使用的人门槛偏高。好在现在官方提供了预设集univerjs/presets一条命令装完npm install univerjs/presets页面里放一个容器div idapp styleheight: calc(100vh - 160px);/div初始化代码非常短import { Univer } from univerjs/presets; import { createUniverSheet } from univerjs/presets/sheet; import univerjs/presets/lib/index.css; const univer new Univer({ locale: zhCN, plugins: [ createUniverSheet({ container: app, }), ], }); // 创建第一个空工作簿 const { univerAPI } univer; univerAPI.createWorkbook();这里有几个点很容易踩容器必须有明确高度。Univer 的 canvas 绝对定位铺在容器里如果父容器高度为 0页面打开就是一片空白控制台还不一定报错。locale 记得配zhCN。默认是英文右键菜单、工具栏、弹窗全是英文配完才是中文界面。createWorkbook()可以不传参数直接建空表也可以传入初始数据。后面定义模板就是靠这个入口做初始化。2.2 DIY 插件组合非必要不建议翻 Univer 文档会看到univerjs/sheets、univerjs/ui、univerjs/engine-render等一堆包那是给深度定制场景用的。比如要把工具栏完全换掉、要实现自己的文件协议才需要自己拼插件。对于填表这种业务导向的项目预设集默认已经带上公式、数字格式、撤销重做、数据验证等常用能力。这里有个重要取舍一上来就自定义插件组合会把大量时间消耗在环境配置而不是实现功能上。先跑通再考虑剪枝。2.3 初始化时我踩过的两个坑第一个坑是样式路径。univerjs/presets/lib/index.css这个路径在不同版本里变过有时是styles.css。建议装完包直接去node_modules/univerjs/presets/lib里看实际文件名别照抄旧博客。第二个坑是多实例冲突。Univer 初始化时会往页面挂全局样式和事件监听如果同一个页面有两个new Univer()快捷键、粘贴事件会互相打架。后来我把填表页设计成单页单实例模板切换都在同一个实例里换 sheet体验也更顺。3. 重头戏把锁定精确落到单元格上3.1 保护模型理解对了代码才不会写反打个比方。工作表保护像一栋楼的门禁全楼默认不许进管理员决定哪个房间可以进。范围保护则是在门禁之上再贴一张特殊通行证针对某个房间单独放行。在 Univer 里的具体表现是开启工作表保护后默认所有单元格都不允许编辑给某个区间建了范围保护并且permissions里带了Edit这一小块区域就被放出来范围保护没有覆盖到的区域仍按工作表保护的默认规则执行。因此锁定全表 开放输入区需要两步配合。只开工作表保护全部锁死用户什么都干不了只建范围保护但没开工作表保护整张表照常随便改等于没锁。3.2 可以直接抄的锁定配置代码以下代码在 Univer 0.2 的 Facade API 上验证过小版本字段名可能有微调但逻辑是一致的async function setupSheetProtection() { const workbook univerAPI.getActiveWorkbook(); const sheet workbook.getActiveSheet(); // 第一步开启整表保护 await sheet.setWorksheetProtection({ name: 填报模板保护, password: 123456, allowSelectLockedCells: true, // 允许点击选中被锁单元格 allowSelectUnlockedCells: true, // 允许点击选中已解锁单元格 allowSort: false, allowFilter: false, allowUsePivotTable: false, allowEditObjects: false, allowEditScenarios: false, allowFormatCells: false, // 禁止改格式防止用户把模板样式破坏 }); // 第二步给输入区单独放行 await sheet.setRangeProtection( [ // 第 3 行到第 20 行、第 2 列到第 5 列 // 即 Excel 里的 B3:E20 { startRow: 2, startColumn: 1, endRow: 19, endColumn: 4 }, ], { name: 用户输入区, password: 123456, permissions: [Edit], } ); }写的时候注意三点行、列都是0 开始的下标。Excel 里的 B3 对应startRow: 2, startColumn: 1新手最容易在这里算错导致锁错格子。permissions: [Edit]是关键。漏写或者权限数组不含Edit范围保护默认表现是只读你反而会把输入区锁死。password只是取消保护的凭证Univer 的前端保护并不做真正的加密这点在 3.4 里专门讲。如果模板管理员要修改输入范围可以删掉旧保护再重建await sheet.removeRangeProtection([ { startRow: 2, startColumn: 1, endRow: 19, endColumn: 4 }, ]); await sheet.removeWorksheetProtection();3.3 锁住之后用户在界面上会看到什么保护生效后的实际表现鼠标移到被锁定区域光标变成箭头而不是十字暗示这里不能操作点击单元格可以选中但键盘输入不会写入内容也不会出现编辑框右键菜单里的插入行排序格式设置等操作被禁用取决于工作表保护的权限开关输入区内一切正常能输入、能回车换格、能复制粘贴。产品层面有个细节值得注意我把allowSelectLockedCells设成true。如果设成false用户点击锁定区域连选中都不允许滚动浏览时点哪里都没反应体验很怪。填表场景建议保留可选中但不可编辑整体观感更像一个正常的表格。3.4 前端锁的边界密码不等于安全机制说点实在话。Univer 的前端保护是界面层约束不是数据层加密。任何有基础的前端开发者打开控制台都可以通过univerAPI执行命令绕开保护。密码也只是存在浏览器内存里的凭证字符串。所以在真实业务里我把这套保护理解成三类用途防手滑避免普通用户不小心破坏模板结构引导填写锁定区域自然形成你要填的地方就这几格的视觉暗示辅助服务端校验提交数据到后端时后端必须按相同规则再校验一遍哪些单元格允许写入。如果你的场景是强权限比如财务数据只能特定人改老老实实在服务端做鉴权前端锁只负责表达这个规则别指望它挡人。4. 表格变成产品填完的数据怎么收回来锁定只是第一步。填表工具最终的价值在于用户填进来的数据能不能及时、准确地回流到业务系统。我拆了四种收集方式按场景选。4.1 实时监听一填完前端立刻知道希望边填边收集的场景用cellChange事件univerAPI.on(cellChange, (params) { const { unitId, subUnitId, row, column, value } params; console.log(单元格变化, unitId, subUnitId, row, column, value); // 在这里做自动保存、校验或者发送到后端 });注意版本差异不同版本回调参数字段名不太一样有的版本是column老版本是col。收到事件先console.log看一眼再写业务逻辑。另外这个事件只在用户交互编辑时触发程序 API 设值不会触发逻辑上要区分开。实时模式适合做填报进度统计给运营一个大屏看谁填到哪了。缺点是事件很密集连续输入会有一堆小请求。我在这里做了 500ms 的防抖只提交最终状态实测效果不错。4.2 主动读取统一时间点拉数据如果不需要实时更稳的方式是用户填完点提交前端一次性把输入区读出来const values await sheet .getRange(2, 1, 18, 4) .getValues(); // values 是二维数组每格是 ICellData取值用 .v const rows values.map((rowArray) rowArray.map((cell) cell?.v ?? null) ); console.log(rows);提交代码const rows await collectInputArea(); await fetch(/api/collect, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ templateId: budget-2025, rows }), });优点是逻辑简单、好回溯后端一次性拿一整块数据加事务也方便。让我说实际取舍线上用填完点提交比边填边存更符合用户直觉我最终上的就是这个方案。4.3 导出 Excel不是所有业务方都接受在线汇总很多运营还是习惯要.xlsx文件。挂上 xlsx 插件后导出就一行事import { UniverXlsxPlugin } from univerjs/xlsx; // 在 new Univer 的 plugins 数组里注册 plugins: [UniverXlsxPlugin()] // 导出整个工作簿 const buffer await univerAPI.exportToXlsx(); const blob new Blob([buffer], { type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, }); const url URL.createObjectURL(blob); const a document.createElement(a); a.href url; a.download 填报数据-${Date.now()}.xlsx; a.click(); URL.revokeObjectURL(url);提醒一下导出的是整个工作簿。如果用户的表格里藏着内部模板 sheet导出前要先把不必要的 sheet 移除或者干脆在独立实例里生成导出文件。不同版本 xlsx 包里的插件导出名有差异装包后看成员名再写别照抄旧文。4.4 把哪几格能填做成配置模板就是一份 JSON做到这一步你会发现定义表格完全可以 JSON 化模板 ID、sheet 名、表头内容、锁定区域、输入区范围、校验规则全都可以是一份配置。我的做法是在后端存模板定义{ templateId: budget-2025, sheetName: 预算填报, headerValues: { A1: 项目名称, B1: 负责人, C1: 预算金额 }, editableRange: { startRow: 2, startColumn: 1, endRow: 19, endColumn: 4 } }前端初始化时读配置动态创建 workbook、填充表头、应用保护。产品经理要加新模板改一份 JSON 就行保护规则从写死代码变成可配置数据这是填表产品后期维护最舒服的状态。5. 实测几周后想吐槽和避坑的部分5.1 保护不生效按这个链路排查最常遇见的坑是我明明设置保护了怎么还能改。我的排查链路固定四步确认设置成功。setWorksheetProtection和setRangeProtection都是异步操作await后最好读返回或查一次保护状态。有些命令是 fire-and-forget 的设置和读取之间需要一点时间你以为设了其实还没落盘。检查工作表保护有没有开。只建范围保护、没开整表总开关全表默认可编辑。百分之八十的不生效都出在这一步。检查 ranges 坐标。startRow/startColumn是全闭区间0 起算。我写错过一次把endRow和endRow - 1混了导致锁定区域比预期多一行。检查权限数组。范围保护permissions漏写Edit输入区也会被锁死症状是想填填不了。5.2 版本升级是真的会改 APIUniver 迭代很快0.1.x 到 0.2.x 之间插件名、初始化方式、Facade API 都变过。我看老教程时createUniverSheet还不存在初始化代码完全对不上。后来学乖了只认两个权威官方文档的快速开始页和node_modules里实际安装包的类型声明。博客可以看思路代码直接复制大概率踩版本坑。另外一个习惯是锁版本号用npm install --save-exact锁住升级时单独开分支重点验证保护逻辑、事件回调、导出这三个最容易受影响的链路。5.3 移动端和性能的取舍Univer 是 Canvas 渲染PC 上性能没有问题。移动端我也试过手指缩放、双击输入基本可用但小屏幕上精准点击小格子的体验实在一般尤其填表场景输入频繁移动端用户更适合做简单表单页而不是直接操作表格。没有明确移动端填表需求的话优先桌面。数据量方面单 sheet 几百行、几十列完全没有压力。但如果模板要支撑上万行局部渲染压力就上来了。别把 Univer 当大数据平台用该分页分页该导出导出。5.4 哪些场景别硬上 Univer说点泼冷水的。下面几种情况建议绕道强实时协同编辑多人同时改同一格还要看到对方光标需要后端协同服务配合不是单纯接前端就能搞定复杂度很高团队没有前端人力至少得有一个人会写一点 TypeScript 来维护初始化、模板配置和提交逻辑纯业务人员用不转强安全要求前端锁挡不住开发者数据安全必须靠服务端。反过来如果是要在自己系统里嵌入一张能填表、能锁格、能回收数据的在线表格Univer 是我目前用过最顺手的选择。整套链路跑通之后一个新模板从配置到上线基本一天内能完成。最后分享一个小技巧上线前强烈建议录一遍屏幕找非技术同事来填一次表重点观察他会不会试图改锁定区域、会不会找不到提交按钮。我在这个环节发现很多用户其实分不清哪些格子能填。后来给输入区加了浅色底纹在被锁区域第一行加了灰底提示灰色区域不可编辑误改率立刻降下来了。这类保护规则做得再严谨都不如视觉引导来得直接。