
最近我一直在研究 Univer 这个开源表格引擎起因是客户那边一个特别常见的需求给用户一张在线填写的表格字段已经定义好比如“姓名、电话、预约时间、备注”用户打开以后只能填自己该填写的那几格表头、公式、说明文本、别人的数据统统不能动。Univer 恰好就是干这个的。简单说Univer 是一套用 TypeScript 写成的开源办公套件核心能力是电子表格可以 npm 装进任意前端项目直接在浏览器里运行自带公式、样式、合并单元格、筛选、协同编辑和单元格保护。和传统 Excel 不同的是它不止是一个“能在网页里打开 xlsx 的查看器”更像一个能被业务系统任意组装和指挥的表格引擎。你可以自己定义一张模板表格再通过数据和命令精确控制哪些单元格可写、哪些只读甚至精确到某个区域只允许某类角色编辑。这也是“用户定义表格其他人只能填写开放单元格”这个场景会被反复强调的原因。对做数据填报、在线报名、订单登记、内部 OA 的团队来说这套东西非常实用。它相当于把一个“在线 Excel 组件”直接嵌进了自己的产品既保留了表格的灵活性又能把用户的编辑边界卡死。下面我会从需求拆解、代码接入、权限控制落地和踩坑经验几个维度把这段时间用下来的完整思路写清楚。1. Univer 项目解读这个表格引擎凭什么出圈1.1 它解决的痛点从“发 Excel 表格”到“在线填报”以前要收集用户数据最常见的做法是发一个 Excel 模板下去让用户填完再传回来。这个流程听起来简单真正跑起来全是坑有人会把表头改掉有人会删掉公式有人复制粘贴把格式弄得乱七八糟最后回收上来的 50 份文件可能每一份结构都不一样。要人工核对、手工汇总时间全耗在“洗数据”上。如果把表格搬到网页里变成一张可交互的在线表通常还会遇到另一个问题用户只能做很固定的表单输入稍微要加点节奏就不灵活。比如你想让用户在表格里临时插入一列备注普通表单产品做不到但直接放开 Excel 编辑权限又等于告诉用户“你可以动一切”。这个矛盾Univer 恰好能接住。Univer 本质上是把“表格引擎”做成了前端组件。它不是把 Excel 的界面搬到浏览器里而是提供了一套完整的单元格模型、样式系统、公式引擎和渲染引擎。你可以用编程的方式去创建表、设置单元格、控制数据范围也可以让用户像操作 Excel 一样操作表。对于“既要表格体验又要管控边界”的需求这是最直接的一条路。1.2 技术底子和生态定位TypeScript、插件化、Canvas 渲染Univer 整个核心是 TypeScript插件化程度相当高。从渲染引擎、公式引擎、UI 组件到协同模块都是以插件形式注册到 Univer 实例里的。这意味着你不想用公式功能就不注册公式插件不想渲染工具栏可以只渲染格子区域。对集成方来说包体积和功能范围都是可控的。它的渲染层走的是 Canvas 路线不是用 DOM 网格堆一个假表格。大量单元格同时变化时DOM 方案会卡顿明显Canvas 方案在性能上更有优势。我自己测过几万行规模的数据表格滚动、选中、输入基本还能保持流畅这在浏览器里已经属于能拿出门的体验。单说开源社区版Univer 提供的核心能力已经覆盖了常规表格场景单元格编辑、公式、条件格式、筛选、数据验证、工作表保护、xlsx 导入导出。它也有商业版和在线协作版本但“限填单元格”这一层核心能力在开源版里通过公开 API 就能做到不需要一上来就上商业套件。这也是它能在技术圈传播开来的一个重要原因很多团队是拿它当嵌入式表格基础设施来用的。2. 核心需求拆解用户自定义表格与单元格只读控制2.1 真实场景报名登记、预约管理、数据采集中的“限填”需求先把需求说透。所谓“用户定义表格”不是用户随便画格子而是运营或管理员在后台设计一张表列名叫什么、哪几列是标题、哪几列是需要填写的、预留多少行。普通用户拿到的是一张已经被定义好的结构表。举个例子一个线下活动的报名表场景可能是这样管理员定义A 列是“姓名”B 列是“手机号”C 列是“所在城市”D 列是“备注”第 1 行是表头从第 2 行开始才是用户填写区用户打开这张表只能填自己的那一行改不动表头也不能把城市改成“随便写的文本”如果有人想删掉第 1 行或者把“备注”列类型改掉系统必须直接拒绝。这和普通表单不同普通表单里的输入框是预先定死的用户只能在框里输入而在 Univer 里用户看到的是一张完整的电子表格视觉上更贴近 Excel操作习惯也是现成的。不需要重新教育用户只需要把“能动的区域”控制好。还有一类场景是数据采集。比如让各门店店长填写库存表表格中一堆单元格是从系统自动带出来的或者是由公式计算出来的店长只需要填“实际盘到数量”那一列。如果没有单元格级保护哪怕是最仔细的店长也会不小心碰到公式、改坏格式。Univer 的优势是能在“开放编辑”和“严格只读”之间按区域细化。2.2 单元格锁定原理locked 标记与工作表保护的双开关要精准控制“哪个单元格能填”本质上是两件事第一单元格本身有没有被标记为锁定状态第二当前工作表有没有开启保护。这个逻辑和 Excel 是一样的理解之后所有配置都通顺。在 Excel 和 Univer 里单元格默认都是“锁定”状态。但只有锁定的标记还不够如果工作表保护没有开启锁定的单元格照样能改。所以正确姿势是先把不需要用户编辑的区域保持默认锁定再把允许填写的区域显式取消锁定最后打开整个工作表的保护开关。保护一开所有锁定单元格全部变成只读而未锁定的那部分仍然是可编辑的。我把这个逻辑类比成考试试卷整张卷子封起来但是答题区域旁边划了明确的可书写范围。单选题区域是密封的你不能在题目上涂改答题卡区域是开放区域你可以往里写字。Univer 的这个“locked 工作表保护”组合就是在纸张上先画出边界再封住边界外的所有部分。实际实现里很多第一次用的人会栽在顺序上只给单元格加了locked: false但忘了开启工作表保护结果用户照样全表乱写反过来只开启保护但忘了把填写区域置为未锁定结果整张表变成纯只读谁也没法填。这两个开关必须同时工作少一个都会歪。3. 实操把 Univer 集成进项目5分钟做出一张“限填表”3.1 前端项目初始化和插件注册直接上实操。我用 Vite 搭了一个纯前端项目不想在集成阶段引入后端依赖。安装依赖时把核心包和常用插件一起装进去npm install univerjs/core univerjs/sheets univerjs/engine-render univerjs/engine-formula univerjs/ui不同版本和插件名在 npm 上会有细微差异但思路一致一个 Univer 实例注册表格插件、渲染引擎插件、UI 插件需要公式再注册公式插件。初始化代码大致长这样import { Univer } from univerjs/core; import { UniverSheet } from univerjs/sheets; import { UniverRenderEngine } from univerjs/engine-render; import { UniverFormulaEnginePlugin } from univerjs/engine-formula; import { UniverUiPlugin } from univerjs/ui; const univer new Univer({ theme: { primaryColor: #3D84FF, }, locales: { [zhCN]: zhCN, }, }); univer.registerPlugin(UniverSheet); univer.registerPlugin(UniverFormulaEnginePlugin); univer.registerPlugin(UniverRenderEngine); univer.registerPlugin(UniverUiPlugin);这里每个人项目可能不同但核心是先把引擎搭起来再往里面塞表格数据。Univer 的 API 在多个版本间调整过真正接的时候以你当前版本的类型提示为准千万不要对着老教程死抄。3.2 定义模板用数据初始化出标题行与填写区接下来最重要的一步是把“模板表”的数据结构定义清楚。我通常会维护一个表格数据对象把表头、填写区域都提前写好这样后续保护配置才有据可依。const workbookData { id: workbook-001, sheets: [ { id: sheet-001, name: 活动报名表, rowCount: 30, columnCount: 6, cellData: { // 第一行表头 A1: { v: 姓名 }, B1: { v: 手机号 }, C1: { v: 所在城市 }, D1: { v: 预约日期 }, E1: { v: 状态 }, F1: { v: 备注 }, }, }, ], }; univer.createUniverSheet(workbookData);通过cellData先把表头放进去行数预留 30 行列数 6 列。这一步做的是“用户定义表格”中的定义也就是管理员视角的设计结果。需要注意的是真正落到业务系统里这个workbookData不该是前端硬编码的应该由后端根据管理员配置动态生成。管理员在后台创建了一个“报名表”配置了列名、类型、填写范围后端把这些配置转换成对应的cellData结构前端再交给 Univer 渲染。这样“每个业务有不同的表”就成了模板配置问题而不是每次都要发版本。3.3 开启工作表保护其他单元格锁死填写区域放行模板建好之后现在做核心动作把第 2 行到第 30 行、前 6 列的区域设置为可填写其余表头区域全部锁死。Univer 里单元格样式带锁概念我先给可填写的单元格显式标记为未锁。这里做了一个简化处理初始化时给第 2 行之后每个单元格附加一个锁定状态属性实际项目中可以批量循环赋值。const FILL_STYLE { locked: false, }; for (let row 1; row 30; row) { for (let col 0; col 6; col) { const addr ${String.fromCharCode(65 col)}${row 1}; if (!workbookData.sheets[0].cellData[addr]) { workbookData.sheets[0].cellData[addr] {}; } workbookData.sheets[0].cellData[addr].s FILL_STYLE; } }这里我把第 1 行表头保持默认锁定从第 2 行开始的填写区全部置为未锁定。之后对整个工作表开启保护通过命令服务执行const workbook univer.getActiveWorkbook(); const sheet workbook?.getActiveSheet(); univer.getCommandService().executeCommand({ id: sheet.mutation.set-worksheet-protection, params: { unitId: workbook?.getId(), subUnitId: sheet?.getSheetId(), protection: { sheet: true, }, }, });执行完这个命令之后整张表所有默认锁定的单元格就都变成了只读只有被标记为未锁定的填写区域还能编辑。如果当时 API 命令名有出入直接查当前版本的类型定义这类命令通常在univerjs/sheets包里都能找到。这个步骤就是需求里“其他单元格用户无法修改”的直接落点。做完之后我建议你亲测一次点击表头看会不会被选中和修改尝试删除整行看是否被拦截。如果能改说明要么单元格没置为未锁要么保护开关没生效老老实实回查锁定状态。3.4 数据验证在填写区进一步限制输入格式锁定已经解决“能不能改”的问题但用户填的内容还是要约束。比如“状态”这一列只允许填写“待确认、已确认、已到场”不能用自由文本乱打手机号列应该只接受数字。这个能力在 Univer 里对应数据验证。我通常用类似下面这样的命令给“状态”列加一个下拉列表规则univer.getCommandService().executeCommand({ id: sheet.command.set-data-validation, params: { unitId: workbook?.getId(), subUnitId: sheet?.getSheetId(), rule: { type: list, formula1: 待确认,已确认,已到场, ranges: [ { startRow: 1, startColumn: 4, endRow: 29, endColumn: 4, }, ], }, }, });数据验证和单元格锁定是两个互补的机制。锁是管“能不能编辑”验证是管“编辑成什么”。两者一起用才能把一张表定义成真正可靠的数据采集工具。经验是能堵的尽量在表格层就堵掉不要指望后端去清洗脏数据。如果用户在表格里输入了乱七八糟的文本哪怕你后端校验时能拦住运营同学看到那一片五颜六色的格子还是会崩溃。数据验证的价值就是让脏数据根本没有机会进入你的系统。4. 业务化落地模板、权限、协同与后端打通4.1 让普通用户自定义表格模板配置方案把 Univer 集成到页面里只是第一步真正业务化要解决的是“谁来定义表格”。前面我们聊到管理员可以配置表格结构这一节把它拆成一套可落地的模板方案。每个模板至少需要三部分基本信息、列定义、填写区域规则。基本信息包括模板名称、表头行数、预留行数列定义包括列名、列宽、列类型、数据验证规则填写区域规则包括哪一行到哪一行开放给填写。管理员在前端配置完后端存成一份 JSON用户在填写端打开时由接口返回这份 JSON再组装成 Univer 需要的workbookData。这里有一个关键点模板和填写数据一定要分开存。模板是“结构”填写数据是“内容”。如果在后台修改了一列名下次用户打开时只影响新数据的展示但历史数据里那一列对应的内容不能丢。我习惯在模板里为每个列定义一个业务字段fieldKey通过fieldKey把模板列和数据字段绑定起来而不是用死板的列名对应。对用户来说“自定义表格”的体验是新建一个模板配置好表格标题和列设置哪些列可填、哪些列只读然后生成一个链接或二维码发出去。收到链接的人打开后看到的是一张真正的表格而不是一堆输入框。这就是 Univer 比传统表单引擎更吸引人的地方。4.2 多人协作时的权限边界与冲突控制表格一旦开放同一个文档可能会被多人同时打开。这里说的“多人协作”不一定是实时协同编辑也可能是“各填各的单元格互不干扰”。Univer 在单机版里可以做单元格锁定但要支撑多人同时写入同一张表必须考虑两个问题。第一个问题是可见性和编辑权限。锁定的单元格是通过客户端命令执行保护的如果用户懂一点前端技巧理论上可以绕开 UI 直接发命令。所以凡是真正要紧的表后端也必须校验这个用户有没有权限修改这个区域。Univer 的权限模型允许你按工作簿、工作表、区域配置编辑权限但生产环境里我建议后端在接收每个变更命令时再验一次做到服务端兜底。第二个问题是命令冲突。两个人同时填同一个单元格或者一个人正在删除行另一个人在填另一个区域如果没有冲突处理最终结果可能会互相覆盖。Univer 有协同编辑的基础能力但要做成完整产品需要接一套房间服务和操作同步机制。如果你只是内部小范围使用可以用简单的“悲观锁”谁先进入某行的填写状态就临时锁住该行其他人只能等。体验虽然不够高级但实现复杂度低很多。我自己的建议是第一版别一上来就追求实时协同先把“多个用户填写同一张模板表”这件事用提交制处理好。比如每个用户提交后把对应行写回后端数据库再用接口回填到工作表。这样既发挥了表格的交互优势又绕开了协同冲突的深水区最稳妥。4.3 数据汇总、导出 xlsx 与接口回调用户填完一张 Univer 表最终数据要怎么拿到业务系统里决定了这个方案能不能真正替代 Excel 发来发去。最直接的方式是监听表格变更把填写区数据实时上报给后端。Univer 里有数据变更事件当用户修改某个单元格时前端能拿到变更的位置和新值。我会在填入事件里做一个轻量收集把用户已修改的单元格暂存在一个 Map 里等用户点“提交”按钮时再统一上报。这样既避免每次 keystroke 都发请求又能保证最终数据的完整性。导出方面Univer 支持导出 xlsx可以把当前表格内容和样式一并生成文件。我的实践是后端接口保存数据时同时把模板的 JSON 结构存下来当用户需要下载最终表格时后端直接用模板 JSON 套上数据库里的真实数据再调导出能力生成 xlsx。这样导出的文件永远是统一的表头、统一的格式不会因为某个用户调过列宽而变得五花八门。接手这样一个系统最容易被忽略的是“数据归属”问题。一张表里有几十行每个用户只填其中的一行或几行导出汇总时必须能精确找出每一行属于谁。所以我强烈建议在后端数据表里加一个row_number字段记录这条数据对应工作表的第几行免得汇总时对不上号。5. 高能避坑Univer 单元格保护的常见问题排查实录5.1 开了保护还是能改检查这两个开关我做第一个 demo 时遇到的问题特别典型工作表的保护也开了但用户还是能改表头。排查了半天最后发现我在代码里写单元格数据时不小心把所有单元格都设置成了locked: false。保护是开了但全表格都没锁那自然随便改。这个问题本质上是“locked”标记的动作范围没控制好。正确做法是先默认所有单元格都锁定再只给填写区域解锁。很多同学会写一个全表循环给所有格子加样式结果把锁也解了。所以排查顺序应该是先看工作表保护有没有开启再看锁定单元格的 locked 状态是不是自己想要的状态。另一个原因是浏览器里的“撤销”操作。用户在填写区输入内容后按 CtrlZ可能会连同你自己设置的格式变更一起撤销导致锁定状态被回滚。遇到这种情况建议在初始化完成后做一个保护配置校验把当前工作表里的锁定区域打印到控制台核对一遍再真机测试。我把常见问题做了个表方便快速排查问题现象可能原因解决方案表头、不可编辑区域也能修改工作表保护没有开启或开启失败检查保护命令参数和返回结果填写区域也无法输入填写区域单元格没有设置locked: false批量给填写区单元格改样式某些单元格既能改又不能改随机出现用了CtrlZ回滚了保护配置初始化后在命令层重新应用保护锁定的区域可以复制粘贴内容只是锁定没有拦截粘贴行为在粘贴事件里判断目标区域是否可编辑导入的 xlsx 保护丢失文件本身未完整导出保护属性用 Univer JSON 作为模板源不要依赖 xlsx 回导5.2 复制粘贴绕过锁定为粘贴操作加上拦截单元格锁定只限制“用户通过键盘或鼠标修改单元格内容”但复制粘贴是另一条路。如果用户选中一个可编辑单元格复制然后直接粘贴到一个只读区域某些实现下粘贴命令会被保护机制拦截但某些版本还是会有漏网之鱼。尤其是跨表粘贴时用户把整块区域内容带进来锁定的区域可能直接被动改掉。我的经验是不能只依赖解锁和保护的“状态”要给粘贴操作加一道显式的判断逻辑。具体做法是在粘贴命令执行前检查目标区域是否包含不可编辑单元格如果包含就弹出提示并取消整块粘贴操作。这个逻辑和 Excel 的体验类似当你尝试粘贴到受保护的单元格Excel 会提示“单元格或图表处于受保护状态”。这个坑很多人会踩是因为开发阶段只测了“直接输入”没测“复制粘贴”和“拖拽填充”。拖拽填充也可能绕过部分锁定逻辑所以测试清单里一定要覆盖直接输入、复制粘贴、拖动填充、批量删除行、插入行。5.3 大表格性能问题配置再复杂也别全量刷新Univer 的 Canvas 渲染性能不错但“性能不错”建立在合理使用的前提下。最容易翻车的是在表格数据变化时直接把整个workbookData重新赋值给 Univer让它全量重建。几万行数据时界面会肉眼可见地卡顿。正确姿势是走命令批量更新。Univer 内部有命令系统局部区域的更新应该通过 change 事件或 setRangeValues 之类的批量操作完成而不是销毁后重建。我在实际项目里会把“模板配置”和“实时变化的数据”分开管理模板配置初始化时设一次填写区的单元格更新走事件驱动每次只改有变化的 cell。另一个经验是不要给整张工作表设置过于复杂的条件格式和边框样式。锁定区域的样式可以只设在边缘列中间大面积单元格保持默认。空样式比复杂样式渲染压力小很多尤其当你预留了 5000 行、几十列的时候每多一个样式对象渲染的开销都可能翻倍。5.4 导入导出兼容性xlsx 保护属性能不能保住Univer 支持导入导出 xlsx这个概念大家都懂但“支持”不等于“所有属性都双向无损”。我试过用一个带工作表保护的 xlsx 文件导入 Univer然后再导出发现导出的 xlsx 里保护设置有时候会丢失。这对纯在线使用没有影响但如果用户习惯“下载一份带回 Excel 操作”体验就很糟糕。方案是不要把 xlsx 当作主存储格式。Univer 的模板结构用 JSON 保存是最完整的所有锁定状态、数据验证、样式都不丢。xlsx 只是给最终用户的分发格式不是内部数据的真源。要分发时从 JSON 模板重新生成一份 xlsx不要拿用户填过的表格回传再导出。如果你必须从 xlsx 导入带有保护的工作表导入后记得再走一遍“开启保护”的命令而不是相信文件里的状态一定被完整带进来。这个操作在代码里只是几行但能避免很多线下返工。6. 我踩过几次坑之后的真实体会整个项目做下来我最深的感触是Univer 这类开源表格引擎真正的价值不是“网页版 Excel”这个标签而是把表格彻底变成了一块可以被业务逻辑驱动的画布。你可以用代码定义一张表的边界可以让不同角色看到不同的编辑区域可以让用户像操作 Excel 一样自然但每一步操作都落在可控范围里。最后分享一个小技巧。如果你要给“填写区域”和“只读区域”做一个视觉上的区分不要只靠锁定功能用户感知不到哪里能填。我的做法是把可填写区域加一个浅色底纹和细边框让用户一眼看出“白色底可以填灰底不能动”这是交互层面和权限层面双保险。再配合一个“剩余填写人数”或“已提交人数”的角标整个表格填起来就很顺手。这套方案没有太多高深的原理就是把 Excel 的底层逻辑和前端工程能力组合好你的业务系统也会因此少掉一大堆“表格发出去又收不回来”的痛苦。