
如果你要在自己的产品里嵌入一个在线表格并且希望它像 Excel 一样能编辑、能美化但又想控制用户只能填某些格子、其他格子全都锁定那我强烈建议你认真看一下 Univer。它是一个用 TypeScript 写的开源电子表格基础设施相当于一个你可以完全自己部署、自己定制、自己扩展的在线 Excel。这篇文章从一个真实需求出发——做一个带模板性质的在线填报表格用户只能填写我指定的单元格其余区域一律不可修改——完整记录我如何用 Univer 把这个功能落地包括权限保护、数据验证、导入导出以及协作场景下的各种坑和心得。全文不吹概念、不堆术语只讲我怎么想的、怎么做的、踩了什么坑以及代码可以怎么抄。适合正在选型前端表格引擎、或者已经在用 Univer 但还没搞明白权限体系的朋友。1. 为什么用 Univer 做“可填写模板表格”1.1 这个需求到底在说什么先把需求拆开我需要做一个在线表格表格的列头、标题、公式、样式都是固定的普通用户进来以后只能往某些空白单元格里填数据其他单元格包括合并单元格、标题行、公式列统统不能动。以前这种需求特别常见于人力资源信息采集、供应链报价单、项目进度填报这类场景本质上就是“把纸质表单变成线上模板”。实现路径上一般有两种思路。一种是完全自己画 UI用 input 模拟单元格这种做起来控制力最强但一旦数据量上来、需要公式和样式排版开发成本会翻好几倍。另一种是找一个现成的表格组件通过配置项锁定单元格。Univer 走的就是第二条路而且它把“锁定单元格”这个能力做成了权限体系而不是简单的只读开关这点很关键。1.2 对比了一圈开源表格引擎后我选了它我在选型时比过四个方案Luckysheet、Handsontable、x-spreadsheet、Univer。直接说结论直到今天如果你想完全开源、免费、可商用、并且原生支持在线协作这四个里头只有 Univer 能站得住。方案协议技术栈公式/样式数据导入导出单元格权限实时协作二次开发友好度UniverApache 2.0TypeScript / Canvas支持支持支持权限点原生支持高插件机制LuckysheetMITJS / jQuery支持支持有限需额外搭建中维护速度一般Handsontable商用/需授权JS部分支持部分支持支持无原生中协议受限x-spreadsheetMITJS基础有限无无低功能简单我重点说下 Handsontable它确实是个很成熟的组件权限控制也好做但商用授权费用不低。如果你只是个人项目无所谓一旦是公司产品商业化要么付费要么换方案。Luckysheet 早期很火社区基础好但它底层依赖 jQuery在 React/Vue 里集成总感觉有一层隔靴搔痒而且它的在线协作能力不像 Univer 那样是从架构上设计进去的。Univer 给我的感觉是“一开始就按产品级标准做基础设施”它不止有 Sheet还有 Doc 和 Slide所以我只需要学习一套数据模型和命令机制以后扩展别的不怕。1.3 Univer 的架构优势在哪里Univer 的架构核心是插件化。它把数据模型、渲染引擎、UI 组件、命令系统拆成了不同的包比如univerjs/core管数据univerjs/engine-render管 Canvas 渲染univerjs/ui管界面布局。这意味着你可以只加载需要的模块也可以把表格嵌入到自己的产品 UI 里而不是被它的框架反客为主。另一个让我选择它的理由是“命令机制”。Univer 的所有操作都走命令包括用户点击、API 调用、远程协作同步都会变成一条条命令。这种设计看起来繁琐但好处是权限校验可以在命令层统一做。也就是说用户在界面上点击、或者第三方调 API最终都会过同一套逻辑不会出现界面锁死了但 API 还能改的漏洞。这也是我后面做单元格锁定最看重的一点。2. 上手前的关键认知先搞懂 Univer 的对象模型2.1 最小化实例化一个 sheet 就够在动手写权限之前先把 Univer 跑起来。目前官方推荐的快速接入方式是用univerjs/presets这个包把常用模块打包成了一个预设。我用的是 Vite React TypeScript 的项目初始化核心代码只有这么点npm create vitelatest univer-form -- --template react-ts cd univer-form npm install univerjs/presets然后修改src/App.tsximport { useEffect, useRef } from react; import { Univer, UniverPresetSheet } from univerjs/presets; import univerjs/presets/lib/styles/index.css; export default function App() { const containerRef useRefHTMLDivElement(null); useEffect(() { const univer new Univer({ theme: default, presets: [ UniverPresetSheet({ container: containerRef.current!, header: true, toolbar: true, formulaBar: true, statusbar: true, footer: true, palette: true, }), ], }); return () { univer.dispose(); }; }, []); return div ref{containerRef} style{{ width: 100%, height: 100vh }} /; }跑起来之后你会看到一个基本完整的在线表格界面有工具栏、公式栏、行列表头、底部标签页。这里要特别留个心container传入的 DOM 元素必须有明确的宽度和高度否则 Canvas 渲染出来是零尺寸页面白屏。我第一次接的时候就是容器高度没设置结果表格区域直接“消失”了。2.2 必懂概念Unit / Workbook / Sheet / Range / CellUniver 的数据模型不复杂但命名需要先对齐。最顶层叫 Univer 实例一个实例可以包含多个 Unit每个 Unit 对应一个文档类型比如 Sheet Unit 就是一个工作簿。工作簿下面再挂多个 Worksheet也就是我们常说的 Sheet 标签页。每一个 Sheet 里单元格用 Cell 表示多个单元格组成 Range 区域。如果你只要做一个单页的填报表格那结构基本就是一个 Univer 实例 一个 Sheet Unit 一个 Worksheet。关键操作用到的 API 基本都是围绕Workbook和Worksheet这两个对象展开的。比如我后面的权限控制就是先拿到某个 Worksheet再对它做保护配置。创建一个带初始数据的 WorkBook常见做法是通过初始化时传入workbookData它的结构大概是这样的const univer new Univer({ theme: default, presets: [ UniverPresetSheet({ container: app, header: true, toolbar: true, workbookData: { id: workbook-01, sheets: { sheet-01: { id: sheet-01, name: 员工信息填报, rowCount: 100, columnCount: 26, cellData: { 0: { 0: { v: 姓名, t: STRING }, 1: { v: 工号, t: STRING }, 2: { v: 部门, t: STRING }, 3: { v: 入职时间, t: STRING }, }, }, }, }, }, }), ], });这里的v是单元格值t是单元格类型STRING是字符串类型。Univer 的数据类型枚举还有NUMBER、BOOLEAN、FORCE_STRING等。模板的列头我就是这样写死的。2.3 命令、变更与服务Univer 的“一切皆命令”刚接触 Univer 时最让我不习惯的是所有修改都要通过命令去执行而不是直接改对象属性。比如改一个单元格的值不是sheet.setValue(...)这种简洁调用而是要执行SetRangeValuesMutation这条命令import { ICommandService, SetRangeValuesMutation, CellValueType } from univerjs/core; const commandService univer.getCommandService(); await commandService.executeCommand(SetRangeValuesMutation.id, { unitId: workbook-01, sheetId: sheet-01, range: { startRow: 1, startColumn: 0, endRow: 10, endColumn: 3 }, value: { 1: { 0: { v: 张三, t: CellValueType.STRING } }, }, });这是初始模板填充数据的方式。我一开始也觉得绕但用熟了以后体会到它的价值命令是“可记录、可撤销、可拦截”的。权限系统完全可以监听或拦截某些命令比对用户是否有权限执行某一类操作。这种设计对在线协作尤其重要因为所有人的操作都要通过命令合并成操作流再同步给其他客户端。没有这套机制想做协作和权限就是一团乱麻。3. 实现“其它单元格不能改”权限保护的完整实操3.1 权限分层工作簿/工作表/区域三级保护Univer 的权限体系是我觉得它最值钱的地方。它把保护分成了几个层级工作簿级、工作表级、区域级。工作簿级覆盖所有 Sheet比如禁止所有人插入新的 Sheet、禁止调整工作表标签顺序工作表级针对某个 Sheet比如禁止编辑整个 Sheet 的单元格区域级则是在工作表上划出特定范围单独设置规则。如果我直接在整张表上开启“禁止编辑”那用户连模板区域带可填区域全锁死了这不行。所以正确思路是反过来先把整张工作表锁定再在需要填写的位置加上区域保护例外规则允许用户编辑这些区域。这有点像用编辑器里的“只读模式 局部白名单”。Univer 的包名在不同版本里略有变化总体是univerjs/sheets-protection或univerjs/protection这类模块。核心 API 围绕“规则 权限点”展开权限点就是具体你能做什么、不能做什么。比如SetCellValuesPermissionPoint控制能不能改单元格值SetCellStylesPermissionPoint控制能不能改样式InsertRowPermissionPoint控制能不能插入行。以我自己用的版本为例伪代码如下import { SetCellValuesPermissionPoint, InsertRowPermissionPoint, DeleteRowPermissionPoint, EditContentPermissionPoint, } from univerjs/sheets-protection; // 先拿到当前 Sheet const workbook univerAPI.getActiveWorkbook(); const sheet workbook.getActiveSheet(); // 设置工作表保护拒绝所有编辑操作 sheet.getPermission().setPermissionPoint(new EditContentPermissionPoint(false)); // 为可填区域设置区域保护规则覆盖默认的“拒绝”策略 sheet.addRangeProtection({ name: 可填写区域, ranges: [{ startRow: 1, startColumn: 0, endRow: 10, endColumn: 3 }], permissionPoints: { [EditContentPermissionPoint.id]: true, [SetCellValuesPermissionPoint.id]: true, [SetCellStylesPermissionPoint.id]: true, }, });这段代码的意思非常直白整个 Sheet 默认不能编辑但第 1 行到第 10 行、A 到 D 列的范围是白名单允许改值和样式。注意不同 Univer 版本的方法名可能从addRangeProtection变成addRangeRule或者统一的SheetPermission服务提供具体以你 npm 包里实际导出的类型为准但思路是通用的。3.2 前端直接锁死保护整个表再解锁可填区域有人可能会问如果我把所有单元格锁死但前端可填区域也允许编辑用户能不能绕过前端直接通过浏览器控制台改数据答案是能纯前端锁定本来就是体验层面的限制真正防篡改必须依赖后端校验。但产品这种场景锁定不是为了防黑客而是为了防误操作、防用户改坏模板所以前端锁定完全够用。在 Univer 里还有一种更细的做法就是给不同的区域设置不同的保护规则。比如 A 列字段名区域禁止编辑且禁止选中B 列填写区域允许编辑但只允许填数字C 列公式区域禁止编辑但允许查看。区域规则之间可以叠加优先级高的规则生效。这个能力是真正支撑复杂业务场景的关键比单纯的整表只读要灵活得多。实操中建议先把模板所有静态内容写入再开启保护。如果先开启保护再写数据写数据这个动作本身可能被权限拦截就得先临时关闭保护写完再恢复。我一开始就在这个顺序上卡了好几次后面统一改成“先填模板再上锁”。3.3 配合数据验证让可填区域也“只能填对的数据”锁定之外还有一层需求很常见填写区域不能是任意内容。比如“员工工号”必须填数字“部门”必须从下拉列表里选“入职时间”必须符合日期格式。这就是数据验证Data Validation的活。Univer 官方有数据验证模块包名一般是univerjs/sheets-data-validation。数据验证支持的类型包括数字范围、日期、文本长度、下拉列表、自定义公式校验等。和 Excel 的用户体验基本一致选中单元格后会显示提示输入非法内容时给出校验错误。设置下拉列表的核心伪代码如下import { ICommandService } from univerjs/core; import { SetDataValidationCommand } from univerjs/sheets-data-validation; await commandService.executeCommand(SetDataValidationCommand.id, { unitId: workbook-01, sheetId: sheet-01, range: { startRow: 1, startColumn: 2, endRow: 10, endColumn: 2 }, rule: { type: list, formula1: [研发部,市场部,人事部], showDropDown: true, allowBlank: false, }, });这里formula1传的是逗号分隔的下拉项字符串。用户点击单元格会出现下拉箭头只能选择我预设的部门名称手动输入其他值会被拦截。在模板场景中数据验证和单元格保护是搭配使用的一个管“能不能写”一个管“写什么”两者结合体验就很完整了。注意一点数据验证的规则在导入导出 Excel 时的兼容性不能 100% 保证。我实测下来Univer 导出到.xlsx文件后再用 WPS 或 Office 打开下拉列表、数字限制这类简单规则能保留但非常复杂的自定义公式校验会退化成普通文本这个在正式项目里要提前有心理预期。3.4 在线协作场景下的权限补充Univer 团队做这个项目时实时协作是原生能力不是后来加的补丁。每个客户端都连着协作网关操作通过 WebSocket 发送到服务端服务端做冲突处理和状态合并再广播给其他客户端。在这个架构下权限校验不能只依赖前端界面隐藏按钮否则一个用户通过 API 改了单元格其他客户端怎么办所以 Univer 的权限校验逻辑在命令层、服务端都有机会介入。具体到工程上如果你需要多人同时编辑同一张表并且不同用户看到相同模板但只能填自己的那部分建议这样做后端保存统一的模板结构用户打开时前端从后端拿模板数据渲染后开启同样的保护规则用户提交的数据只提交填写区域内的单元格后端合入数据库如果需要多人实时协同再部署 Univer 的协作服务端并把授权校验挂到服务端命令入口。这里提醒一下Univer 的脚手架文件包含前后端两部分。前端只是编辑器真正做数据持久化和权限控制需要你自建一个服务端。服务端负责读模板、下发模板、接收填写结果、校验内容。如果你的产品是给企业内部几十个人用不做实时协作其实也够前端锁定 表单提交就能满足业务不一定非要上完整协作链路。4. 完整代码示例从零搭一个在线填报表4.1 工程初始化Vite React Univer我的项目结构很简单就一个前端页面。初始化命令上面已经写过了这里补充依赖安装时的版本意识。Univer 还在快速迭代不同小版本的 API 变动比较大所以建议安装时锁定版本号不要直接装latest否则今天能跑的代码下个月升级后可能就编译不过了。npm install univerjs/presets0.1.0 univerjs/core0.1.0 univerjs/sheets-protection0.1.0具体版本号以你开发时官方发布为准我这里只是演示锁版本的做法。4.2 注入表格数据和模板样式初始化时如果不用workbookData也可以通过命令动态创建。两种方式我都用过模板场景我更倾向用workbookData因为模板结构是固定的初始化时就一次性写清楚逻辑最简单。模板样式方面给表头加背景色、字体加粗可以通过SetRangeStyleCommand来做import { SetRangeStyleCommand } from univerjs/sheets; await commandService.executeCommand(SetRangeStyleCommand.id, { unitId: workbook-01, sheetId: sheet-01, range: { startRow: 0, startColumn: 0, endRow: 0, endColumn: 3 }, style: { bl: 1, // bold bg: { rgb: #F2F2F2 }, ht: 2, // horizontal alignment center }, });Univer 的样式字段是缩写bl是加粗bg是背景色ht是水平对齐vt是垂直对齐fs是字号。第一次用确实记不住我通常是查类型定义直接补全而不是死记硬背。4.3 导出 Excel 与恢复模板模板填完之后用户要能导出成 Excel 文件。Univer 提供了univerjs/export-xlsx和univerjs/import-xlsx模块可以导出当前工作簿或导入已有 xlsx 文件。导出代码很简单import { ExportXlsxService } from univerjs/export-xlsx; const exportService univer.getService(ExportXlsxService); const result await exportService.exportXlsx({ unitId: workbook-01, path: 员工信息填报.xlsx, });这里有个细节导入 Excel 后原来设置的保护规则和数据验证可能丢失或变化。因为 xlsx 文件格式本身对保护和验证的支持不像 Univer 内部 API 那么完整。如果业务要求用户上传 Excel 作为模板建议后端统一解析一次 Excel 然后转成 Univer 能识别的模板结构不要在导入后依赖 Excel 自带保护。4.4 关键代码片段解说我把最核心的“模板初始化 保护 数据验证 导出”整理成一个逻辑顺序初始化 Univer传入workbookData定义模板。用SetRangeStyleCommand给表头加样式。用SetRangeValuesMutation写入模板固定内容比如标题行、公式列示例数据。获取当前 Worksheet设置整表不可编辑。给填写区域添加区域保护例外规则允许编辑和修改样式。给填写区域添加数据验证规则限制输入类型。用户填写完成后通过前后端接口提交填写区域的数据。这套顺序建议做成一个统一的初始化函数不要让每个页面各自实现否则后面维护权限逻辑会非常痛苦。权限规则的增删集中在服务端配置下发更安全。在线协作模式下模板的保护规则通常由服务端创建好再下发给所有客户端。客户端并不自己定义规则只是展示规则带来的效果。这样可以让权限唯一集中不用每个前端各写一套减少不一致风险。5. 常见问题与排查技巧实录5.1 问题速查表我整理了几个实操中最常遇到的问题和排查方向直接列成表格方便大家直接对照。现象可能原因排查方向整个表格白屏容器没有宽高检查container父元素尺寸预设的工具栏按钮点了没反应插件未加载检查 presets 里是否启用了对应模块设置了保护但还能编辑命令层没走权限校验检查是否真正执行了保护命令以及权限点名称是否正确数据验证下拉不显示缺失showDropDown属性确认规则配置里showDropDown: true导出后保护丢失xlsx 格式限制导出前重新套用规则或后端生成模板协作模式下权限不一致客户端各自定义规则改为服务端下发统一规则5.2 性能与渲染避坑Univer 用 Canvas 渲染大数据量下性能比 DOM 方案强很多但它不是没有成本。如果你在一个 Sheet 里塞了 10 万行数据初始化渲染依然会有卡顿。建议模板场景下控制数据量比如实际填报表单的几十行完全无压力但如果你要做数据大屏展示那就用数据透视或分页加载的思路不要试图在单个 Sheet 里渲染无限行。还可以关闭一些不用的预设能力比如不需要幻灯片就在 preset 里只加载 Sheet 相关模块模块少了初始化自然更快。5.3 我踩过的坑和心得最后分享几个真实的踩坑记录。第一个是最容易忽视的Univer 的presets默认加载的是中文还是英文界面取决于语言配置记得在初始化时显式设置locale否则会出现一半中文一半英文的界面。第二个是字体问题Canvas 渲染时如果用了用户本机没装的字体导出的 PDF 或图片里字体就会回退到默认字体排版会变样。做正式模板前建议统一指定 Web Font 或用系统字体。第三个是命令失败一定看返回值executeCommand返回的是命令是否执行成功的布尔值不检查的话经常默默失败尤其是权限拦截导致的静默失败排查起来特别费劲。我的体会是Univer 是一个上限很高的工具但它不是一个开箱即用、零学习成本的组件。你越熟练它的命令系统和权限模型越能发挥出它的价值。尤其是模板填报这类场景把保护规则和数据验证组合起来体验和开发效率都远超自己硬画表单。如果你正在做的项目和这篇里描述的需求相似直接按这个思路去搭应该能把最核心的部分快速跑通。