
1. Univer是什么为什么值得关注1.1 从Excel到Web表格的技术路线变化在线电子表格这个领域前几年最热闹的还要数Luckysheet一时间铺天盖地的国产开源Excel方案都围着它转。但Luckysheet后来基本停止维护了它的核心思想和技术积累并没有消失而是以一个新项目的形态继续演进这个项目就是Univer。Univer定位是面向Web的办公套件前身继承了不少Luckysheet的设计思路但底层完全是用TypeScript重写过的。它最鲜明的特点是整个表格区域用Canvas分层渲染而不是传统的DOM节点渲染。你滚动表格、拖动选区、输入内容时看到的所有画面本质上都是绘制在Canvas上的图形这样做的好处是单元格数量大时性能不会像DOM方案那样迅速崩塌。对我这种做了多年B端项目的人来说Univer真正吸引我的地方在于它的插件化和命令化架构。整个编辑器不是一堆代码拧在一起的大杂烩而是拆成核心引擎、渲染引擎、UI插件、公式引擎、数据校验插件等模块。你要公式就挂公式插件要数据校验就挂数据校验插件不用的功能可以不加载项目体积和复杂度都可以控制。1.2 Univer的核心能力一览如果只用一句话概括Univer能让你在浏览器里跑一个接近Excel体验的表格编辑器。拆开来看它目前比较成熟的能力包括电子表格核心单元格编辑、选区操作、行列调整、合并单元格、冻结窗格这些基础操作是养家糊口的本事做得比较扎实。公式系统内置了几百个函数支持跨工作表引用公式引擎是独立的一个模块计算和渲染分离。数据校验下拉列表、数字范围、文本长度、自定义公式校验这个能力在填单场景里特别重要。条件格式、图表、透视表复杂数据分析场景能撑起来。单元格与工作表保护这是这篇文章的重点也是实现“用户只能填指定单元格”的关键机制。协同编辑Univer有协同设计的底子虽然生产环境中多数团队还是用“写数据再刷新”的方式但如果你要上多人实时协作它有对应的能力基础。还有一个很实际的优势Univer官方提供了一个在线Playground你打开官网就能直接在一个Demo页面里试玩表格的全部交互。很多团队评估方案时第一步不是看文档而是先上手玩一玩这步它做得很好。1.3 为什么选Univer而不是其他方案市面上Web表格方案一大堆我简单拉个对比你就明白我的取舍逻辑了方案许可证技术路线维护状态适合场景UniverApache 2.0TypeScript Canvas 插件化活跃迭代快需要深度定制、私有化部署的B端产品LuckysheetMIT早期Canvas方案基本停更老项目维护、少量新项目Handsontable商业授权部分免费DOM渲染稳定数据录入型表格功能相对轻x-spreadsheetMITCanvas功能简单轻量需求快速演示Handsontable的功能边界比较轻对复杂公式和图表支持有限而且商用要买授权。x-spreadsheet更像是一个“能看的表格”真要拿去做复杂业务你会发现处处受限。Luckysheet虽然是MIT协议但代码已经停滞遇到浏览器兼容问题或者想新增功能只能自己硬啃。Univer出身于Luckysheet团队Apache 2.0协议改起来压力小而且项目本身还在高速迭代。我做技术选型时有一个朴素标准如果一个开源项目半年没有新提交、Issues堆积成山那不管它现在多好用未来都是风险。Univer目前显然不在这个名单里。2. 核心需求拆解用户只能编辑指定单元格2.1 这个需求背后的真实场景我做过的项目里这个需求出现得非常频繁而且形态高度一致。背景通常是某个部门提供一份Excel模板希望外部协作方填几个字段但绝对不能让他们破坏模板的格式和结构。典型场景包括人事部门发员工信息收集表员工只需填姓名、部门、联系方式其他列是系统自动计算或管理员维护的。财务部门做费用报销填报用户填金额和事由审批状态、流水号、税额计算列是锁死的。供应链场景中供应商填报价一旦提交就不能改动模板里的产品清单和报价公式。这种需求在Excel里是老传统了先保护工作表再把允许编辑的单元格设置为不锁定。Univer继承了这个交互模型所以你在Univer里做同样的事情思路完全一致——先解锁白名单区域再打开工作表保护开关。这两个动作缺一不可很多人踩坑就是只做了其中一个。2.2 工作表保护的工作机制Univer里的保护机制分两级理解清楚这两级后面写代码就不会糊涂第一级是工作表保护Worksheet Protection。它像一个总闸开启后整张表进入受保护状态普通用户不能对锁定的单元格做任何修改。同时它还管着一堆细粒度权限能不能选择锁定单元格、能不能插入行列、能不能删除行列、能不能排序、能不能用筛选这些都可以单独开关。第二级是单元格锁定状态Cell Locked。每个单元格样式里都有一个保护属性默认是锁定状态。当工作表保护开启时“锁定”的单元格不可编辑反过来如果某个单元格样式里把锁定状态解除了即使工作表保护开着它依然可以编辑。这两个机制配合起来就是经典的白名单模式默认全部锁定然后把需要用户填写的区域逐个解锁。你的业务逻辑就是在生成模板时循环你要放开的几个范围把对应单元格的保护属性改掉最后调用保护命令开启总闸。2.3 数据校验是兜底输入质量的关键单元格锁定解决的是“能不能改”的问题数据校验解决的是“改成什么”的问题。填单场景里这两件事必须同时做因为你可以让用户填写“部门”这一格但用户如果是自由文本输入他填“研发”、“研发部”、“研发一部”都可能数据一多统计时就是一场灾难。Univer的数据校验插件支持常见的校验规则整数、小数、日期范围、文本长度还有一个特别实用的列表校验——也就是下拉选择。下拉校验的公式格式和Excel一致比如研发部,产品部,市场部这样用户点开单元格只能从这三个值里选既保证了数据整洁又减少了输入成本。数据校验还可以配置是否允许为空、错误时是否弹提示这些细节在真实场景里都很重要。2.4 容易被忽略的配套细节在实际做模板时有几个配套细节特别容易被忽略。第一个是隐藏列和隐藏行。模板里经常需要放一些辅助计算列这些列用户不应该看到但公式可能引用它们。正确做法是把辅助列隐藏起来同时保持工作表保护状态这样用户既看不到也改不了。第二个是公式的保护语义。你可能会把“税额金额*税率”这种计算逻辑直接写在单元格里。如果只是锁定这个单元格用户虽然不能改结果但双击单元格还是可能看到公式内容。在Excel里这叫“隐藏公式”功能Univer对公式单元格也有类似的隐藏控制属于单元格保护属性的一部分做财务类模板时这个一定要开。第三个是从安全角度说的前端锁定不是安全边界。任何前端限制都只防君子不防小人因为请求还是可以伪造。后端在接收提交数据时必须对每个字段做白名单校验不能信任前端传过来的任何字段是否“允许修改”。我见过不止一个项目只做了前端锁定、后端直接把整张表数据落库结果被懂行的用户绕过界面改了锁定列这种事情一旦发生就是事故。3. 实操搭建Univer填单应用3.1 工程初始化与依赖安装我这边以Vite Vue 3工程为例React版本思路完全一样因为Univer本身就是框架无关的官方对Vue和React都有示例。先创建一个基础工程pnpm create vite univer-form-demo --template vue-ts cd univer-form-demo pnpm install然后安装Univer相关依赖。这里我按目前主流的包结构给你列一份如果你安装的版本更新包名可能会有调整这一点后面细说pnpm add univerjs/core univerjs/design univerjs/engine-render univerjs/engine-formula pnpm add univerjs/ui univerjs/sheets univerjs/sheets-ui univerjs/sheets-formula pnpm add univerjs/sheets-data-validation如果你是赶项目、不想纠结插件拆分Univer官方还提供了预设包preset一行注册就能把常用插件挂全适合先跑通流程再逐步拆。3.2 初始化Univer实例初始化代码放在一个独立模块里避免在组件里反复创建实例。最基础的初始化长这样import { Univer } from univerjs/core; import { RenderEngine } from univerjs/engine-render; import { UniverUI } from univerjs/ui; import { UniverSheetsPlugin } from univerjs/sheets; import { UniverSheetsUI } from univerjs/sheets-ui; import { UniverSheetsFormulaPlugin } from univerjs/sheets-formula; import { UniverFormulaEnginePlugin } from univerjs/engine-formula; import { UniverSheetsDataValidationPlugin } from univerjs/sheets-data-validation; import { defaultTheme } from univerjs/design; import zhCN from univerjs/locale/zh-CN.json; const univer new Univer({ theme: defaultTheme, locale: zhCN, locales: { zhCN }, }); univer.registerPlugin(RenderEngine); univer.registerPlugin(UniverUI, { container: document.getElementById(app)!, }); univer.registerPlugin(UniverSheetsPlugin); univer.registerPlugin(UniverSheetsUI); univer.registerPlugin(UniverFormulaEnginePlugin); univer.registerPlugin(UniverSheetsFormulaPlugin); univer.registerPlugin(UniverSheetsDataValidationPlugin);注意一点容器元素的尺寸必须有确定值。很多人初始化后表格不显示八成是挂载的div高度是0Univer不会帮你撑高度。我习惯给容器写死一个高度或者做自适应时先测好尺寸再初始化。3.3 创建带模板数据的表格Univer创建表格数据用的是一套JSON结构和Luckysheet的数据格式一脉相承。我直接贴一个最简单的员工信息收集模板三列十几个行表头给出来内容区域留白univer.createUniverSheet({ id: employee-form-001, name: 员工信息收集表, sheetOrder: [sheet-01], sheets: { sheet-01: { id: sheet-01, name: 信息填写, rowCount: 200, columnCount: 26, cellData: { 0: { 0: { v: 姓名 }, 1: { v: 部门 }, 2: { v: 联系方式 }, }, 1: { 0: { v: }, 1: { v: }, 2: { v: }, }, }, }, }, });这里不理解的读者也不用慌cellData是一个二维映射外层键是行号内层键是列号v是单元格的值。实际项目中模板数据通常是从后端接口拿的你拿到JSON后直接塞给createUniverSheet就行。这也意味着模板本身可以去后台维护后端改了一版前端渲染出来就是新模板无需发版。3.4 实现单元格锁定与工作表保护现在到重头戏让用户只能填写指定区域。上面分析过核心就是两步先把允许填写的区域解锁再开启工作表保护。第一步把需要填写的区域比如第二行到第十行的前三列全部解锁。这里我用的是命令服务的方式这样的好处是改动会正确进入Undo/Redo链路不会把内部状态搞乱import { ICommandService } from univerjs/core; import { SetRangeValuesMutation } from univerjs/sheets; const commandService univer.getCommandService(); async function unlockRange(sheetId: string, startRow: number, startColumn: number, endRow: number, endColumn: number) { await commandService.executeCommand(SetRangeValuesMutation, { sheetId, range: { startRow, startColumn, endRow, endColumn, }, values: { // 这里需要按行按列构造单元格对象设置保护属性为“不锁定” }, }); }第二步开启工作表保护。这一步相当于Excel里“审阅-保护工作表”命令大致如下await commandService.executeCommand(SetWorksheetProtectionMutation, { sheetId: sheet-01, protection: { lock: true, allowSelectLockedCells: false, allowSelectUnlockedCells: true, }, });这里我要特别提醒Univer的版本迭代非常快我上面写的命令名和参数结构是当前版本的写法你实际安装的版本可能有出入。遇到类型报错或者命令找不到不要死磕直接去查你锁定的版本对应的文档和类型定义。代码结构和思路是稳的但API名字这种东西版本一升级就可能变。表格初始化后你直接在界面上点那些解锁的区域是可以正常输入的点表头或者其他锁定区域会提示这格不能编辑。整个交互和Excel保护工作表之后的体验非常像。3.5 给可编辑区域加下拉校验单元格解锁只是让用户能填数据规范性要靠数据校验来管。我给“部门”这一列B列设置一个下拉列表让用户只能从预设部门里选await commandService.executeCommand(SetDataValidationCommand, { ranges: [ { startRow: 1, startColumn: 1, endRow: 199, endColumn: 1, }, ], rule: { type: list, formula1: 研发部,产品部,市场部,财务部,人力资源部, allowBlank: true, showErrorMessage: true, }, });这样用户在B列点击单元格时会看到下拉箭头只能从给定的五个部门里选择想乱填就填不进去。如果你不想写代码配置校验Univer的UI界面上也提供了数据校验入口鼠标点一点就能配置。生产环境我更推荐代码配置因为模板是动态生成的每个客户部门列表都不一样写死在界面上没法维护。3.6 获取填写结果与导出用户填完数据前端要做两件事一是把数据回传给后端二是如果业务需要导出成Excel文件。读取用户填写的区域用Univer的Range APIconst workbook univer.getUniverInstance(UniverInstanceType.UNIVER_SHEET); const sheet workbook?.getActiveSheet(); const range sheet?.getRange(1, 0, 10, 2); // A2:C11 const values range?.getValues();拿到的是一个二维数组你遍历之后组装成提交数据发给后端接口就行。导出Excel文件在Univer里也有对应的命令注册了导出插件后调用导出命令就能让用户下载.xlsx文件。我做的项目还有一个变体需求提交前先让用户确认一遍我通常不导出文件而是把填好的数据渲染到一个解析页面让用户核对确认后再落库。这个做法多一步开发但能省掉很多“我填错了你帮我改回来”的扯皮。4. 常见问题与排查技巧4.1 锁定和保护不生效这是这个需求下遇到最多的问题排查路径基本是固定的。先确认你是不是两个步骤都做了单独解锁单元格没开保护等于没锁单独开保护但没解锁区域用户连该填的格子也点不动。第二个常见原因是版本API变更导致保护命令根本没执行成功。你可以打开控制台看命令返回结果是成功还是报错。因为Univer的命令系统所有操作都有返回状态排查时不要只看界面先看日志。第三个原因藏得比较深如果你的模板是通过导入xlsx文件得到的工作簿那么导入进来的单元格样式可能被文件里的格式覆盖。也就是说你在前端代码里“解锁”了某个区域但导入的文件里恰好那个区域被设置了锁定样式两边打架最终表现不可预期。我的建议是模板尽量用Univer的JSON结构定义不要依赖导入或者导入后统一刷一遍目标区域的样式。4.2 公式显示#NAME?或者不计算看到表格里的公式不计算先检查公式引擎插件有没有注册。Univer的公式是独立引擎不注册UniverFormulaEnginePlugin和UniverSheetsFormulaPlugin公式就只是纯文本不会出结果。还有一个容易踩的坑是公式计算是异步的。如果你在创建表之后立刻读单元格公式结果可能还没算出来拿到的是空值或#NAME?。正确做法是等公式计算完成事件之后再去取值或者主动触发一次计算再读取。4.3 表格渲染空白或错位我遇到过一次非常典型的渲染问题表格所在的页面是Tab切换结构表格初始化时所在的Tab是隐藏状态容器宽度为0。Univer在容器不可见时执行了布局计算切回来之后Canvas没有重新测量尺寸于是表格显示不全或者渲染出来一块空白。解决方案分两种一种是等Tab切到前台、容器可见后再初始化Univer这是最省心的方式另一种是初始化后监听容器尺寸变化调用Univer的刷新接口重绘。如果你做的是响应式布局窗口缩放后表格错位多半也是同一个问题——容器resize后没有通知渲染引擎重算。4.4 大数据量下的性能取舍Univer的Canvas渲染比DOM方案强很多在常规数据量几千行下表现很流畅。但如果你硬塞进去几十万行、每行还带公式和样式那任何前端表格方案都会吃力。我在实际项目里的处理方式是分层用户看到的是一个“待办视角”的表格只展示和当前用户相关的行范围真正的全量数据在后端。换句话说表格当交互层用数据存取走服务端接口需要聚合统计时后端算好结果再回填表格。这种架构下表格的渲染压力被控制住了同时功能完整度没有打折扣。4.5 权限模型要前后端配合设计最后再强调一遍权限问题。我用Univer实现的单元格锁定本质是前端交互层面的约束它服务于用户体验也拦截了大部分误操作但它不是安全防线。一个稳妥的设计是后端下发模板时同时下发一份“可编辑区域清单”的元数据前端拿到清单后动态把这些区域解锁并开启保护用户提交时后端根据同一份清单做字段级校验不接受清单之外的字段修改。这样即使有人绕过前端直接POST脏数据后端也会拒绝。审计方面我建议给提交记录加上操作时间、提交人、设备信息特别是财务、人事这类敏感场景出现纠纷时至少能追溯。个人体会把Univer用在填单场景前后做了快两个月整体感受是它把Excel里最核心的交互模型搬到了Web上而且做得足够灵活。我最开始走弯路的地方就是老想着“前端一把梭”试图在表格初始化后硬改内部数据模型结果要么样式对不上要么保护状态混乱。后来想通了Univer的命令系统就是设计来干这个事的遵循它的规则走比自己绕开接口改内部状态省心得多。如果你要接新的填单项目我建议先别急着写业务代码把这篇文章里的锁定逻辑和数据校验思路在一个Demo页面上跑通再往后端接口对接。另外一个小建议Univer版本更新很频繁项目里最好把依赖版本锁死升级前先在分支上验证一遍核心功能别让“顺手升级”变成“上线上火”。表格类功能的回归点又多又碎版本升级的谨慎程度值得参考银行系统改核心的那种心态。