去年做内部数据收集工具的时候,产品经理抛过来一个很刁钻的需求:给业务同事发一张表格,他们只能在指定的空白单元格里填数据,其他区域一个字都不能动。我当时第一反应是,这不就是 Excel 里的“允许用户编辑区域”吗?但在线表格应用里要实现这个效果,还真得认真选一下技术方案。后来我接触了 Univer,一个开源的在线表格引擎,它能在浏览器里原生支持这种“锁定不可编辑区域、开放指定单元格”的交互。
如果你也在做数据填报、审批流、项目周报这类需要“让别人填表格,但又不希望他们乱改公式和结构”的在线工具,Univer 是个很值得研究的方案。它能直接嵌进你现有的前端项目,自己实现可编辑区域的锁定和开放,解决表格填写权限控制的问题。这篇文章我会从需求拆解到落地实现,完整记录我用 Univer 做在线填报表格的过程,顺带把我踩过的坑一并整理出来,希望能给同样在做表格类产品的朋友一些参考。
1. Univer 是什么:重新定义浏览器里的表格能力
1.1 为什么我开始关注 Univer
做前端的应该都有感觉,这几年“在线表格”已经从加分项慢慢变成了很多 B 端产品的刚需。
我接手的那个内部工具有个很典型的背景:业务团队每周要填项目排期表,之前都是把 Excel 文件在群里传来传去,版本混乱到让人头大。后来我们打算在公司内部系统里直接嵌入表格模块,要求很简单:支持在线编辑、支持多人查看、能指定某些区域允许编辑,其他区域锁定。当时我们评估过 SheetJS、Luckysheet 这些方案,也想过自己写一个 canvas 表格组件,但要么是社区维护力度不够,要么是自己写成本太高。后来同事提了一句 Univer 在做这个方向,我就去 GitHub 上看了看。
Univer 是一个用 TypeScript 写的开源在线表格引擎,基于 Apache 2.0 协议开源,国内团队在维护。它的定位不是做一个“在线 Excel”产品,而是把表格能力打包成一套 SDK,让任何产品都能在几天内接入一个能跑公式、能协同、能灵活控制编辑权限的表格。Univer 官方计划支持表格、文档、幻灯片三类载体,表格是其中最成熟的部分。
我当时看到的核心特性包括:类 Excel 交互、公式计算引擎、条件格式、数据透视表、协同编辑,以及官方专门提到的“可编辑范围控制”。后面这项正好切中了我的需求。
1.2 Univer 的定位与核心模块
Univer 的架构如果从代码层面拆解,大致可以分成几个层次:
- 核心层(Core):管理工作簿、工作表、单元格数据模型,不依赖具体 UI。
- UI 层(Design / UI):提供主题、布局、交互组件,让表格能够被鼠标和键盘操作。
- 表格引擎层(Sheets):负责表格的行列数据、格式、公式计算等业务逻辑。
- 插件层(Plugin):Univer 的所有功能基本都是插件,你可以只加载需要的插件,比如只引入表格引擎而不引入文档和幻灯片。
这种插件化设计的好处在于,你的项目只需要按需引入,不需要把整个仓库都拖进来。我希望在内部系统里嵌入一个表格组件,那么实际用到的只是核心层、UI 层和 Sheets 层,再加上一个编辑权限控制的 API。整体包体积可控,构建配置也简单。
Univer 还有一个让我比较放心的点:它内部采用了自绘渲染方案,不是直接套用 HTML table 标签去模拟 Excel 体验。当数据量上来、单元格滚动频繁的时候,自绘渲染的流畅度明显优于 DOM 方案。当然这也意味着你不能直接把普通的 HTML 元素塞进单元格里,不过对于表格场景来说,Univer 已经提供了足够的自定义能力。
1.3 Univer 的技术特点:值得选型的关键理由
对比几个我实际调研过的方案,Univer 有几项优势是选型时最打动我的:
| 技术特点 | 说明 | 对我这个项目的影响 |
|---|---|---|
| 插件化架构 | 功能按需加载,避免引入不必要的代码 | 集成体积可控 |
| 自绘渲染引擎 | 大量单元格场景下滚动更流畅 | 数据量较大的表格也能撑住 |
| 可编辑范围权限 API | 支持保护整张表后按范围开放 | 直接对应我们的填报需求 |
| 同步协同能力 | 底层支持协同编辑状态管理 | 后续多人同时填写有扩展空间 |
| 前后端打通顺畅 | 工作簿快照可以序列化为 JSON 保存 | 数据落库和回显很自然 |
不是每家公司都有精力自己从零写一套表格引擎。Univer 的价值在于,它把“在线表格”这种通用能力变成了可嵌入的工具,而我们只需要聚焦业务层:设计表单结构、设置可编辑范围、处理提交后的数据。对于我这种需要快速交付项目的团队来说,这是最明显的收益。
2. 单元格保护与可编辑区域:从需求到实现的完整拆解
2.1 需求分析:让填的能填,不能填的碰都不能碰
在做表格类功能时,往往最基础的需求反而是最麻烦的:一张表里面,不同角色的权限完全不同。
比如说求 Excel 里最常见的“产品需求收集表”:
- 产品经理填“需求标题”和“需求描述”。
- 研发负责人填“排期计划”。
- 测试负责人填“测试进度”。
- 别的人可以看,但不能改。
放到在线表格场景里,就是用户 A 打开页面后,只能改某些单元格,其他单元格看起来是“死的”:点击没反应,双击没反应,右键菜单也没有编辑选项。用户 B 可能对应另一块可编辑区域。还有一种情况更严格——用户填写区域也不能互相覆盖,比如一行数据中 A 列只能张三填,B 列只能李四填。
这个需求本质上是表格数据权限的一部分。传统做法是后端根据用户身份过滤,再返回不同的数据字段给前端,但这样表格的交互体验就很容易被破坏:明明是一整张表,你却要拼凑出若干个碎片视图。
Univer 的做法更接近 Excel 的机制:工作表本身是完整的,通过“保护”和“例外”来控制谁可编辑哪个区域。整张表先处于锁定状态,然后开放特定范围作为可编辑区域。这种模型理解成本低,也容易和后端权限系统契合。
2.2 权限控制的基本原理:数据层与交互层双保险
我当时在思考这个功能怎么实现时,先梳理了一下在线表格里“不可编辑”到底应该做什么。
最表层的是交互层。用户点击一个不可编辑的单元格时,编辑器不应该进入编辑状态,快捷键也不能触发输入,右键菜单里的“插入、删除、清除内容”等操作应该置灰。Univer 在保护区域会拦截这些操作,效果上和我们打开 Excel 里的“锁定单元格”很像。
第二层是数据层。前端界面拦截了输入,不代表数据就一定安全。如果有人通过开发者工具调用内部方法,或者直接给后端发请求,数据依旧可能被改。所以真正的权限控制必须配合后端:Univer 把工作簿状态序列化后,后端在保存时必须校验用户是否有权限修改对应的范围。
在实际集成时,我用的策略是:
- 前端通过 Univer 设置保护区域,保证正常用户的操作被拦截。
- 后端保存时再根据用户身份和提交的数据范围做二次校验,防止绕过前端。
- 只在用户被明确授权的区域,才允许数据落库。
这套“前端拦截 + 后端校验”的组合,才算是把可编辑区域控制真正落到了实处。
2.3 Univer 中实现可编辑区域的思路与步骤
Univer 中实现“用户只能填指定单元格”的思路,核心是两块:
第一,保护整张工作表。这意味着在没有授权的情况下,所有单元格默认不可编辑。保护操作可以设置密码或权限选项,避免用户随意解除保护。
第二,添加可编辑范围。在保护的基础上,指定某些单元格或某个矩形区域为“例外”,这些区域对用户开放,可以正常输入和修改。
我实际用到的流程基本是这样的:
- 初始化一个 Univer 实例,创建工作簿。
- 在对应的工作表中先填充表头、说明文字、公式等固定内容。
- 对整个工作表开启保护。
- 把需要用户填写的单元格范围加入可编辑列表。
- 用户在页面打开组件后,只能点击可编辑范围填写,其余单元格无法进入编辑状态。
这个模型和 Excel 的“允许用户编辑区域”几乎一致,对于熟悉 Excel 的用户来说,上手成本很低。
3. 用 Univer 搭建在线填报表格:实操全过程
3.1 初始化 Univer 项目与工作簿
先说环境。我的项目是基于 Vue 3 + Vite 搭建的,Univer 本身框架无关,在 React、Vue 甚至原生 JS 里都能跑。为了后面的演示清晰,这里以最直接的 TS 项目为例。
安装依赖:
npm install @univerjs/core @univerjs/sheets @univerjs/ui @univerjs/design @univerjs/engine-formula创建一个容器,给表格设置高度:
<div id="univer-container" style="width: 100%; height: 600px;"></div>然后初始化 Univer 实例:
import { Univer } from '@univerjs/core'; import { defaultTheme } from '@univerjs/design'; import { UniverFormulaEnginePlugin } from '@univerjs/engine-formula'; import { UniverSheetsPlugin } from '@univerjs/sheets'; import { UniverSheetsUIPlugin } from '@univerjs/sheets-ui'; import { UniverUIPlugin } from '@univerjs/ui'; import { enUS } from '@univerjs/ui/locale'; const univer = new Univer({ theme: defaultTheme, }); univer.registerPlugin(UniverSheetsPlugin); univer.registerPlugin(UniverFormulaEnginePlugin); univer.registerPlugin(UniverUIPlugin, { container: 'univer-container', locale: enUS, }); univer.registerPlugin(UniverSheetsUIPlugin);这段代码的作用是启动一个最简的表格应用。第一次跑通后,页面上应该能看到一个完整的、类 Excel 的表格界面。如果这里没出现表格,优先检查容器高度,以及注册插件的顺序。
初始化之后,Univer 会默认创建一个空工作簿。接下来要做的,是往工作簿里写入我们的表单结构。
3.2 制作表单:表头、公式与样式
我要做的是一张“项目周报填报表”,大致结构是:
- A 列:任务名称(固定内容)
- B 列:负责人(固定内容)
- C 列:本周完成进度(用户填写)
- D 列:存在问题(用户填写)
- E 列:备注(用户填写)
- F 列:自动计算进度百分比(公式,禁用)
先在工作表里写入表头和固定内容。Univer 中可以用底层命令直接操作单元格数据,也可以用 UI 里的方式设置值:
const workbook = univer.getActiveWorkbook(); const worksheet = workbook.getActiveSheet(); // 设置表头 worksheet.getRange(0, 0, 1, 5).setValues([ ['任务名称', '负责人', '完成进度', '存在问题', '备注'] ]); // 写入固定内容 worksheet.getRange(1, 0, 3, 2).setValues([ ['需求评审', '张三', '', '', ''], ['UI 设计', '李四', '', '', ''], ['前后端联调', '王五', '', '', ''], ]);这里我刻意把部分单元格留空,作为用户的填写区域。
接着可以给表头设置一些样式,让它看起来像“模板”而不是普通的空表格:
const headerRange = worksheet.getRange(0, 0, 1, 5); headerRange.setFontWeight('bold'); headerRange.setBackgroundColor('#F2F4F8'); headerRange.setHorizontalAlignment('center');还可以在“完成进度”列后面加一列自动计算。但因为我们后面的保护区域要把公式列锁死,所以先把公式写进去:
// 假设 C 列填的是百分比数字,F 列做汇总展示 worksheet.getRange(1, 5, 3, 1).setFormula([ ['=IF(C2<>"", C2*1, "")'], ['=IF(C3<>"", C3*1, "")'], ['=IF(C4<>"", C4*1, "")'], ]);公式列是给用户看的,不应该被直接修改,所以在保护配置里,F 列必须锁定。
3.3 锁定不可编辑区域并开放指定单元格
到这一步,最关键的功能终于上场了。
我的目标是:整张表默认不可编辑,只有 C、D、E 三列对用户开放。
Univer 里的做法是先保护整个工作表,再将指定范围加入可编辑区域。下面是基于我实际使用版本的示例,API 命名在不同版本可能略有调整,但思路是一致的:
// 第一步:保护整张工作表 worksheet.protect({ password: '', // 空密码代表仅 UI 拦截,不涉及解除保护 }); // 第二步:添加一个可编辑区域,覆盖 C1:E4 worksheet.addProtectedRange({ name: 'user_editable_area', range: { startRow: 1, startColumn: 2, endRow: 3, endColumn: 4, }, allowEdit: true, });这组操作的含义是:工作表的所有单元格默认被锁定,但 C2:E4(也就是任务名称下方的三行填写区域)被设为可编辑。用户在页面上尝试点击 A 列或 F 列时会发现,单元格虽然能选中,但无法进入编辑状态,右键菜单里的“编辑”选项也是不可用的。
如果你需要开放多个不连续的区域,可以重复调用addProtectedRange。比如只让“完成进度”列可编辑,而“存在问题”和“备注”列保持只读,那就只开放 C 列的范围:
worksheet.addProtectedRange({ name: 'progress_editable_area', range: { startRow: 1, startColumn: 2, endRow: 3, endColumn: 2, }, allowEdit: true, });实际操作时,我还会封装一个方法,根据用户的角色和权限动态生成可编辑区域列表。这样不同用户打开同一张模板,看到的可编辑范围完全不同。
3.4 数据收集与结果落库
用户填完表格后,后端需要拿到这些数据。Univer 提供了非常方便的序列化能力,我可以直接把整个工作簿状态导出成 JSON:
const snapshot = workbook.save(); const jsonData = JSON.stringify(snapshot); // 将 jsonData 发送给后端保存下一次打开页面时,再把这个 JSON 传回 Univer,就能完整恢复表格的内容、样式和公式。这种方案的优点是,用户填写到一半的草稿也能完整保留,不需要逐格解析数据。
如果只想要某个区域的纯数据,也可以直接读取:
const values = worksheet.getRange(1, 2, 3, 3).getValue(); console.log(values);这里拿到的就是一个二维数组,可以直接交给后端做字段映射。我在项目中同时用了这两种方式:读全部快照用于“保存草稿”,读取特定范围用于“提交正式数据”。
要注意的是,快照里也会包含之前设置的保护和可编辑区域信息。如果你的业务要求在用户提交后锁定整张表,可以在保存前重新调整保护设置。但更稳妥的做法是后端保存时把提交人的身份、提交时间写进独立字段,前端层面只负责交互锁定。
4. 常见问题与排查技巧实录
4.1 保护区域形同虚设怎么办
我刚开始接入 Univer 时遇到过一个问题:明明调用了worksheet.protect(),但页面刷新后单元格还是可以编辑。
排查下来发现,问题出在调用时机上。Univer 的很多操作需要在工作簿完全加载后再执行,如果页面初始化还没完成就调用 protect,操作会被忽略。解决办法是等组件渲染完成后再配置权限:
univer.onReady(() => { const workbook = univer.getActiveWorkbook(); const worksheet = workbook.getActiveSheet(); worksheet.protect({ password: '123456' }); worksheet.addProtectedRange({ ... }); });另外一个原因是版本不同,API 命名有变化。Univer 在 v0.x 和 1.x 之间迭代很快,我在升级版本后把protect写成了内部旧接口,功能直接失效。建议以官方文档和当前版本的类型提示为准,不要凭印象写。
4.2 复制粘贴绕过限制怎么处理
在线表格里,用户可以把其他区域的内容复制过来,粘贴到受保护范围里。这是一个容易被忽略的漏洞。
我当时测试时发现,虽然用户无法在锁定区域直接输入,但通过右键“粘贴”仍然可以把数据填进去。这其实是因为粘贴操作在 Univer 中走的是剪贴板命令,和正常的单元格编辑不是同一套拦截逻辑。
要解决这个问题,需要拦截粘贴事件,判断目标区域是否允许编辑。Univer 的命令系统里有beforePaste之类的钩子,可以在粘贴操作前做校验:
// 伪代码,表示拦截粘贴并校验目标区域 univer.onCommand((command) => { if (command.type === 'paste') { const range = command.payload.range; if (!isEditableRange(range)) { return false; // 阻止粘贴 } } });这里的关键是维护一个“哪些区域可编辑”的规则列表,每次粘贴前拿目标区域去匹配。注意,前端拦截能挡住正常用户,但不一定能挡住恶意请求,后端保存时一定要再次校验数据变更范围。
4.3 大数据量下的性能优化与移动端适配
Univer 的画布渲染性能在常规数据量下表现不错,但表格行数一旦超过几千,滚动和公式计算已经开始有明显卡顿。我建议:
- 不要一次性在工作表里加载几万行数据,按需渲染和懒加载是更好的选择。
- 公式引擎很强大,但复杂公式数量过多时会拖慢计算速度。可以把不必要实时计算的公式改为后端计算,前端只展示结果。
- 移动端适配方面,Univer 的画布在手机浏览器上也能正常显示,但编辑体验仍需优化。最好在移动端限制可编辑单元格的点击范围,并禁用一些复杂的右键菜单操作。
我在项目里做的妥协是:移动端默认只读模式,填写任务统一在 PC 端完成。这样既保证了用户体验,又不需要对移动端做过多定制。
4.4 版本升级导致 API 变化
这是我最想强调的一点。Univer 作为快速迭代的开源项目,API 变动比较频繁。我的项目从一个早期版本升级到较新版本时,很多接口签名都变了,比如工作簿实例的获取方式从getWorkbook()变成了getActiveWorkbook(),主题、参数结构也不同。
建议在项目里封装一层适配器。不要让业务代码直接散乱地调用 Univer 的 API,而是把所有和表格操作相关的方法集中到一个服务类里。这样即使 Univer 升级,只需要改适配器内部实现,业务逻辑不用动。
5. 实际使用感受与后续扩展建议
5.1 我踩过的坑
个人经验来看,接入 Univer 最大的坑不是技术,而是“表设计”。
产品经理最开始给需求时只说了“指定单元格可编辑”,但没有想清楚哪些字段是最终结果、哪些字段是中间过程。等我把整张表保护起来,用户发现问题了:有些人需要修改“负责人”列,而有些人需要改“备注”列,但角色一多,权限就乱成一团。
后面我们调整方案,把可编辑区域跟后端权限策略绑定,按角色配置“某列某个范围可写”,而不是一刀切。Univer 可以很灵活地添加多个可编辑区域,但前期的业务建模必须提前和产品对齐。
另外一个实际踩过的坑是,在保护模式下,样式修改也被一并禁用。刚开始我们只保护数据,结果用户无法调整列宽、行高,体验很别扭。后来我们专门开放了样式编辑,同时锁定公式和固定内容区域。这里需要根据业务形态做取舍。
5.2 Univer 还能怎么玩
我在完成填报功能后,又做了一些扩展,发现 Univer 的开放能力比预期的更强。
比如可以结合公式引擎做自动计算。我在表格里设置了一些汇总公式,用户填完进度后,汇总行自动更新,不需要额外写代码。又比如通过监听单元格变更事件,把填写记录实时同步给后端,实现类似“自动保存”的效果。
其实,Univer 已经把我的项目从一个“内部数据收集工具”变成了一块可以灵活组合的积木。只要把表格组件的基础能力搭好,后续不管是做审批表单、周报模板,还是做在线排期表,都只需要调整模板和权限规则即可。
现在回头看,Univer 解决问题的思路其实和 Excel 是一致的:先用保护机制让结构固定下来,再把需要开放的区域交还给用户。这套机制在真实的 B 端场景中非常实用,如果你也在做类似的在线表格项目,建议在原型阶段就把可编辑范围控制考虑进去,而不是等上线之后再来补,那时候改造成本会高出不少。