
1. 从“univer”这个名字说起它到底想解决什么问题第一次听到 univer 这个名字很多人会以为是某个云服务或者某个后端框架。实际上它是一套面向表格场景的前端 SDK核心能力是把电子表格的渲染、编辑、公式计算、协同这些能力封装成可复用的模块让开发者可以把它嵌进自己的产品里。你可以把它理解成“把 Excel 的能力拆成零件按需组装到你的网页里”。我最早接触这类需求是在做一个内部数据填报系统的时候。业务方的诉求很朴素给一张固定格式的表让不同的人只填自己负责的那几列其他列锁死不能动。听起来简单但真做起来你会发现纯手写一个表格组件要处理的事情非常多——单元格的选区、键盘导航、复制粘贴、公式联动、撤销重做每一项都是坑。univer 这类 SDK 的价值就在于它把这些通用能力都做好了你只需要在它的插件体系上做定制。热搜词里出现了 SDK、Node.js、Canvas、插件架构这几个关键词基本勾勒出了 univer 的技术轮廓它是一个基于 Canvas 渲染的、采用插件架构的前端 SDK同时有 Node.js 侧的服务端能力用于协同和公式计算。这篇文章我会围绕这几个点展开重点讲清楚三件事univer 的整体设计思路为什么这么选、单元格权限控制这类定制需求怎么落地、以及在实际集成过程中会遇到哪些坑。适合谁看如果你正在做在线表格、数据填报、报表配置、低代码平台里的表格模块或者你只是单纯好奇一个现代表格引擎内部是怎么组织的这篇内容都能给你一些可以直接抄作业的东西。我不会只讲概念会把参数、步骤、排查思路都摊开说。2. univer 的整体架构与设计思路拆解2.1 为什么是 Canvas 而不是 DOM这是很多人第一个会问的问题。传统表格组件大多用 DOM 来渲染单元格每个单元格一个 div 或者 td。这种方式的好处是天然支持 CSS、事件绑定直观、可访问性好。但它的致命问题在于性能当表格规模上到几万行、几十列的时候DOM 节点数量会爆炸浏览器的布局和重绘开销会直接把页面拖垮。Canvas 的思路完全不同。它把整个表格画在一张画布上单元格不是真实的 DOM 节点而是绘制出来的像素。这样一来无论表格有多少行多少列DOM 里始终只有那么几个 canvas 元素。滚动、缩放、选区高亮这些操作本质上都是重绘性能可控。但 Canvas 也带来了代价。你没法用 CSS 去控制某个单元格的样式也没法直接给单元格绑事件所有的命中检测、事件分发都要自己实现。univer 的做法是在 Canvas 之上抽象出一套渲染层和交互层把“哪个坐标对应哪个单元格”这件事封装起来对上层插件暴露成类似 DOM 的接口。这就是为什么它必须采用插件架构——因为 Canvas 的灵活性需要靠分层来管理复杂度。提示如果你的表格数据量在千行以内其实 DOM 方案完全够用不必为了 Canvas 而 Canvas。Canvas 的优势要到万行级别才明显体现出来。2.2 插件架构到底解决了什么univer 的插件架构不是那种“为了显得高级”而设计的。它解决的是一个很现实的问题表格的能力边界太宽了。有人只需要基础编辑有人要公式有人要协同有人要图表有人要权限控制。如果把这些全塞进一个核心包里体积会失控而且任何一个功能的改动都可能影响其他功能。插件架构把核心拆成两层一层是不可变的内核负责数据模型、渲染管线、事件总线这些基础设施另一层是各种可插拔的功能模块比如公式插件、协同插件、权限插件。每个插件通过注册的方式挂到内核上声明自己关心哪些事件、需要扩展哪些能力。这种设计带来的直接好处是你可以只引入自己需要的插件。一个只需要展示和基础编辑的场景可以完全不引入公式和协同打包体积能小一大截。另一个好处是定制需求可以通过写新插件来实现而不是去改核心代码升级的时候不会因为改过源码而痛苦。2.3 Node.js 侧承担了什么角色热搜词里出现了 Node.js这不是偶然。univer 的协同和公式计算有一部分是放在服务端的。公式计算尤其典型如果所有公式都在浏览器里算遇到复杂依赖链或者大数据量主线程会被阻塞页面直接卡死。把计算放到 Node.js 服务端浏览器只负责渲染结果体验会好很多。协同场景也是同理。多人同时编辑一张表需要有一个权威的服务端来做冲突合并、操作排序、状态同步。univer 提供了 Node.js 侧的能力来支撑这套逻辑。这意味着你在部署的时候除了前端静态资源还需要跑一个 Node.js 服务。2.4 核心模块的职责划分把 univer 拆开看大致有这么几个核心部分模块职责是否可替换数据模型层管理单元格数据、样式、行列结构内核固定渲染层基于 Canvas 绘制表格内核固定交互层处理选区、键盘、鼠标事件内核固定公式引擎解析和计算公式插件可替换协同模块多端状态同步插件可选权限模块控制单元格可编辑性插件可定制这张表里最关键的信息是数据模型、渲染、交互这三层是内核你改不动也不应该改公式、协同、权限这些是插件层是你做定制的主战场。理解了这条边界后面做权限控制的时候就不会走弯路。3. 单元格权限控制从需求到落地的完整思路3.1 需求拆解什么叫“用户只能填某些单元格”这个需求听起来一句话但拆开看有好几个维度。第一是“哪些单元格可编辑”这可能是固定的列也可能是根据当前用户角色动态决定的。第二是“不可编辑的单元格表现成什么样”是灰掉、还是完全禁止选中、还是允许选中但禁止输入。第三是“这个限制在什么层面生效”是纯前端拦截还是服务端也要校验。我见过很多项目在这三点上没想清楚做到一半发现业务方要的是“允许选中查看公式但不能改”而开发做成了“完全禁止点击”返工成本很高。所以在动手之前一定要把这三个维度跟业务方对齐。3.2 权限模型的设计基于行列还是基于单元格univer 的数据模型是行列式的所以权限控制最自然的做法是基于行和列来定义。比如“第 3 列到第 5 列可编辑”“第 10 行以下只读”。这种模型的优点是规则少、性能好判断一个单元格是否可编辑只需要查它所在的行列是否命中规则。但如果业务需要的是零散的、不规则的单元格权限比如一张表里东一个西一个可编辑格那就需要基于单元格坐标来定义。这种模型灵活但规则数量可能很大判断时需要用到哈希表来加速查找。实际项目里我建议优先用行列模型因为绝大多数填报场景的权限都是按列划分的。只有当业务确实需要不规则权限时才退到单元格模型。下面是一个行列权限规则的示例结构const permissionRules { editableColumns: [2, 3, 4], editableRows: { start: 1, end: 100 }, readOnlyCells: [A1, B2], role: editor };这个结构里editableColumns定义了可编辑的列索引editableRows定义了可编辑的行范围readOnlyCells用来覆盖个别例外role则用来支持多角色场景。判断逻辑就是先看列、再看行、最后看例外。3.3 在 univer 里怎么挂载权限逻辑univer 的插件体系里控制单元格是否可编辑通常是通过拦截编辑命令来实现的。当用户尝试进入某个单元格的编辑态时会触发一个命令你可以在插件里监听这个命令判断当前单元格是否在允许编辑的范围内如果不在就阻止它。具体来说你需要关注的是编辑相关的命令比如进入编辑、提交编辑、粘贴内容这些。拦截的时机很关键如果只在提交时拦截用户已经输入了半天才被拒绝体验很差应该在进入编辑态之前就拦截让用户根本点不进去。// 伪代码示意注册一个权限插件 class PermissionPlugin { onStart() { this.dispose this._commandService.interceptCommand( sheet.command.set-editing, (command) { const cell command.params.range; if (!this._isEditable(cell)) { return false; // 阻止命令执行 } return true; } ); } _isEditable(cell) { // 结合上面的 permissionRules 做判断 return checkPermission(cell, this._rules); } }这段代码的核心是interceptCommand它让你有机会在命令真正执行前做判断。返回 false 就相当于把这次操作吞掉了。这是 univer 插件架构里做权限控制最标准的方式。3.4 只读单元格的视觉反馈光拦截操作还不够用户需要一眼看出哪些格子能填、哪些不能填。univer 的渲染层支持自定义单元格样式你可以给只读单元格加一个浅灰背景或者把文字颜色调淡。这里有个细节要注意不要用太重的灰色否则用户会以为单元格被禁用了连看都不能看。我一般用#f5f5f5这种很浅的灰做背景配合正常的文字颜色传达的是“这里不用你填”而不是“这里坏了”。另外如果只读单元格里有公式计算出来的结果用户可能想查看公式。这时候可以允许选中但禁止编辑选中后在上方公式栏显示公式内容。这个体验比完全禁止点击要好得多。4. 实操过程从零集成 univer 并实现权限控制4.1 环境准备与依赖安装univer 是前端 SDK所以基础环境就是 Node.js 加一个前端构建工具。Node.js 版本建议用 LTS太新的版本有时候会遇到依赖不兼容的问题。我实测下来 Node 18 和 Node 20 都比较稳。安装依赖的时候要注意univer 是拆成多个包发布的核心包和各个插件包是分开的。你不需要一次性全装按需引入即可。一个最小可用的表格场景通常需要核心包加上基础 UI 插件。# 初始化项目 npm init -y # 安装核心依赖 npm install univerjs/core univerjs/design univerjs/ui # 如果需要公式能力 npm install univerjs/sheets-formula # 如果需要协同能力 npm install univerjs/sheets-collaboration注意univer 的包版本之间是有兼容要求的核心包和插件包的大版本号要一致。如果你混用了不同大版本的包很可能在初始化的时候报错而且报错信息不一定直观。建议在 package.json 里锁定版本或者用同一个版本号批量安装。4.2 初始化一个最小可用的表格初始化的过程本质上是创建一个 univer 实例然后把插件逐个注册进去。这里的关键是注册顺序有些插件依赖其他插件提供的能力顺序错了会报“找不到依赖”的错误。import { Univer } from univerjs/core; import { UniverSheetsPlugin } from univerjs/sheets; import { UniverSheetsUIPlugin } from univerjs/sheets-ui; import { UniverSheetsFormulaPlugin } from univerjs/sheets-formula; const univer new Univer(); // 先注册基础表格能力 univer.registerPlugin(UniverSheetsPlugin); univer.registerPlugin(UniverSheetsUIPlugin); // 再注册依赖基础能力的插件 univer.registerPlugin(UniverSheetsFormulaPlugin); // 最后挂载到 DOM univer.createUniverSheet(document.getElementById(app));这段代码里UniverSheetsPlugin提供数据模型和渲染UniverSheetsUIPlugin提供工具栏和交互界面UniverSheetsFormulaPlugin提供公式能力。顺序上UI 插件依赖核心插件公式插件依赖核心插件所以核心必须最先注册。4.3 注入初始数据与权限规则表格初始化的时候你需要把业务数据灌进去同时把权限规则也配置好。数据格式是 univer 定义的一套结构核心是单元格的值和样式。const initialData { id: report-sheet, sheetName: 数据填报表, cellData: { 0: { 0: { v: 姓名 }, 1: { v: 部门 }, 2: { v: 本月工时 }, 3: { v: 备注 } }, 1: { 0: { v: 张三 }, 1: { v: 研发部 }, 2: { v: }, 3: { v: } } } }; const permissionConfig { editableColumns: [2, 3], editableRows: { start: 1, end: 50 }, readOnlyStyle: { bg: { rgb: #f5f5f5 } } };这里cellData的键是行号和列号从 0 开始。v字段是单元格的值。权限配置里editableColumns指定第 2、3 列可编辑也就是“本月工时”和“备注”两列其他列只读。4.4 实现权限判断与样式应用把权限规则和渲染结合起来需要在插件里做两件事一是拦截编辑命令二是给只读单元格应用样式。class CellPermissionPlugin { constructor(rules) { this.rules rules; } onStart() { // 拦截编辑命令 this.disposables.push( this._commandService.interceptCommand( sheet.command.set-editing, (command) this._canEdit(command.params.range) ) ); // 监听渲染事件应用只读样式 this._renderService.onCellRender((cell) { if (!this._canEdit(cell)) { cell.style.bg this.rules.readOnlyStyle.bg; } }); } _canEdit(cell) { const { row, col } cell; if (!this.rules.editableColumns.includes(col)) return false; if (row this.rules.editableRows.start) return false; if (row this.rules.editableRows.end) return false; return true; } }这段代码里interceptCommand负责拦截onCellRender负责样式。两者配合用户既点不进只读单元格也能一眼看出哪些格子不能填。4.5 服务端校验不能省前端拦截只是体验层面的真正要保证数据安全服务端必须再校验一遍。因为前端代码是暴露给用户的懂技术的人完全可以绕过前端直接调接口提交数据。服务端校验的逻辑和前端保持一致拿到提交的数据后逐单元格判断是否在允许编辑的范围内如果有越权修改直接拒绝整个提交并返回错误信息。这一步在 Node.js 侧做可以复用同一套权限规则定义避免前后端规则不一致。function validateSubmission(submittedData, rules) { for (const [row, cols] of Object.entries(submittedData)) { for (const [col, value] of Object.entries(cols)) { if (!isEditable(Number(row), Number(col), rules)) { return { ok: false, message: 单元格 ${row},${col} 不允许修改 }; } } } return { ok: true }; }提示前后端共用权限规则的时候最好把规则抽成一个独立的配置文件或者常量模块两边都引用同一份。我见过太多项目前后端各写一套规则结果上线后发现对不上用户能改的格子被服务端拒了排查起来很费劲。5. 常见问题与排查技巧实录5.1 初始化报错找不到依赖这是最常见的问题通常有两个原因。一是插件注册顺序不对依赖的插件还没注册就注册了依赖它的插件。二是包版本不匹配核心包和插件包的大版本号不一致。排查方法很简单先看报错信息里提到的插件名确认它依赖的插件是否已经注册。如果顺序没问题就去 package.json 里检查所有 univer 相关包的版本号确保大版本一致。我一般会在安装的时候统一指定版本比如全部用^0.1.0这种。5.2 只读单元格仍然可以粘贴内容拦截了编辑命令但用户还是可以通过复制粘贴往只读单元格里塞数据。这是因为粘贴走的是另一条命令链路你只拦截了编辑命令没拦截粘贴命令。解决办法是把粘贴相关的命令也拦截掉。univer 里粘贴通常涉及sheet.command.paste这类命令你需要一并监听。更稳妥的做法是在数据提交前做一次全量校验把越权修改的单元格过滤掉或者直接拒绝提交。5.3 大数据量下滚动卡顿虽然 Canvas 渲染比 DOM 快很多但如果你的数据量特别大比如十万行以上滚动还是可能卡。这时候需要开启虚拟滚动只渲染可视区域内的单元格。univer 的渲染层支持视口裁剪但需要你确认是否开启了相关配置。另外公式计算如果放在前端大数据量下也会拖慢渲染。这种场景建议把公式计算移到 Node.js 服务端前端只拿计算结果。5.4 权限规则动态变化后不生效有些场景下用户的角色会变权限规则需要动态更新。如果你只是在初始化时配置了规则后续更新规则后表格不会自动刷新。解决办法是在规则变化后主动触发一次重渲染。univer 提供了刷新视图的接口调用一下就能让新的权限规则生效。同时记得把拦截命令里的规则引用也更新掉否则拦截逻辑还是用旧规则。问题现象可能原因解决方向初始化报依赖错误插件顺序或版本不匹配检查注册顺序统一版本号只读格可粘贴未拦截粘贴命令补充拦截粘贴链路滚动卡顿未开虚拟滚动或前端算公式开启视口裁剪公式后移规则更新不生效未触发重渲染主动刷新视图并更新规则引用5.5 协同场景下的权限冲突多人协同的时候权限控制会更复杂。比如 A 用户正在编辑某个单元格B 用户的权限规则突然变了导致这个单元格对 B 变成只读。这时候需要有一套机制来处理正在进行的编辑操作。我的经验是权限变更时不要粗暴地中断正在进行的编辑而是等当前编辑提交后再应用新规则。同时服务端要做好冲突检测如果两个用户同时改了同一个单元格要有明确的合并策略。6. 一些实操心得与扩展思路做这类表格权限控制的项目我踩过的坑主要集中在“边界情况”上。比如合并单元格的权限怎么算如果 A1 和 B1 合并了A1 可编辑但 B1 只读这个合并格到底能不能编辑我的处理方式是合并单元格的权限取所有组成单元格的并集只要有一个可编辑整个合并格就可编辑。这个规则不一定适合所有场景但至少逻辑清晰用户容易理解。另一个心得是关于性能的。权限判断如果每次渲染都跑一遍完整规则在大表格下会有开销。我的做法是把权限判断结果缓存起来规则不变的情况下直接查缓存。缓存失效的时机就是规则变更的时候这时候清空缓存重新计算。扩展思路上这套权限模型可以进一步做成基于角色的访问控制。不同角色对应不同的规则集用户登录后根据角色加载对应规则。再进一步可以做成基于表达式的动态权限比如“只有当 A 列的值等于某个特定值时B 列才可编辑”。这种动态权限在审批流场景里很常见实现上就是在权限判断函数里多一层对当前行数据的读取。最后分享一个小技巧调试权限问题的时候可以在开发环境里加一个开关打开后把所有只读单元格用红色边框标出来可编辑的用绿色边框。这样一眼就能看出规则有没有生效比对着代码猜要快得多。上线前记得把这个开关关掉或者只在开发环境启用。