最近因为要做一套“用户自定义收集表格”的功能我把 Univer 这个在线表格开源库从头到尾完整试了一遍。需求说起来很简单领导或者管理员先在页面上定义一张表把表头、说明文字、下拉选项都配好然后把这张表发给其他人填写填写的人只能往指定的格子里录入内容其余位置一律不允许动。就是这样一个听起来不难的“填表”场景真落到 Excel、WPS、老牌表格控件这些方案上时每个都有让人难受的地方。Univer 是这段时间搜索热度上升最快的在线表格基础库它给我的第一印象不是“又一个表格工具”而是一套可以嵌进自己业务系统的表格基础设施。这篇东西算是我完整跑通 Univer 的记录包含环境准备、权限控制思路、后端联动方案以及几个我在实际项目里踩到并解决的坑。1. 为什么我会把 Univer 当成“表格基础设施”而不是又一个在线表格1.1 它解决的是“业务系统需要一张活表”的问题过去做在线填表大多数团队会走两条路。第一条路是直接用 Excel 文件后端生成一个模板文件用户下载后本地填写再上传回来。这条路看着简单实际上从模板生成、格式校验、数据清洗到版本管理每一步都在给项目埋雷。用户改了不该改的表头把公式删了或者填了一堆根本不合逻辑的数据你都得事后靠脚本去救。第二条路是自己基于 HTML 画一个“看起来像表格”的表单界面。这在小规模场景下还凑合但一旦涉及行数很多、单元格之间有关联计算、需要粘贴批量数据、要呈现冻结行列和筛选视图自研成本就会疯涨。Univer 在这两条路之外提供了一种新选择它是一个以 TypeScript 为核心的办公套件 SDK可以在你的页面里直接渲染出一个功能完整、体验接近桌面表格的在线表格。它不是 SaaS不要求你把数据放到某个特定平台而是把一个表格“组件”完整地交给你由你自己的业务系统决定数据从哪来、存到哪去。对做填报表、台账、项目跟进表这类场景来说相当于把最难的“表格引擎”外包掉了你只需要专注自己业务逻辑那一层。1.2 它拆出来的三个核心能力我理解 Univer 的能力模型可以拆成三层文档模型层Core/Sheets负责表格里单元格、行列、样式、合并、公式、数据验证这些数据的存储与计算在整个 Univer 架构中相当于表格的大脑渲染引擎层RenderEngine负责把文档模型画到浏览器里处理选区、滚动、缩放、编辑框这些交互相当于表格的眼睛和手业务插件层UI 相关插件负责菜单、工具栏、右键菜单、弹窗等界面逻辑让表格有完整的人机交互体验。日常问题里的“用户能不能改这个单元格”就同时涉及文档模型里的锁定位、渲染引擎里的编辑器交互以及权限配置。你只改样式不够只改交互也不够这也是很多人在实践中最容易搞混的地方。我后面会专门讲这层拆解。1.3 开源协议和商业使用的边界Univer 主仓库采用的许可证以仓库里实际包含的 LICENSE 文件为准主要包遵循开源许可方式发布。这意味着你可以把它拉下来看源码、改源码、部署到自己的服务器上不用向某个平台注册或申请。但有两个细节需要注意一是如果你修改了核心代码并对外提供服务可能需要按开源协议要求保留版权声明并公开修改后的源码二是 Univer 仓库里可能有部分字体、图标或设计资源遵循不同许可使用时要留意。团队在决定是否采用前建议把项目根目录的 LICENSE 和 NOTICE 文件都过一眼这是正规流程别跳过。2. 十分钟跑通 Univer 工作区从安装到渲染出第一个格子2.1 环境准备和包依赖我用的环境是 Node 18 以上前端脚手架是 Vite。Univer 本身对构建工具没有特别要求Webpack、Vite 都能正常跑。安装时先按需引入这几个包univerjs/core核心模型包括工作簿、工作表、单元格、样式、公式引擎univerjs/sheets电子表格插件提供表格文档模型的核心能力univerjs/sheets-ui电子表格的用户交互界面包括选区、编辑框、右键菜单univerjs/sheets-render渲染引擎负责在 canvas 上绘制表格univerjs/sheets-formula公式引擎需要计算能力时引入univerjs/ui和univerjs/design基础 UI 框架与设计语言。安装命令npm install univerjs/core univerjs/sheets univerjs/sheets-ui univerjs/sheets-render univerjs/ui univerjs/design如果你要用数据验证下拉、批注、查找替换、条件格式这些能力还要从univerjs/sheets-*系列里找对应插件。Univer 的包粒度比较细好处是你不会把没用的代码打进来坏处是刚入门的人容易被包名搞晕。我的建议是先只装核心六个包跑通一个空表格再按功能需求逐个加插件不要一上来全装。2.2 初始化一个最简工作簿我接入时写的第一版初始化代码大致是这样的import { Univer, LocaleType } from univerjs/core; import { UniverSheetsPlugin } from univerjs/sheets; import { UniverSheetsUIPlugin } from univerjs/sheets-ui; import { UniverRenderEnginePlugin } from univerjs/sheets-render; import { UniverUIPlugin } from univerjs/ui; const univer new Univer({ locale: LocaleType.ZH_CN, }); univer.registerPlugin(UniverRenderEnginePlugin); univer.registerPlugin(UniverUIPlugin, { container: univer-container, }); univer.registerPlugin(UniverSheetsPlugin); univer.registerPlugin(UniverSheetsUIPlugin); const workbook univer.createUniverSheet({ id: collect-sheet, sheetOrder: [sheet-1], sheets: { sheet-1: { id: sheet-1, name: 收集表, rowCount: 100, columnCount: 20, cellData: {}, }, }, });页面里只需要放一个div iduniver-container/div样式高度自己控制。这一版跑起来后你能得到一张 100 行 20 列、可以点击和输入内容的在线表格。我当时试完第一反应是“这比我想象中轻。” 它的交互流畅度、选区绘制、滚动跟手程度都已经是可以直接拿给业务方看的水平。2.3 从业务 JSON 生成表格数据Univer 创建表格时接受一段 JSON 来描述整个工作簿。单元格的定位是按{行号: {列号: 数据}}来组织的行号和列号都从 0 开始。例如我想做一个报名登记表把姓名、手机号、报名时间这几个表头放在第一行初始数据按下面的结构传const sheetData { id: collect-sheet, sheetOrder: [sheet-1], sheets: { sheet-1: { id: sheet-1, name: 报名登记, rowCount: 200, columnCount: 10, cellData: { 0: { 0: { v: 姓名 }, 1: { v: 手机号 }, 2: { v: 报名时间 }, }, }, }, }, }; const workbook univer.createUniverSheet(sheetData);这样你就拥有了一张带标题的空白表。核心的一点是Univer 接受的是“工作簿快照”也就是说你传给它的 JSON 既是创建参数也是数据持久化格式。换句话讲你把这份 JSON 存到数据库里下次打开时原样丢回来页面就能恢复成之前的状态。这就是我后面做“Univer 在线”时数据持久化最简单的入口。2.4 渲染引擎的一些使用感受Univer 的渲染是用 canvas 做的这点和很多传统表格控件用 DOM 逐格渲染有本质区别。好处是就算你建一个几千行的大表滚动起来也不会因为单元格数量太多而把浏览器拖垮。但代价也很明显你不能用 CSS 去改单元格内部的表现所有“长得好看”的需求都得通过单元格样式协议来控制。另一个体验是如果你在container还没布局完成时就注册 UIPlugin表格可能会出现宽高计算不对的问题。我习惯在window.onload之后再初始化或者用ResizeObserver监听容器变化并调用刷新。3. “能填的格子”和“不能填的格子”Univer 里拆成三层来看3.1 先拆需求用户口中的“不能改”其实有三种含义我拿业务方的原话去对实现方案的时候发现“其他单元格用户无法修改”这句话在不同人嘴里意思不一样。归纳下来有三类第一类单元格内容不可被鼠标选中并修改也就是只读锁定第二类单元格可以看但我不想让它出现在编辑状态里比如“可以复制但不许改”第三类单元格允许被改但要满足格式约束比如只能在几个选项里选不能乱填。这三类需求在 Univer 里对应的实现手段完全不同。如果你不拆开写出来的代码很容易是“看起来限制了但绕一下就能破”。下面我按层讲。3.2 第一层单元格样式锁定 工作表保护Univer 的单元格样式协议里有一个锁定位。你可以把它想象成 Excel 里的“锁定单元格”它只是给每个格子打了一个标真正让锁定生效的前提是“工作表处于保护状态”。实际做法是创建表格时把不允许用户编辑的表头、说明区域、公式区域统一打上锁定标记其余需要用户填写的空格保持默认不锁定然后对整个工作表开启保护。开启保护后所有被打过锁定标记的单元格都会变成不可编辑状态而未锁定的单元格仍然可以正常录入。在 Univer 丢数据时单元格样式可以放在单元格数据的s字段里大概是这样const sheetData { sheets: { sheet-1: { cellData: { 0: { 0: { v: 姓名, s: { locked: true, fill: { rgb: #f2f2f2 } } }, 1: { v: 手机号, s: { locked: true } }, 2: { v: 报名时间, s: { locked: true } }, }, 1: { // 这一行没有 locked 标记允许填写 0: { v: }, 1: { v: }, 2: { v: }, }, }, // 开启保护 protection: { protect: true, }, }, }, };我再补充一点protection的具体字段名在不同小版本里有过调整有的版本需要把权限信息写完整。如果你用的版本和网上贴的不一致最稳妥的方式是查你本地安装版本的 .d.ts 类型定义文件CtrlF 搜 “protection” 或者 “Protection”一两分钟就能锁定正确字段。Univer 尚在快速迭代期网上抄来的代码报类型错误很正常。3.3 第二层拦截编辑行为堵住“复制粘贴”和“键盘敲入”的路只有单元格锁定还不够。实际产品里用户尝试绕过“只读”的方式很多全选后按下 Delete、选中锁定区域直接输入、通过复制粘贴把内容覆盖进去。Univer 保护模式对常规操作确实有效但如果你希望某些操作在交互层就直接被拦掉可以在命令执行前加一层监听Univer 的核心是基于命令模式工作的任何编辑行为都会先产生一个命令部分版本允许你在命令真正执行前介入并取消它。通用思路是注册一个命令通道拦截器监听beforeCommandExecute之类的钩子判断当前选区是否落在“允许编辑”的范围外如果是就直接阻止并给页面一个提示const allowedRange { startRow: 1, endRow: 199, startColumn: 0, endColumn: 9 }; function isRangeAllowed(range) { return range.startRow allowedRange.startRow range.endRow allowedRange.endRow range.startColumn allowedRange.startColumn range.endColumn allowedRange.endColumn; }这种“允许范围”的写法实际效果是把整张表变成了一个表单引擎只有命中范围的区域可以编辑其余一律拦截。这比单纯依赖样式锁更符合“部分格子可填”的业务语义因为你能在拦截器里拿到操作的具体信息可以做弹窗提示、写审计日志、就连“谁在什么时候尝试改了不该改的地方”都能记录下来。我这套逻辑后来被我单独抽成了一个工具函数接进四个项目里复用效果稳定。3.4 第三层用数据验证约束用户填进去的内容能改的格子只能算第一步填进去的东西合不合法是第二步。Univer 的电子表格插件支持数据验证也就是给某个区域预设“只能是数字”“只能从下拉列表选择”“必填”这些规则。在做报名表这类场景时我给手机号那一列加了“11 位数字校验”给城市那一列加了下拉选项。用户填错时表会直接给出提示从源头降低脏数据。在业务里我建议把三套机制组合使用下面这张表是我自己总结的对应关系用户实际需求主要手段补充手段表头、公式、说明文字不可改单元格锁定 工作表保护命令拦截器兜底只能填指定区域允许范围拦截设置选区样式高亮填的内容必须是数字或选项数据验证提交前二次校验填写后不可再修改提交态锁定整表后端拒绝变更请求3.5 给“自定义表格”业务的设计建议回到热搜词里的“用户定义表格”Univer 并不会自动把你变成“填表类应用”它只提供表格能力定义流程需要你自己写。我的做法是先把“定义表格”和“填写表格”拆成两个模式管理员进入“设计模式”时创建表格、写表头、设置锁定和校验规则填写人进入“填写模式”时表格被加载为受保护状态只有允许格子可以编辑。一份同一个工作簿 JSON两种界面入口逻辑互不干扰这是我认为最省力的架构。4. “Univer 在线”背后的持久化与权限联动方案4.1 纯前端渲染不代表能脱离后端很多人一听说 Univer 是纯前端的就以为“在线表格”等于把表格渲染出来就够了实际跑完才发现渲染只是开始。你的表格数据如果只存在浏览器内存里用户一刷新就全没了。所谓“Univer 在线”核心是在线保存和在线协作这少不了后端参与。我的落地方式是后端只做两件事存工作簿 JSON、做权限判断。工作簿 JSON 可以直接落库。考虑到 JSON 字段可能会嵌套很深在 MySQL 里我用json类型字段存取在 MongoDB 里就直接存 document。为了减少传输体积我会把工作簿快照做一层精简只保留当前业务需要的 sheet 和 cellData不把历史版本、临时状态都装进去。4.2 最少改动的一套前后端接口设计在设计后端接口时我尽量避免了“把 Univer 的整个文档模型暴露给每个接口”这种高耦合做法。把接口收敛成四个就够初版跑了POST /api/sheet创建一张表后端生成工作簿 JSON返回表 IDGET /api/sheet/:id加载表返回工作簿 JSON 和管理员设置的填写范围元数据PUT /api/sheet/:id保存表前端把编辑后的单元格数据形成 diff提交给后端POST /api/sheet/:id/submit完成填写后端对数据做最终校验然后锁定整表。前端拿到管理员配置的允许填写范围后把它和一整套工作簿 JSON 一起传给 Univer。这样“哪些格子能填”这个规则既存在于前端展示层也存在于后端校验层。千万别只在页面上限制接口层面也必须做同样的检查否则别人构造一个请求就能绕过前端把数据改掉。4.3 认证与“谁能填哪些格子”的联动权限模型再往下走半步就是具体到“不同用户看到同一张表锁定范围不一样”。比如部门主管填的是审批栏普通员工只填申请栏。Univer 本身不负责你的登录体系它是通过“你给它什么样的工作簿快照”来体现权限差异的。后端在返回工作簿快照时根据当前登录用户的角色动态生成 cellData 里的锁定标记就行。同一个模板给普通员工加载时审批栏是 locked给主管加载时申请栏是 locked。这个方案实现成本非常低因为 Univer 的锁定信息是数据的一部分不是组件状态后端改几个字段就完成了。4.4 部署打包和体积控制Univer 的各插件打包后体积并不小毕竟它是一个完整表格引擎。首屏加载时建议把 Univer 相关的包单独打成 vendor chunk开启 gzip 或者 brotli 压缩。我的实测经验是按需加载插件比试图靠 tree shaking 把核心包拆没更靠谱。如果项目首页不需要表格就把 Univer 初始化代码用动态 import 包起来点击“编辑表格”那一刻再加载。初次商用之前建议在低端安卓机上做一轮渲染测试canvas 渲染在中低端移动设备的滚动手感会跟你开发机上有明显差异但普通填报表场景的数据量一般不会构成瓶颈。5. 实测中让我印象深刻的几个坑和应对方式5.1 看起来锁定了但双击单元格还是能编辑这是我第一次做 Univer 权限控制时最典型的“假成功”现场样式里加了锁定保护也开了但页面一跑双击表单头背后的区域照样能打字。排查到最后发现是两个原因叠加。第一我把单元格的锁定标记塞进了s字段但保护配置跟工作表的id没对上保护等于没生效。第二编辑框的拦截逻辑写在了渲染层的点击事件上而 Univer 的编辑启动有自己的一条命令链路我在错误的位置做了拦截自然拦不住。解决路径很直接先确认保护状态真的打进了文档模型再确认命令拦截器挂在正确的命令总线上。如果你遇到同样问题不要急着改代码先在控制台里把工作簿实例打印出来找到当前工作表的保护配置和允许编辑范围看看和页面上呈现的行为是否一致90% 的怪问题都能在这个环节发现。5.2 全选删除能把“锁定区域”一起删掉单元格锁定的常规理解是“不允许输入”但真实用户的操作习惯千奇百怪。有人会全选整张表后按 Delete或者拖选一大片区域做复制粘贴测试时发现部分场景下锁定区域的内容还是会被改动。这个坑的根源是保护机制和命令拦截器的覆盖范围不同。保护模式能挡住常规的单元格编辑但批量操作时走的是另一套命令路径。我的修法就是前面说的那套拦截器把“允许编辑范围”作为唯一事实来源不管用户是单格编辑、批量粘贴还是区域删除全部套进同一个范围判断函数里。写完后我把常用的破坏性操作全列成一个测试清单包括输入、粘贴、删除、填充、拖拽每一项都实际点一遍确认没有漏网之鱼才算过。5.3 用户填写超长文本把表格布局挤飞Univer 的默认行高列宽是固定计算的用户填入一大段文字后单元格显示效果和自己本地 Excel 不太一样经常出现“字还在但表格看起来乱糟糟”的反馈。后来我在给填写区设置单元格样式时把文本换行和纵向对齐都显式写进去了同时在数据验证里限制了最大长度体验才正常。这个小问题本身不难但它提醒我一个道理在线表格的表单化改造永远要在需求阶段就把“填完长什么样”“填错怎么办”想清楚。5.4 不同小版本之间的 API 差异Univer 处于快速迭代期我写这篇记录时的 API 形态和网上很多旧教程里的写法已经有出入再过几个月可能又不一样。这不算缺陷开源项目快速演进很正常。我的建议是以你实际安装的版本类型定义为准不要盲目复制旧文章里的代码。6. 如果你也想做“可填写的在线表格”最后给你几个实在建议如果让我重新选型一次我仍然会用 Univer但会在一开始就做好三件事第一把工作簿 JSON 当成核心数据资产来管理所有业务逻辑都围绕这份 JSON 展开第二把“可编辑范围”抽象成独立的配置不给业务方直接暴露底层锁定能力第三把前端限制和后端校验当成一件事永远不要只信任页面上那层规则。我最后再分享一个小技巧用 Univer 做填报表时在创建表格的初始数据里把所有需要用户填写的格子先用浅色背景标记出来让用户打开表格一眼就知道“这里可以填那里不能填”。这个设计省下了大量“我该往哪里写”的咨询成本。把表格引擎的细节藏在底层把业务的引导做在明面上这就是“给用户一张表”这件事最舒服的体验。