做一个在线填表功能,模板制作者把一张Excel发给我,要求是:用户能填“姓名、电话、备注”这几列,但“评分、审核意见”这些列绝不能动。一开始我以为就是套几个input框,真正做完才发现,从单元格锁定、工作表保护到权限控制,每一步都有坑。后来换成了univer这个开源在线表格方案,事情才变得可控。
这篇文章不写PPT式的功能介绍,只聊两件事:univer能拿来做什么,以及怎么把一个“用户只能填指定单元格,其余锁定只读”的真实场景落地。适合正在做B端表单、数据填报、后台表格引擎的项目负责人和前端开发。
1. 我为什么关注Univer:开源表格组件这片市场已经散得太久了
1.1 以往要在线表格,基本只有两条路:缝缝补补和重金采购
做在线表格的人都知道,HTML table只适合看,不适合编辑。真正要支持录入、复制粘贴、公式、合并单元格,几乎都得自研或者用现成的表格组件。
早期我用过一些纯前端方案,它们的特点是“表面像表格,内里还是一堆DOM”。单元格一多,滚动卡顿;Excel文件导进来,样式丢大半;想锁定某些区域,要么自己算坐标,要么在事件里拦截。维护成本非常高,而且每加一个需求,就得动一遍渲染层。
另一条路是采购商业表格控件。功能确实全,但单价高,而且很多核心能力是黑盒,遇到问题只能提工单。最难受的是授权模式,一个项目套一个授权,做成SaaS产品后每个租户都要算一遍License。长期来看,这笔账并不便宜。
univer出现在这个节点上,等于给中间地带补了一个选项:开源、可嵌入、组件化,而且不是那种只做“单元格展示”的玩具,而是把公式、样式、多表格、数据验证这些引擎能力做了进去。
1.2 Univer的核心思路:引擎与UI彻底解耦,而不是又一个组件库
我第一次看Univer架构时,最大的感受是它不像“插件”,更像“框架”。Univer把一套完整的办公套件内核(文档、表格、幻灯片)抽离出来,渲染层用Canvas重绘,业务逻辑放在独立插件包里,跟宿主框架完全无关。
这意味着什么?你在React项目里可以嵌入,在Vue项目里可以嵌入,哪怕只是用原生JS写一个静态页面,也可以嵌入。它不绑定你现有的技术栈,这是很多同类组件做不到的。
Univer的数据模型也接近Excel的语义:工作簿、工作表、单元格、范围、样式、公式,都是结构化对象。前端拿到的是数据,而不是一堆乱七八糟的DOM节点。这为后面做“指定单元格锁定”提供了很好的基础,因为锁定的前提是:我能精确描述任何一个单元格的位置和状态,而不是靠UI状态去硬记。
1.3 和Luckysheet、Handsontable、SpreadJS放在一起看
| 维度 | Univer | Luckysheet | Handsontable | SpreadJS |
|---|---|---|---|---|
| 开源/免费 | Apache-2.0,核心代码开源 | MIT,但项目已明显放缓 | 开源版GPL,商用需购买授权 | 纯商业授权 |
| 框架绑定 | 不绑定,React/Vue/原生均可 | 偏向原生和Vue封装 | React封装成熟 | 依赖自家体系较深 |
| 渲染方式 | Canvas渲染,支撑大数据量 | Canvas+部分DOM | DOM渲染为主 | Canvas,偏重传统桌面体验 |
| 公式能力 | 内置较多函数,兼容Excel语义 | 公式能力一般 | 有公式但偏基础 | 公式很强,毕竟是老牌商业控件 |
| 学习与定制成本 | 插件机制清晰,适合二次封装 | 改渲染层很吃力 | 改交互容易,重逻辑费劲 | 定制只能走官方API |
坦白说,没有谁是完美的。Univer的优势在于它的底子更现代,插件化方式对开发者更友好;缺点也很明显,版本迭代快,API变动频繁,网上能找到的中文资料深度不够。这就更需要把实践步骤和坑记录下来。
2. 上手跑通:用Vite加TypeScript把Univer嵌进自己的页面
2.1 最小工程怎么建
我建议用Vite加TypeScript起步,别一开始就引入复杂脚手架。原因很简单:Univer是典型的前端重逻辑库,TS类型能帮你提前发现很多API误用,Vite的按需编译也能让开发期反馈更快。
npm create vite@latest univer-demo -- --template vue-ts cd univer-demo npm install这里我用的是Vue模板,如果你用React,思路完全一致,Univer不挑框架。安装Univer时,直接引入官方预设会更省事,它会把表格、公式、UI这一层都组装好。
npm install @univerjs/presets @univerjs/core2.2 写一个能正常渲染的入口
Univer的预设方案给了一个很友好的入口:createUniverBasic。它会自动注册工作簿所需的插件,我们只需要把容器节点传进去。
import { createUniverBasic } from '@univerjs/presets'; import { LocaleType } from '@univerjs/core'; import '@univerjs/presets/lib/styles.css'; const univer = createUniverBasic({ container: document.getElementById('app') as HTMLDivElement, locale: LocaleType.ZH_CN, });这一步跑通后,页面上会出现一个完整的在线表格:工具栏、工作表标签、选区、公式编辑栏都有。如果你用的是不同类型号,记住你的版本里偏细节的API可能有出入,但只要沿着预设方法走,大部分情况不会太离谱。
2.3 把自定义的模板数据塞进表格
创建空表格只是第一步,实际业务需要“打开”一个可编辑模板。Univer加载数据的方式是把工作簿数据对象传给实例创建,直观理解就是把一个JSON结构丢给表格引擎。
const univer = createUniverBasic({ container: document.getElementById('app') as HTMLDivElement, locale: LocaleType.ZH_CN, }); univer.createUniverSheet({ sheets: [ { name: '登记表', rowCount: 50, columnCount: 10, cellData: { 0: { 0: { v: '姓名' }, 1: { v: '电话' }, 2: { v: '备注' }, 3: { v: '评分(只读)' }, }, 1: { 0: { v: '' }, 1: { v: '' }, 2: { v: '' }, 3: { v: '90' }, }, }, }, ], });这里的cellData结构就是行号对应列号,单元格内容是{ v: 值 }。注意,cellData是Univer内部最常打交道的数据结构,后面做锁定、样式修改、数据回填都会围绕它展开。
写这一行代码时,我建议顺手把版本固定下来。Univer目前还处于快速迭代阶段,今天能用的API,下个小版本可能就改了名字。我的习惯是锁版本号,升级时专门看Changelog,而不是等着意外打破。
3. 落地“用户填指定单元格,其余锁定只读”的整个流程
3.1 先把锁定和保护这两个概念弄清楚
很多人在这一步栽跟头,是因为分不清“锁定单元格”和“保护工作表”。简单说:
- 所有单元格默认都处于“锁定”状态,但这个状态在没有保护机制时不起作用,用户依然能编辑。
- 只有当你“保护工作表”之后,锁定的单元格才真正变成只读,那些被设置成“取消锁定”的单元格依然允许填写。
这就解释了为什么有些人设置了半天,发现所有单元格还是能改:他只是改了单元格的锁定属性,但根本没用文件保护去激活它。
我把这个机制做成了一张直观的逻辑对照:
| 单元格锁定状态 | 工作表保护状态 | 用户实际效果 |
|---|---|---|
| 默认锁定 | 未保护 | 可编辑 |
| 手动取消锁定 | 未保护 | 可编辑 |
| 默认锁定 | 已保护 | 只读 |
| 手动取消锁定 | 已保护 | 可编辑 |
这个设计其实是传统的Excel保护模型,Univer继承了同一套语义。理解了它,你就能处理90%的“可填不可改”需求。
3.2 用UI快速做一个“可填写区域”模板
先别急着写代码,用Univer自带的界面把流程走一遍,你会对锁定机制更有体感。
- 选中允许用户填写的区域,比如A2:C20。
- 在单元格格式设置里找到“保护”相关选项,把“锁定”取消勾选。
- 右键点击左下角的工作表标签,选择“保护工作表”。
- 确认保护范围,保存。
做完之后,用鼠标点击刚才取消锁定的区域,可以正常输入;点击其他单元格,会提示该单元格已只读。这个交互就已经满足了“用户填一些单元格,其他单元格无法修改”的原始需求。
但UI操作适合演示,真实项目必须走API。因为模板是后端下发的,每次生成的区域可能不同,我们不能指望管理员手动去点。
3.3 用API控制工作表的保护状态
在Univer中,修改保护状态最核心的是SetWorksheetProtectionMutation这一个命令。它的作用是设置某个sheet的保护配置,包括是否开启保护、哪些操作被禁止。
import type { Workbook } from '@univerjs/core'; import { SheetExtension } from '@univerjs/sheets'; async function enableSheetProtection(univerInstance: any, unitId: string, sheetId: string) { await univerInstance.getCommandService().executeCommand( 'sheet.mutation.set-worksheet-protection', { unitId, subUnitId: sheetId, protection: { protect: true, permission: { select: true, edit: false, }, }, } ); }这段代码表示:开启保护,允许选区操作,禁止编辑。Univer的权限还支持配置是否允许排序、筛选、插入行列等等,但对我们这个场景,核心就是edit: false。
注意,Univer不同小版本对命令ID和参数结构的命名可能微调。如果你在自己的工程里执行后没有生效,最有效的排查办法是打开浏览器控制台,手动用Univer的UI开启一次保护,观察Network面板或调试信息里出现了什么命令,再用你实际的命令ID去替换。
另外,Univer的编辑器在“锁定”这个细节上,需要预先保证需要填写的区域是“取消锁定”状态。API层面,如果你的模板数据已经通过JSON加载,可以在初始化数据时直接把可编辑区域的locked属性设为 false。比如某个单元格:
cellData: { 1: { 0: { v: '', locked: false }, }, }不过要提醒的是,在Univer当前版本中,锁定属性更多是配合保护命令生效的。只在样式层设置了locked而没开启保护,用户依然能编辑。两者缺一不可。
3.4 不同登录用户看到不同可编辑区域
项目越做越深时,你会发现“用户”不是一个统一的群体。A角色只能填A列,B角色只能填B列,这是最常见的权限需求。
我目前的做法是用Univer的“多工作表”能力配合保护策略:给不同的角色准备不同的工作表,每张表只解锁该角色负责的区域,其余区域全部锁定。用户登录后,根据角色动态加载对应的工作簿数据,或者隐藏掉不该看的工作表。
更细的颗粒度可以做到单元格级权限,但那通常需要组合权限服务和后端鉴权,复杂度会明显上升。我的建议是:先评估业务是否真的需要单元格级权限,如果只需要“某些列可编辑”,用多sheet方案最稳妥,性能也更好。
3.5 别忽略服务端的校验边界
这里必须泼一盆冷水:Univer做的“锁定”是前端交互层面的保护。它让普通用户无法在界面上修改,但它挡不住懂技术的人绕过前端直接调用后端接口,往被锁定的单元格写数据。
所以,严格意义上说,Univer负责的是“体验和约束”,服务端必须在接收数据时再次校验:哪些单元格允许被这个用户修改,哪些字段必须原样返回。这就像门锁,它防的是顺手推门的人,不是防着破墙而入的人。
如果项目对数据一致性要求很高,建议后端保存模板定义时,同时记录“可编辑区域清单”,提交数据时逐格校验。别把信任完全交给前端表格组件。
4. 实际接入Univer时踩过的几个坑
4.1 版本之间API漂移,远比想象中严重
最典型的是:上个月还能用的executeCommand参数,升级一个小版本后,命令ID直接变了。Univer把命令分成了交互命令和基础命令,不同插件包可能导出不同的常量,如果你按旧版文章写死字符串,就会在运行时报错。
我现在的对策是:把Univer相关调用尽量收敛到一个独立模块里,对外只暴露enableProtection(sheetId)、disableProtection()、setCellData()这类业务方法。以后换API,只改这一个模块,不至于全项目搜字符串。
这算不算过度设计?以Univer目前的速度看,完全有必要。
4.2 设置“取消锁定”时,误把整行样式重置
有一次我在初始化数据时把一整行的单元格样式统一设置,结果覆盖了之前单独处理的锁定状态。代码只设置了字体加粗,但因为传入的样式对象不完整,Univer把这一行所有单元格的 locked 属性都重置成了默认值。保护开启后,原本开放的填写区域也变得不可编辑。
排查了很久才定位到:单元格样式是完整覆盖的,更新时没有合并旧的locked属性。后续所有类似操作,我都会先读取单元格当前样式,再合并新属性,避免踩踏:
const currentStyle = cell.getStyle(); cellData[row][col] = { v: value, s: { ...currentStyle, locked: false }, };4.3 大数据量加载时首屏白屏,渲染配置被忽略
表格行数多、样式也多的时候,Univer渲染压力会变大。我试过一次性加载几百行带边框、底色、公式的模板,首屏等待时间明显变长。
Univer的Canvas渲染虽然在滚动性能上比DOM方案强很多,但初始化数据量仍然需要控制。实际做法是:先加载用户可见区域的数据,其余行列延后填充;涉及超大数据量时,关闭网格线的选项也有一定收益。这不是Univer的bug,而是所有表格引擎的正常权衡。
4.4 样式表忘记引入,工具栏成了一片空白
这是个很低级但很常见的错误。只安装了JS包,没引@univerjs/presets/lib/styles.css,页面会出现一个功能正常但没有任何UI样式的“裸表格”。所有图标位置都是空白,按钮点了有效果但看不见。
排查方法也比较直接:打开控制台看有没有加载CSS文件的404。Univer的样式体系是独立打包的,不同入口需要对应不同的样式文件,这一条基本次次踩,记住了就不用再浪费时间。
5. 从嵌入到在线协同:Univer到底能做多深
5.1 起初的只读与锁定,只是Univer能力里的很小一块
如果你只把Univer当成“单元格只读控件”,确实有些大材小用。它本身支持公式计算、条件格式、数据验证、多级撤销、打印等,这些能力对做表单和业务系统是实打实的加分项。
比如数据验证,可以在用户填错格式时直接拦截;公式可以自动算出某些统计字段。配合保护机制,公式列设为锁定,用户只能填源数据,计算结果自动更新,这个体验比后端穷举计算要舒服太多。
5.2 真正的多人协同需要更谨慎的选型
很多人看中Univer就是冲着“在线协同”来的。但要注意,开源版默认更多承担的是客户端渲染和单机编辑,真正要支持多人同时修改同一张表,需要额外的协同服务端支撑,官方把协同能力放在了SDK版本里。
如果你的项目确实需要多人在线实时编辑,不能只装一个开源包就指望搞定。建议提前跟官方确认授权方式和接入成本。如果是内部工具或者面向少量用户,先做单机编辑加手动刷新,也足够扛过MVP阶段。
5.3 我对Univer的未来看法与当前使用建议
我的整体判断是:Univer已经跳出了“又一个表格组件”的层次,它在把整个办公套件的内核做成开源基础设施。短期看,API不稳定是最大的隐忧;长期看,这反而是社区生态活跃的表现。
对想直接用的人来说,我的建议是:锁定版本,做好封装,把模板数据、权限规则和UI交互解耦。不要追求用上每一个新功能,先把你最需要的“指定单元格填写、其余锁定”跑稳。等版本趋于稳定,再考虑向协同、公式编排这些深水区扩展。
这不仅是技术选型,也是项目掌控力的问题。工具再强,也不如把边界守清楚。