
1. 项目概述先说结论Univer 是一个开源的在线电子表格解决方案核心想解决的事情就是“让表格成为可以多人协作、可嵌入、可定制的数据工具”。我最初接触到它是因为团队需要一个既能快速落地、又不想被底层渲染细节拖住的在线表格模块。市面上开源表格项目不少但要么是纯展示型要么依赖太重Univer 在“在线编辑”和“可深度定制”之间平衡得比较好尤其是它对单元格权限控制的表达能力恰好能覆盖一个我在实际项目里经常遇到的场景让用户定义表格然后让用户去填写一些单元格其他单元格用户无法修改。这类需求的本质是“结构化数据收集”。传统做法是设计表单但表单的灵活性不够直接用 Excel 收集又会遇到格式破坏、误改公式、数据校验缺失的问题。Univer 给出的思路是用电子表格作为交互界面但通过数据校验、工作表保护、选区内禁止编辑等机制把用户的操作范围收敛到“应该填写的地方”。这样既有 Excel 的灵活度又有表单的约束力尤其适合做数据填报、预算拆分、信息收集这类内部工具。这篇内容适合两类读者一类是准备在自研系统里嵌入在线表格的前端工程师另一类是产品负责人想判断 Univer 是否适合做“表格化数据采集”方案。我不会堆砌官方文档里已有的内容更侧重实际接入时容易忽略的细节以及“如何优雅地限制用户只能改指定单元格”的实现路径。另外一个值得先说的背景是Univer 早期版本在社区讨论中常被拿来和 Luckysheet 比较它们的迭代方向有明显差异。Luckysheet 更专注电子表格本身的能力Univer 的设计从一开始就考虑到了跨端、跨框架、可嵌入包括文档、幻灯片、表格的统一底座。这一点在实际选型时影响很大因为你引入的不仅仅是一个表格组件而是一整套可扩展的文档引擎。实测下来Univer 对 React、Vue 的集成都比较自然且提供了较为完整的数据操作 API。2. 核心功能与设计思路2.1 从“填表需求”到“单元格权限”的拆解先把这个需求拆开看支持用户定义表格然后让用户去填写一些单元格其他单元格用户无法修改。这里面有三个关键能力点用户能自定义表格结构包括列名、行数、数据格式。用户填写的范围被限制在特定单元格或区域。未开放的单元格对普通用户是只读或完全不可编辑的。第三个能力在 Univer 中主要靠“工作表保护”和“区域权限”实现但有个容易踩坑的细节工作表保护是一个整体开关一旦开启所有单元格默认都不可编辑你需要显式声明哪些区域是允许编辑的。这个“默认拒绝 显式放行”的模型和大多数人的直觉相反导致初次使用时经常出现“保护后所有格子都填不了”的问题。保护逻辑的实际执行层面Univer 在底层会拦截单元格编辑事件判断当前选中区域是否在放行名单中。如果不在编辑操作会被回滚或者直接弹出提示。这种拦截发生在数据模型层而不是单纯的 UI 层禁用所以即便通过键盘快捷键、复制粘贴等非常规路径去改动单元格也无法绕过。这一点对数据准确性至关重要我在实际使用中测试过几种绕过方式包括批量粘贴、拖拽填充、定位清除内容均被正确拦截。除了保护机制Univer 还提供了数据验证Data Validation能力可以给指定区域设置下拉选项、数值范围、必填校验等。这正好解决“用户可以填、但不能乱填”的需求。配合单元格保护就能形成一套完整的“自由定义 有限填写”闭环。2.2 用户自定义表格的三种实现路径“用户定义表格”这个需求可以有很多层含义取决于自定义的深度。第一层用户只能定义表头和列结构行数由系统控制。这种最简单可以先用 Univer API 创建一个空表格预置列名再通过保护机制锁定表头区域。第二层用户能定义表头、列宽、列类型并添加任意行数据。这种需要把 Univer 的表格操作权限与业务角色绑定。比如管理员角色可以编辑任何单元格普通参与者只能编辑主体数据区域。第三层用户在界面上通过交互方式“设计”一张新表格包括添加列、设置校验规则、配置填写权限然后发布给其他人填写。这种其实就是把 Univer 当作一个“表格设计器”来使用。第三层是最复杂的因为它要处理的是“元表格”和“实例表格”之间的关系。我的建议是不要把业务层面所有自定义逻辑都塞到 Univer 里它擅长的是“对一个确定结构的表格做在线编辑”而不是自己去建模一套动态表结构。因此实践中更好的做法是用一套配置面板比如简单的表单工具让用户设定字段、类型和权限然后通过 Univer 的 API 动态生成表格实例。这样既保留了 Univer 的在线编辑体验又让自定义能力变得可入库、可审计。动态生成表格实例时有两个 API 比较关键一个是创建空白工作簿并写入表头另一个是设置列宽、隐藏网格线、保护指定区域。前者解决“表格长什么样”后者解决“谁能填什么”。我在实际操作中习惯把这两步拆成两个函数createTableTemplate(config) 和 applyPermissionRules(workbook, config)职责分离后续调试和维护也轻松很多。2.3 为什么选择 Univer 而不是自研或纯表单方案我给不同项目做过表格方案选型坦白说自研在线表格的成本是被严重低估的。Excel 里的一个“单元格”看起来简单背后涉及渲染引擎、公式引擎、数据模型、剪切板、拖拽交互、协同冲突处理。这些功能如果全部自研一个精干的团队至少也要半年以上才能达到可用的程度而且后续维护成本极高。Univer 的价值在于把这一层都做好了并且作为开源项目提供了完整代码和扩展机制二开成本远低于从零自建。对比纯表单方案也有明显差异。表单适合“字段固定、流程固定”的场景一旦用户提出“我想加一个备注列”“我希望把区域调大一点”表单的配置复杂度就会急剧上升。Univer 的好处是用户面对的是真正的电子表格可以自由增删行列、调整格式、插入批注只要权限允许。所以如果你做的是“半结构化数据收集”即大体格式相同、但每批数据都有细微差异Univer 会比表单方案更合适。Univer 的协同能力也值得一提。早期版本是传统的前后端协同新版已经引入了类似 CRDT 的协同算法可以实现实时多端编辑。这意味着多个用户可以同时填写不同的单元格而不需要人为加锁等待。不过有一点要注意Univer 的协同是建立在它自己的底层模型之上的如果业务系统里有大量外部数据需要实时同步进表格需要合理设计数据推送机制否则可能出现版本冲突。3. 实操准备与环境集成3.1 快速启动一个 Univer 表格实例在动手之前先把基础环境准备好。Univer 目前对打包工具的支持比较友好使用 Vite 或者 Webpack 都可以。我习惯用 Vite 创建工程配置少、启动快。Node 版本建议至少 18否则部分依赖可能会编译报错。初始化一个 React Univer 的最小项目可以这样操作。先创建项目并安装依赖npm create vitelatest univer-demo --template react-ts cd univer-demo npm install univerjs/core univerjs/presets univerjs/preset-sheets univerjs/ui这里稍微说明下几个包的关系。univerjs/core是核心逻辑层负责数据模型、命令系统、协同等univerjs/preset-sheets是电子表格的预设能力集合univerjs/ui是界面层。官方还提供了univerjs/presets来快速启动适合不想逐个注册插件的场景。然后在入口文件里初始化 Univerimport { Univer } from univerjs/core; import { UniverPreset, PLUGIN_NAMES } from univerjs/presets; import { createSheetPreset } from univerjs/preset-sheets; const univer new Univer(); univer.registerPlugin(UniverPreset, { plugins: createSheetPreset(), });这样就能渲染出一个完整的电子表格工作区。如果你需要做更细的控制比如去掉菜单栏、只保留工具栏可以在createSheetPreset的配置里调整 UI 组件的显隐。我在搭建过程中遇到过一个问题新版本依赖包的版本号如果不匹配初始化时会直接报错报错信息比较隐晦。后来我把所有univerjs/*包都固定到同一个版本号问题才消失。所以这里强烈建议安装时要么全部用 latest要么全部用某个确定版本号不要混搭。3.2 前端框架接入时的关键取舍Univer 底层是用原生 TS 写的不绑定任何框架理论上可以嵌入任意前端框架。但实际接入时React 和 Vue 生态的集成方式还是有差异的。在 React 项目中我推荐使用官方的 React 绑定包univerjs/react。它提供了组件化的 API可以直接用UniverProvider和SheetComponent这样的结构包裹接管 Univer 实例的生命周期避免手动管理 ref 和销毁逻辑。实际操作中这样写import { UniverProvider, SheetComponent } from univerjs/react; function App() { return ( UniverProvider SheetComponent / /UniverProvider ); }组件卸载时UniverProvider会自动执行univer.dispose()避免内存泄漏。有一点需要特别留意如果同一个页面中需要创建多个 Univer 实例一定要保证每个实例有独立的 id否则静态资源加载会互相干扰工具栏也会失效。3.3 典型场景定义表格并允许部分填写下面用一个我自己做过的“项目信息收集表”场景来演示完整流程。这个场景的需求是管理员创建一张表固定好“项目名称、负责人、开始日期、预算金额、状态”这几列然后发送给多个用户填写用户只能填写“负责人、开始日期、预算金额”这几个字段连表头都不能改。先定义表格配置const sheetConfig { sheets: [ { name: 项目信息, rowCount: 100, colCount: 10, defaultText: , }, ], };然后创建表格实例并写入表头const workbook univer.createUniverSheet(sheetConfig); const sheet workbook.getActiveSheet(); const headers [项目名称, 负责人, 开始日期, 预算金额, 状态]; headers.forEach((text, index) { sheet.getCell(0, index)?.setValue(text); });接下来是核心部分设置保护规则。第一列“项目名称”和第五列“状态”是用户不能修改的负责人、开始日期、预算金额所在列则开放给普通用户填写。我需要先把整张工作表保护起来然后单独放行 B、C、D 三列区域。在 Univer 的权限模型中设置入口是getPermissionManager().setPermission()它接收单元格地址或区域对象。代码逻辑大致如下const permission sheet.getPermissionManager(); // 开启工作表保护这一步会让所有单元格默认不可编辑 permission.setPermission({ sheetId: sheet.getSheetId() }, { edit: false }); // 放行 B、C、D 两行之后的所有单元格 const editableRegion { startRow: 1, endRow: 99, startColumn: 1, endColumn: 3, }; permission.setPermission(editableRegion, { edit: true });这里有个小细节区域的行索引、列索引都是从 0 开始。第二列是索引 1第四列是索引 3。如果配置不好用户能填的区域会偏一列排查时比较容易迷糊。建议在代码里加一个辅助函数把列名转换成索引比如把 B 转成数字 1减少人工计算失误。4. 权限控制的进阶机制4.1 用户级权限与角色分级基础的保护 放行模型可以解决“谁可以改这块区域”的问题但实际业务往往是多角色的。比如普通用户只能填自己负责的行部门负责人可以看到所有数据但不能修改别人的格子管理员拥有一切权限。Univer 并不是一套完整的用户权限体系它更多是提供了单元格级别的“编辑锁”。在真实项目中需要把它和业务系统的用户体系打通。常用的做法是在业务层拦截 Univer 的权限判断在 Univer 之上再包一层权限过滤在打开表格时根据当前登录用户和业务配置动态生成“哪个区域对哪个角色可写”的规则集合然后一次性在初始化时设置好。听起来简单但实际操作时有几个坑要避开。第一个坑如果多个用户或角色对同一区域有不同权限动态生成的规则可能互相冲突。最好提前定义好优先级。比如“管理员覆盖普通用户”“区域级覆盖工作表级”这样的规则必须在权限表里写清楚。第二个坑权限规则是静态写入的用户切换角色后如果不重新加载表格规则不会自动更新。因此当我需要动态切换用户视角时会先univer.dispose()销毁实例再重新创建一个带新权限配置的实例。虽然笨一点但逻辑清晰、不容易出错。第三个坑Univer 的权限管理针对的是“当前用户”模型它并不感知谁是“普通用户”、谁是“管理员”这些概念需要业务层自己维护。我的做法是把权限规则集中在后端生成前端只是把规则结果塞给 Univer。这样即使有人恶意调用 API也无法绕过权限判定因为最终的数据提交还是要经过后端的二次校验。4.2 数据校验与填写约束如果只做“可编辑/不可编辑”限制还不足以应对真实的填充场景。比如“预算金额”列要求只能输入数字且不能超过 100 万“开始日期”列必须是合法的日期格式“状态”列只能从下拉框里选“未开始、进行中、已完成”这三个值。这些约束属于数据校验层Univer 提供了覆盖这些需求的接口。在 Univer 中数据校验通过DataValidation插件注册可以对单个单元格或区域设置规则。举个实际例子给“状态”列设置下拉列表const dataValidation univer.getPlugin(DataValidation); dataValidation.addRule({ range: { startRow: 1, endRow: 99, startColumn: 4, endColumn: 4 }, type: list, formula1: 未开始,进行中,已完成, allowBlank: true, showDropdown: true, });下拉校验的实际体验和原生 Excel 比较接近用户点击单元格后出现下拉箭头选择受限值无法录入其他内容。这种方式比“让用户自由输入再校验”要友好得多也减少了后端数据处理时的异常分支。对于数值范围校验“预算金额”可以这样设置dataValidation.addRule({ range: { startRow: 1, endRow: 99, startColumn: 3, endColumn: 3 }, type: number, operator: between, formula1: 0, formula2: 1000000, allowBlank: false, });一旦用户试图输入超出范围的值Univer 会弹出错误提示并且不把非法值写入数据模型。这一点我实测过粘贴一串超范围的数据时也会被整体拦截不会出现“部分行成功部分行失败”的状态。要特别强调的是数据校验永远不能取代后端校验。Univer 的校验是前端体验层它拦截的是普通用户的操作但无法阻止恶意请求直接调用底层 API 篡改单元格。所以我通常在提交按钮的逻辑里再做一次全表数据合法性检查把问题格子的坐标和提示理由一次性返回给用户。4.3 隐藏行列与公式保护的组合用法有一种更隐蔽的权限问题比“能改不能改”更麻烦那就是“能不能看见”。需求上可能要求用户看不到其他人的填报内容比如“每个销售只能看到自己的客户报价”。这种场景单纯用编辑权限不够必须结合行列隐藏、列宽、条件渲染等手段。Univer 提供了隐藏行列的 APIsheet.hideColumns([2, 3, 4])这样就能把指定列隐藏起来。隐藏后用户看不到这些列的数据也无法通过拖拽等方式让其重新出现。但要注意这种隐藏只是视觉层面的数据仍然在模型里协同另一端的人依然能看到。如果希望彻底隔离数据可见性必须依赖后端按用户返回不同的表格数据集这就不是纯前端隐藏能解决的了。在实际项目中我常常用“行列隐藏 编辑权限 数据校验 公式保护”的组合实现一个相对完整的数据隔离体验。公式保护的意思是如果一个单元格里有公式即使该区域被放行为可编辑用户也不能修改公式本身。Univer 可以把公式和普通值区分开通过immutable属性让公式单元格只读。这样做的好处是防住了“手滑删公式”这种低级失误。5. 实操过程与核心环节实现5.1 构建“预算填报”的完整流程为了把这个方案讲透我完整复盘一个近期做的“部门预算填报”项目。需求是总部下发一张预算表模板各事业部填写“人员预算、差旅预算、设备预算、其他支出”四列最后自动求和。模板由总部统一锁定各事业部只能填写自己的行不能看到其他部门的数据。第一步创建表格模板。我用 Univer 生成一个包含主标题、表头、部门行、汇总行的工作簿。表头的样式、列宽、背景色都提前配置好。汇总行用公式生成比如第 99 行的 F 列写入SUM(B2:B98)。这行是自动计算用户不能改所以我会把汇总行权限设为只读。第二步设置列权限。每个部门需要填写的是 B、C、D、E 四列而 A 列部门名是系统自动填充的用户不能改。因此工作表保护后我只放行 B~E 列区域的可编辑权限。汇总行 F 列和表头行不放行。第三步做行级隔离。每个事业部的账号在浏览器中访问时前端拿到的是该部门对应的行数据集合。我在初始化时只写入该部门允许看到的数据其他部门的数据用后端接口过滤后再渲染到表格上。前端的隐藏手段在这里不够用必须靠后端做数据裁剪。第四步数据校验。对“人员预算”“差旅预算”等列设置数值范围校验比如不能为负数不能超过单笔上限。填写过程中如果用户输入了非数字字符Univer 会阻止录入。5.2 关键代码示例与注释把上述流程简化后核心代码是这样组织的。先是创建表格和受保护区域的辅助函数function setupBudgetSheet(workbook, allowedColumns) { const sheet workbook.getActiveSheet(); const permission sheet.getPermissionManager(); // 保护整张工作表 permission.setPermission({ sheetId: sheet.getSheetId() }, { edit: false }); // 放行允许编辑的列例如 B、C、D、E 列 allowedColumns.forEach((colIndex) { permission.setPermission( { startRow: 1, endRow: 98, startColumn: colIndex, endColumn: colIndex }, { edit: true } ); }); }调用时传入列索引数组setupBudgetSheet(workbook, [1, 2, 3, 4]);这里有一个体验优化要讲保护状态开启后表头区域和汇总行都是不可编辑的但用户点击这些区域时Univer 默认会跳出一个英文错误提示类似 This cell is protected对国内业务来说不太友好。我建议在初始化时关闭这类提示或者用中文的提示组件替换。做法是监听 Univer 的通知事件在权限校验失败时弹出一个自定义的中文提示而不是让用户看英文报错。5.3 提交数据与后端同步数据填完之后面临的问题是怎么把表格里的数据同步到后端Univer 提供了getSnapshot()方法可以在任意时刻抓取当前工作簿的数据快照。这个快照包含所有单元格的值、公式、样式和校验规则。不过对它下一步的操作有很多坑。如果直接把这个快照整个发给后端后端要解析的字段非常多性能不好。更合理的做法是遍历指定区域提取用户实际填写的数据function extractFilledData(sheet, startRow, endRow, startCol, endCol) { const result: ArrayRecordstring, unknown []; for (let row startRow; row endRow; row) { const record: Recordstring, unknown {}; for (let col startCol; col endCol; col) { const cell sheet.getCell(row, col); record[col_${col}] cell?.getValue() ?? ; } result.push(record); } return result; }这个函数返回一个数组每行对应一个对象键名用列序号便于后端映射到数据库字段。数据处理还有一个高频需求表格提交后需要锁定已提交数据防止用户二次修改。我的处理方式是在提交成功后重新调用一次权限设置把刚才放行的区域全部收回来让表格整体进入只读状态。如果有“第一次提交后发现填错了需要修改”的场景可以做“重新打开”的二次解锁机制不过要加上审批或操作留痕避免数据被随意回改。5.4 协同编辑状态下的权限一致性如果要开启实时协同权限判断的时间节点就变得敏感。两个人同时编辑同一张表一个人尝试修改受保护单元格另一个人正在修改相邻单元格这两次操作在协同底层可能是并发执行的。Univer 的协同算法能够合并单元格层面的变更但权限校验必须在每个操作提交前做。我最开始做协同联调时出现过一个诡异问题A 用户明明没有权限修改 F 列但协同同步后他本地却显示 F 列的内容变了。排查后才发现A 用户无权限是因为前端没有给 F 列设置“不可编辑”权限而不是后端拒绝了这次修改。这个问题的本质是权限设置也在协同同步的范围内如果不小心把一个带有权限规则的快照推送给所有端权限就会被覆盖。解决方案是权限配置信息不要随表格快照一起同步而是由前端在每次初始化时通过接口单独获取并在创建 Univer 实例后立即注入。这样协同端之间传输的数据只包含单元格数据不包含权限元数据。同步过程中权限配置始终由业务端控制而不是被某个客户端的本地状态带偏。6. 常见问题与排查技巧实录6.1 保护模式下所有单元格都无法编辑这是接入 Univer 后最常遇到的第一个问题。现象是设置permission.setPermission({ sheetId }, { edit: false })之后再放行某些区域编辑时没有任何反应。排查思路是确认“放行”代码是否真的执行了。Univer 的权限 API 不是增量覆盖而是整体博弈后设置的规则会按区域优先匹配。我建议在设置完权限后用permission.getPermissions()把结果打印到控制台核对目标区域是否已经是可编辑状态。如果结果显示edit: true但 UI 仍然不可改可以检查是否设置了多个sheetId。在多工作表模式下放行规则如果不指定sheetId容易落到默认工作表上而用户看到的是另一张表。6.2 下拉校验规则不生效数据校验的下拉框不出现是很常见的现象。多数原因不是代码写错而是校验规则的作用区域和实际选区不一致。比如行索引从 0 开始填了endRow: 99但表格实际只有 50 行规则会被裁剪到有效范围之外。另一种情况是校验插件的注册顺序问题。如果 Univer 实例初始化的时候没有把DataValidation插件注册进去后续addRule就会静默失败。排查时先确认插件是否在createSheetPreset()的返回数组里缺失的话单独补齐univer.registerPlugin(DataValidationPlugin);第三类坑是“混合格式”问题。当下拉公式里的枚举值包含全角逗号时解析会出错。比如未开始,进行中,已完成这个字符串本身没有问题但如果录入的人把逗号输入成中文逗号校验会整体失败并且不提示。这个坑在对外发布表格时要尤其注意。6.3 动态修改权限后界面未刷新有一些场景需要用户停留在当前表格页然后由系统后台修改某区域的权限。修改后本地界面没有立即生效继续操作旧单元格还是可编辑。这里涉及 Univer 的权限缓存机制。权限变更后Univer 不会自动重绘所有单元格需要手动刷新 UI 区域。我采用的方案是重新初始化工作表数据sheet.getPermissionManager().refreshPermissions();这个 API 在不同版本中名字可能不同新版默认叫refreshPermissions。如果找不到对应方法可以先用一次轻量级的sheet.getCell(0,0)?.setValue(sheet.getCell(0,0)?.getValue())触发一次单元格重绘让它重新感知权限状态。这个方法有点土但实测有效适合应急场景。6.4 隐藏列后用户仍能通过公式引用有读者问过隐藏列的数据虽然看不到用户能不能通过自己新写一个公式引用了隐藏列的值来“猜”出敏感数据可以。Univer 的隐藏是视觉层面的数据模型中的值依然存在。所以遇到真正的敏感数据隔离需求唯一可靠的做法是在后端就过滤掉。为了兼顾表格可用性和数据安全我的推荐方案是三级隔离后端按用户返回裁剪后的数据集前端用隐藏列辅助视图整洁敏感单元格同时设置不可编辑和不可见权限。三层都做到位才能称得上“权限可控”。6.5 表格加载白屏与依赖版本问题Univer 更新节奏比较快白屏大概率是版本不兼容。具体表现是初始化不报错、页面空白、控制台无异常。这时候先看浏览器 Network 面板确认univer相关的 JS 和 CSS 文件有没有正常加载。很多时候是 CSS 没引入整个表格区域渲染出来了但是所有元素都是透明的。需要确认的引入内容包括import univerjs/preset-sheets/lib/index.css;如果忘记引入样式文件所有单元格和工具栏都是白板状态而且没有控制台错误。这个问题非常隐蔽我在接入早期每天都有人问所以在这里单独拿出来提醒。7. 经验总结我在实际使用中的几个建议说了这么多最后分享一些从实际项目中沉淀下来的体会。第一Univer 的学习曲线不算陡但它的“历史包袱”不少。因为功能范围广涉及工作簿、工作表、单元格、权限、数据验证等多个子系统官方文档有时不会把所有组合场景写清楚。遇到疑难问题多去 GitHub 的 issue 区和 demo 代码里翻一翻比看文档更有收获。第二权限模型是 Univer 的一个亮点但也是最容易出问题的点。每次更新版本前我建议专门跑一遍权限相关用例特别是区域匹配和保护开关。因为权限相关的 API 变动往往不会出现在更新日志的显著位置。第三如果团队缺乏前端表格组件开发经验Univer 的接入成本相对较低但不要因为“开源”就放弃阅读源码。它本身的命令刷子和协同逻辑非常值得学习对未来做更复杂的业务也是一种积累。最后我还想单独强调一个之前踩过的坑在 Univer 中尽量不要通过“直接修改 DOM”的方式去改变单元格样式。Univer 的渲染层有自己的缓存机制如果绕过 API 修改恢复数据时样式可能会全部丢失。所有对单元格的操作都应该走它的命令系统和数据模型。Univer 的生态还在快速演进但从当前版本来看它已经完全可以承担“用户自定义表格 有限单元格填写”这类业务需求。希望这篇内容能帮你少走弯路把在线表格能力快速接入自己的系统。