如果只是让用户在网页里打开一个在线 Excel,市面上的轮子其实不少;但如果你的需求是“管理员定义好一张表格,普通用户只能在指定单元格里填写,其他地方碰都不能碰”,选型就变得有意思了。我最近在做内部运营信息收集系统时,就撞上了这个需求,技术调研一圈之后,落地方案是Univer——一个基于 Canvas 渲染、支持多端集成的开源在线表格引擎,既能当完整表格应用用,也能当 SDK 嵌进 React/Vue 项目里。这篇文章不是官方文档的搬运,完整记录了我把 Univer 接入真实项目、实现“模板表格 + 指定单元格可编辑”的整个过程。
在动手之前,先明确几个关键词:所谓的“univer 在线”,实际上是 Univer 提供了一套可自托管的在线表格能力;所谓“支持用户定义表格,然后让用户填写一些单元格,其他单元格无法修改”,本质是“工作表保护 + 单元格锁定 + 权限规则”的组合应用。搞清楚这三者的关系,你就能在任何项目里复现同样的交互。
1. 为什么最终选了 Univer:一次在线表格组件选型复盘
1.1 需求拆解:真正要的不是“编辑器”而是“填表器”
很多企业内部系统里的表格需求,听起来是“做个在线 Excel”,做产品的人脑子里浮现的是微软 Excel 那种全功能编辑器,但真实业务要的往往是一个“填表器”。
我这次的需求很典型:运营团队每天要维护一张任务跟进表,表格里有多列字段,其中表头、基础数据、只读汇总列由管理员维护,普通执行人员只需要填写自己负责的那几行、那几个单元格,比如“今日进度”、“完成情况备注”,其他区域不允许修改。更进一步,这个表格是模板化的,每周都会复制一份新的,填完的数据要回传到业务系统里做汇总统计。
如果直接接一个完整的 Excel 编辑器,会遇到三个问题:
- 所有单元格默认都可编辑,权限控制要自己再包一层。
- 复制粘贴、拖拽填充、撤销重做会绕过很多交互限制。
- 数据回传时机和格式解析要另写一套逻辑。
所以真正的选型诉求是:必须具备可编程的单元格级权限控制,同时保留 Excel 的操作体验,并且能够嵌入式集成。
1.2 候选方案横向对比
我实际调研过的方案包括 SheetJS、Luckysheet、x-spreadsheet,以及最后选中的 Univer。对比结论如下:
| 方案 | 能力定位 | 在线编辑体验 | 权限与锁定能力 | 二次开发友好度 |
|---|---|---|---|---|
| SheetJS | 纯解析和生成 Excel 文件 | 无 UI,只处理数据格式 | 无,需自己实现 | 适合文件转换,不适合在线编辑 |
| x-spreadsheet | 轻量在线表格组件 | 基础单元格编辑,交互较简单 | 缺少系统化的保护机制 | API 简单,但能力边界明显 |
| Luckysheet | 在线表格,功能较完整 | Canvas 渲染,公式/透视表齐全 | 有工作表权限但维护状态不稳定 | 早期很流行,新项目活跃度下降 |
| Univer | 可扩展办公套件 + 表格 SDK | Canvas 自研渲染,交互完善 | 保护/锁定/权限规则体系完整 | 插件化架构,命令系统清晰 |
纯数据场景我会选 SheetJS,它是文件解析领域的常青树;但如果要的是“网页里能填、能锁、能回传”的交互式表格,SheetJS 帮不上忙。x-spreadsheet 做原型验证很快,但真要上生产,权限控制会变成你的噩梦。
Luckysheet 和 Univer 的关系值得说一句:Univer 出自同一批核心开发者的新一代产品,在设计之初就把“可扩展性”和“权限模型”放在了底层,而不是像 Luckysheet 那样靠后期补丁式叠加功能。
1.3 我选 Univer 的三个核心理由
第一,渲染与交互的自研程度高。Univer 用 Canvas 自绘表格区域,DOM 承担的是外壳和定位,因此大数据量滚动时不会有几千个 DOM 节点把浏览器拖垮。对内部系统这种长期开着不关机的页面来说,这点很重要。
第二,操作全部走命令系统,权限可以被拦截。Univer 里用户的每一次编辑,本质上都是向数据模型派发一个命令。这意味着权限控制不只是在 UI 上禁用鼠标键盘,而是可以在命令链路里做校验,这是一个“可编程权限”的架构基础。
第三,模板化的表格复制非常容易。Univer 的工作簿数据是结构化 JSON,模板可以存成配置,每次新建实例时直接注入数据。对“每周复制一份新表”的业务场景来说,这比操作 DOM 再导出要可靠得多。
2. Univer 的底层设计:读懂这几个机制才能做好权限控制
2.1 Canvas 渲染 + 命令系统:界面是“画”出来的,不是“写”出来的
传统网页表格组件通常依赖 DOM 表格元素,单元格越多 DOM 越多,性能下降明显。Univer 走了另一条路:整个表格区域绘制在 Canvas 上,DOM 只承载最外层容器和必要的辅助元素,所有交互通过事件系统捕获,再转换成内部命令。
这个架构的直接好处是:当你要做权限控制时,不会面对几十个可点击的单元格 DOM 节点,而是面对一个明确的命令入口。比如用户键入“A1”,会触发一个编辑命令,经过命令系统处理后在画布上重绘。权限校验就可以挂在这条命令路径上。
坏处也很明显:用传统的前端调试方式,比如在 DOM 里找元素的 class、监听 click 事件,会很不顺手。排查交互问题时,更依赖 Univer 自身的实例方法和控制台输出。
2.2 单元、工作簿、工作表的对象模型
Univer 里有一个核心概念叫 Unit(单元),可以理解为一个可独立运行的文档实例。对表格场景来说,创建的是一个 UniverSheet 类型的 Unit,内部层级是这样的:
Univer 实例 └── UniverSheet Unit(电子表格单元) └── Workbook(工作簿) └── Worksheet(工作表,可以有多个) └── Range / Cell(区域和单元格)理解这个层级对权限控制很重要:你要“锁表头”,其实操作的是某个 Worksheet 的单元格区域;你要“只允许某人填某几列”,操作对象是 Range。所有 API 都是从这个模型出发的。
2.3 单元格数据与保护锁定的关系
这是整篇文章最关键的一个认知。Univer 沿用了 Excel 的“锁定 + 保护”机制:
- 每个单元格的数据里,带有一个锁定标记(protect/lock 相关字段),表示这个单元格是否处于“锁定”状态。
- 工作表有一个独立的“保护开关”,默认是关闭的。
- 只有保护开关打开时,锁定的单元格才真正不可编辑。
- 锁定的单元格还可以配置例外规则,比如“锁定但允许选中”“锁定但允许复制”。
用一个生活化的类比:单元格的锁定标记相当于每扇门自己的锁,工作表保护是整栋楼的安保总闸。总闸不开,门锁形同虚设;总闸开了,锁着的门才进不去;例外规则相当于给某些人发了特定房间的钥匙卡。
2.4 协同与权限点的机制
Univer 本身内置了协同编辑能力,多人同时打开一份表格时,每个客户端的操作会同步到其他端。配合协同机制,权限控制会细化为两个层面:
- 数据层:直接不允许发变更命令。
- 体验层:界面上不允许编辑,并给出明确提示。
Univer 实现了基于“权限点”的机制,可以针对整个文档、单个工作表、或者某个区域授予或收回不同权限。我这次实战主要用到的是数据层的保护锁定,如果你的业务需要自定义规则,走权限点会更灵活。
3. 快速接入:从 npm 安装到一个可编辑的表格
3.1 最小安装
Univer 的包发布在 npm 上,核心包是@univerjs/presets,它会帮你聚合好表格能力相关的模块。安装命令如下:
npm install @univerjs/presets如果你的项目需要用到具体的高级能力,比如公式计算、条件格式、筛选,再按需装对应的包。我建议不要一次性全量安装,Univer 的插件化设计就是希望你在用到的功能上做聚合,避免主包体积虚高。
3.2 初始化一个带数据的工作簿
安装完之后,先创建一个带初始数据的表格。下面是最简启动代码,我用的版本是 0.2.x 左右的接口,如果你看到的方法名略有出入,以你安装版本的 TypeScript 类型定义为准:
import { Univer, UniverInstanceType } from '@univerjs/presets'; // 创建 Univer 实例,这是整个表格应用的总入口 const univer = new Univer(); // 创建一个电子表格单元,并注入初始数据 const unitId = univer.createUnit(UniverInstanceType.UniverSheet, { data: [ ['任务名称', '负责人', '今日进度', '备注'], ['需求评审', '张三', '已完成', ''], ['UI 走查', '李四', '进行中', ''], ], }); // 拿到当前工作簿实例 const workbook = univer.getUnit(unitId);这段代码执行后,页面上已经能渲染出一个可编辑的表格。Univer 会自动生成一个默认工作表,data 数组的第一行会作为表头数据写入,但要注意,此时这些单元格默认都可以编辑。
3.3 在 React 项目里的挂载方式
如果你的前端框架是 React,核心思路是:用一个容器 ref 接收 Univer 挂载后的 DOM,并且保证只在组件挂载时初始化一次,避免重复创建实例。
import { useEffect, useRef } from 'react'; import { Univer, UniverInstanceType } from '@univerjs/presets'; export function ExcelTemplate() { const containerRef = useRef<HTMLDivElement>(null); const univerRef = useRef<Univer | null>(null); useEffect(() => { if (!containerRef.current) return; const univer = new Univer(); univerRef.current = univer; const unitId = univer.createUnit(UniverInstanceType.UniverSheet, { data: [['任务名称', '负责人', '今日进度']], }); return () => { univerRef.current?.dispose(); }; }, []); return <div ref={containerRef} style={{ width: '100%', height: 600 }} />; }这里有一个容易忽略的点:容器必须有明确的高度,否则 Canvas 渲染不出来。官方 Demo 里一般会设置 600px 或全屏高度,实际嵌在后台系统里时,我建议给容器一个 flex: 1 的 CSS 样式,让它随父容器自适应。
3.4 先用官方在线 Demo 验证交互
如果不确定某些交互表现是否符合预期,建议先去官方仓库的在线体验页试一下。Univer 官方提供了可以直接打开的线上 Demo,在里面可以验证:单元格选中、拖拽填充、右键菜单、撤销重做、公式计算等基础行为。
我当时的做法是,先在在线 Demo 里手动模拟“填表”流程,确认无卡顿,再回到本地工程里接代码。另外,Univer 在控制台暴露了实例对象,你可以在浏览器 Console 里直接打印 workbook、worksheet、单元格数据,这种调试路径比对着文档猜 API 高效很多。
4. 核心实战:只允许用户填写指定单元格
这一节是全文重点。回到最初的需求:管理员定义表格,普通用户只能填“今日进度”这类指定单元格,其他单元格不可修改。
4.1 先弄清“锁定 + 保护”的组合逻辑
在 Univer 里,单元格是否可编辑,取决于两个条件同时满足:
- 单元格本身处于“未锁定”状态。
- 工作表保护处于开启状态。
如果你只设置了单元格锁定,但不开启工作表保护,那锁定完全无效;反过来,工作表保护开启了,但单元格没有锁定,那它依然是可编辑的。
所以你的默认策略应该是:把表格里所有单元格都设为锁定,再把允许用户填写的单元格设为不锁定,最后开启工作表保护。
这个逻辑和 Excel 的设计一致,一旦想通,后面所有代码都是顺理成章。
4.2 模板准备:默认全部锁定,再开放特定区域
先用代码把模板数据准备好。假设允许用户填写的区域是 D 列(“今日进度”)从第 2 行到第 100 行,其他区域全部锁定:
import { Univer, UniverInstanceType } from '@univerjs/presets'; const univer = new Univer(); const unitId = univer.createUnit(UniverInstanceType.UniverSheet, { data: [], }); const workbook = univer.getUnit(unitId); const worksheet = workbook.getActiveSheet(); // 第一步:将整个表格区域默认设为锁定 worksheet.getRange(0, 0, 200, 10).setStyle({ protect: { lock: true }, }); // 第二步:将允许填写的 D 列(索引为 3)第 2 行到第 100 行取消锁定 worksheet.getRange(1, 3, 99, 1).setStyle({ protect: { lock: false }, });这里用getRange(row, column, rows, columns)这种方式定位区域,参数分别是起始行、起始列、行数、列数。要注意行列索引从 0 开始,所以第 2 行的索引是 1,D 列的索引是 3。
提示:不同版本对单元格锁定字段的命名可能不一样,有的版本是
protect.lock,有的版本在cellData里用p.lock表达。建议打开 TypeScript 类型定义确认一下,不要硬背方法名。
4.3 开启工作表保护并配置例外
单元格锁定状态设置好之后,开启工作表保护:
worksheet.protect({ lock: true, password: undefined, // 可配置一些例外行为 options: { allowSelectLockedCells: true, allowSelectUnlockedCells: true, allowFormatCells: false, allowDeleteRows: false, allowInsertRows: false, }, });开启保护后,界面上锁定单元格会变成只读状态,用户点击时会发现无法输入内容。这已经能满足“只能填指定单元格”的核心诉求。
我还建议保留allowSelectLockedCells: true,这样用户虽然不能修改锁定单元格,但可以选中它们查看内容,符合填表场景下“我需要看表头但是不能改表头”的使用习惯。如果设成 false,用户连点击锁定单元格都会被拦截,体验反而奇怪。
4.4 用权限规则实现的另一种思路
保护锁定的方案适合“静态模板”,如果你的业务规则是动态的,比如“用户在提交之前可以反复修改,提交之后所有单元格都锁定”,单纯靠锁定标记就不够用了,需要配合 Univer 的权限点机制。
思路大致是:在命令系统里拦截编辑类的命令,根据自己的业务状态判断当前是否允许写入目标区域。逻辑分两步:
- 监听用户的编辑动作。
- 判断目标单元格是否在允许区域内,以及当前业务状态是否允许编辑。
这里不贴太长代码,核心是理解:保护锁定解决的是“区域能不能编辑”的静态问题,权限点机制解决的是“什么时候能编辑”的动态问题。生产环境里复杂填表需求往往是两者配合使用。
4.5 体验细节:高亮提示可填写区
这里补充一个不涉及源码但很影响使用体验的细节。数据录入员面对一整张大表格时,其实不知道哪些格子可以填。只做锁定保护,用户只能通过“试着点一点”来判断,这很低效。
我的做法是在模板里给可编辑区域加醒目的底色:
worksheet.getRange(1, 3, 99, 1).setStyle({ background: '#fffbe6', protect: { lock: false }, });这样一来,用户打开表就知道黄色区域是自己需要填的地方,配合表头上方的说明文字,几乎不需要再写操作指南。这个细节在试运行阶段反馈尤其好。
5. 实测中容易踩的坑与绕过办法
5.1 锁定不生效:忘了开启工作表保护
我在第一次联调时就踩了这个坑。当时只设置了单元格的锁定标记,没有开启 worksheet 的 protect 方法,结果界面上所有单元格都可以编辑。排查过程其实很简单:先在控制台打印出 worksheet 的保护状态,发现lock是 false,再回头补上保护逻辑。
如果你也遇到“锁了但没锁住”的情况,优先检查两点:
- 工作表保护是否开启。
- 目标单元格的锁定标记是否真的是
true,因为有些批量设置方法可能把样式写到了错误的行列范围。
5.2 复制粘贴绕过锁定
单元格锁定只能限制“直接键入”,但拦截不了“复制粘贴”的默认行为。用户完全可以从其他表格里复制一段数据,粘贴到锁定区域里,这在某些浏览器版本下会直接覆盖掉只读单元格的内容。
我当时实测下来,Chrome 里粘贴行为会触发粘贴命令,但不同版本行为不一致。最稳妥的做法是在命令链路层校验,禁止对锁定区域的粘贴覆盖。可以监听粘贴命令,判断目标选区是否包含已锁定的单元格,如果包含就阻止执行并弹出提示。
这个坑比较隐蔽,因为你在受控测试时往往不会想到去复制粘贴,但真实用户一定会这么干。
5.3 撤销重做会把保护状态一起回滚
另一个让我头疼的问题是撤销重做。在开启工作表保护的情况下,如果用户先做了一次编辑,然后按 Ctrl+Z 撤销,某些操作链路会把保护状态也一并回滚,导致整个工作表保护被意外关闭。
这不是必现问题,但一旦出现,权限就会在不知不觉中失效,比直接绕开锁定更危险。排查方向是查看撤销命令是否包含设置保护状态的记录。我的临时解法是加了一个定时状态校验器,每隔几秒检查一次工作表保护状态,如果发现被关闭就重新开启,并用控制台日志记录触发时刻。后续可以继续研究命令滤网,把保护状态变更从撤销链中剔除。
5.4 不要只信任前端权限
最后这条算不上 Univer 的坑,而是所有前端表格类项目共同的认知问题:前端权限只是交互限制,不是安全边界。
用户可以通过浏览器控制台直接调用 Univer 实例的方法来改写单元格数据,哪怕你把界面的选区和右键菜单全部锁死,也拦不住专业用户用脚本来操作。所以凡是填写后需要入库的数据,后端接口必须再次校验。表格组件在前端负责体验,后端负责正确性,这条原则不要动摇。
6. 从模板表格到真实业务:我的落地经验与后续扩展
6.1 模板配置的下发与版本管理
落地的第一步是把模板配置和权限配置抽离成独立 JSON。不要把模板写死在代码里,否则每周复制一张新表时会非常痛苦。
我建的模板数据结构大概是:
{ "version": "2025-06-W1", "sheetName": "运营任务周表", "columns": ["任务名称", "负责人", "今日进度", "备注"], "editableRange": { "startRow": 1, "startCol": 3, "endRow": 99, "endCol": 3 }, "protected": true }后端接口返回这个 JSON,前端拿到后循环创建 Univer 工作簿。这样产品经理改模板字段时,只需要更新配置,不用改代码,也不用发版。版本号记录在字段里,方便追溯某周的数据对应的是哪版模板。
6.2 填写后数据回读与解析
用户填写完毕,前端需要把表格数据回传到业务系统。Univer 可以一次性读取整个工作表的数据:
const data = worksheet.getRange(0, 0, 100, 10).getValues();这个data是一个二维数组,每一行是一个普通数组,对应一行的所有单元格值。回传前要做几件小事:
- 把空字符串统一转成 null,避免后端把“空值”和“未填”混淆。
- 过滤掉完全没有数据内容的行。
- 对日期、数字这类数据类型做一次格式化,Univer 单元格里存的可能是对象而不是纯字符串。
回传时机上,我建议提供“保存”按钮让用户主动提交,而不是实时上传。实时上传看起来时髦,但在填表场景里会让用户有心理压力,而且会产生大量无意义的中间版本。
6.3 后续扩展:公式联动、数据校验、打印导出
模板表格上线后,我陆续收到了几个合理的扩展需求,也给后来者提个醒:
- 公式联动:比如“完成率”列是通过今日进度和其他指标自动计算出来的,这类单元格应该锁定,由公式驱动而不是由用户手填。Univer 支持公式写入单元格,前提是设置了
formula字段。 - 数据校验:比如“今日进度”只允许填“未开始、进行中、已完成”三个值,可以在前端下拉选择,也可以在提交时校验。Univer 有数据校验相关方向的能力,如果版本支持,直接用。
- 导出文件:很多业务方还是习惯要一份 Excel 文件存档。Univer 生态里可以接入导出能力,生成真正的 .xlsx 文件交给用户归档,而不是让他们截图。
这些扩展有一个共同点:都是在数据模型层面做文章,不需要改动渲染层。这也是我把表格能力押注在 Univer 上的信心来源——架构上的松耦合决定了后续迭代的上限。
如果让我重新选一次,我依然会选 Univer。这个选择不是因为它的功能列表最齐全,而是因为它的“锁定 + 保护 + 权限命令链路”设计,恰好覆盖了填表类场景从静态模板到动态权限的全部想象空间。你在接入时,不必追求一次性把官方所有能力都配上,从最小可用版本开始,先把“锁表头、放开填写区”这条主链路跑通,再逐步往上面加公式、校验和导出,每一步都有清晰的扩展接口等着你。