ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Univer实战:用开源在线表格实现指定区域可编辑的模板表

Univer实战:用开源在线表格实现指定区域可编辑的模板表 Univer 这个开源在线表格项目最近在我常逛的几个前端技术群里反复出现。一句话介绍它是一套基于 TypeScript 构建的在线办公套件核心能力是在浏览器里跑一个接近桌面版 Excel 的表格引擎——多工作表、公式计算、条件格式、复制粘贴、撤销重做都有而且项目整体开源可以自行封装进任何业务系统。我第一次认真研究它是因为一个很实际的业务需求要做一张用户可填写的模板表模板里字段名称、说明文字、计算公式这些区域是固定的用户只能往指定的几个单元格里填内容其他单元格点都点不动。这正是热词里反复提到的那句话——Univer 支持用户定义表格让用户去填写某个单元格其他单元格无法修改。这类需求在报名登记、信息补录、采购申请、客户建档里到处都是。这篇文章不打算泛泛介绍 Univer 有哪些功能而是从选型、环境搭建、单元格锁定、数据校验到踩坑记录完整还原我用它落地可填表单表格的全过程给准备在业务里引入 Univer 的人一条可以参考的路径。1. 选型那几天为什么从 Luckysheet 和 Handsontable 转投 Univer1.1 先把需求边界划清楚在做技术选型之前我把需求拆成了五条硬性标准浏览器端直接可用不需要用户安装任何客户端模板中指定区域必须只读只有授权区域可编辑单元格要支持下拉选项、数字校验等表单级约束填完后能把数据批量取出来提交后端项目要开源能接受二次开发和后续扩展看起来都是很常规的要求但真正去对比的时候才发现能同时满足前两条的开源方案就已经不多了。尤其是锁定整个工作表但开放特定区域这个能力很多表格库要么没实现要么实现得特别别扭。1.2 三个可选方案的横向对比我最早考虑的是 Luckysheet毕竟它名气大很多老项目都引过。但真正跑起来之后发现它的公式能力偏弱而且在允许用户编辑区域这个场景上并没有一个干净的做法更多是停留在整表只读这种粗粒度控制上。Handsontable 交互体验不错但商用授权费用不低而且它更偏向数据网格而不是表格文档——用户对 Excel 的那些习惯动作右键菜单、格式刷、下拉填充在 Handsontable 里需要额外适配。对比项LuckysheetHandsontableUniver开源状态开源但更新缓慢开源版功能受限商用需授权开源且持续迭代单元格只读/锁定支持部分配置能力较弱商业版支持较好免费版受限支持区域保护与编辑拦截公式引擎简单公式可跑一般覆盖常用函数中文社区早期较活跃现在萎缩社区在国外国内团队主导中文资料多二次开发成本底层较老模块耦合有一定成本插件化架构分层清晰Univer 是梦数团队的作品可以理解成 Luckysheet 的下一代重写。我重点看了两件事一是它的整体架构二是它的命令系统。Univer 把数据层和界面层拆得很开所有操作都走命令Command这就为权限控制提供了一个非常干净的切入点——拦截命令就等于拦住了入口。1.3 Univer 的分层模型为什么对权限控制友好你可以把 Univer 想象成一栋房子的结构最底层是核心包univerjs/core负责表格数据模型、命令调度、撤销重做这些水电管线中间是能力插件比如univerjs/sheets提供表格能力本身univerjs/sheets-formula提供公式计算最上层是 UI 插件比如univerjs/sheets-ui渲染工具栏、右键菜单、单元格编辑等界面这个分层不是随便设计的。它意味着你可以在不引入任何 UI 的情况下用纯数据模型去读写单元格也可以在不影响数据层的情况下替换整套 UI 主题。对我们做表格模板的人来说分层带来的直接好处是锁定单元格这种需求可以在数据层拦截也可以在 UI 层隐藏两层配合能达到用户点都点不进去的效果。2. 最小可用工程先把一张模板表跑起来2.1 依赖安装与版本锁定我建议任何新项目在引入 Univer 时都先把版本锁死。因为 Univer 目前还在快速迭代期包名、API、命令 ID 在不同 minor 版本之间都有过变动。我这里以当前主分支的写法为例npm install univerjs/core univerjs/design univerjs/sheets univerjs/sheets-ui univerjs/sheets-formula univerjs/sheets-data-validation装完之后看一眼 package.json尽量把统一版本号固定下来不要直接写^或者~。否则下次 install 可能就给你升到一个 API 不兼容的版本代码里一堆 TS 报错。如果你只用在线表格也可以考虑官方提供的轻量封装包但底层思路是一样的。2.2 初始化 Univer 实例并注册插件以最典型的 React 接入方式为例HTML 里先准备一个挂载点div iduniver-app stylewidth: 100%; height: 600px;/div然后在业务组件里初始化import { Univer } from univerjs/core; import { LocaleType } from univerjs/core; import { defaultTheme } from univerjs/design; import { UniverSheetsPlugin } from univerjs/sheets; import { UniverSheetsFormulaPlugin } from univerjs/sheets-formula; import { UniverSheetsUiPlugin } from univerjs/sheets-ui; const univer new Univer({ theme: defaultTheme, locale: LocaleType.ZH_CN, }); univer.registerPlugin(UniverSheetsPlugin); univer.registerPlugin(UniverSheetsFormulaPlugin); univer.registerPlugin(UniverSheetsUiPlugin, { container: univer-app, });不同版本里 UI 插件的挂载方式可能有差异有的版本是在createUniverSheet之后再调用 UI 渲染有的版本要求容器参数直接传给 UI 插件。如果你在自己的版本里找不到container这个配置项优先去官方 example 目录里找对应的初始化写法那里是最准的。2.3 用 cellData 预置模板静态内容初始化工作簿时可以直接把模板的静态数据带进去。我用员工信息补录登记表举个具体例子univer.createUniverSheet({ id: emp-fill-2025, sheets: [ { id: fill-sheet, name: 登记表, cellData: { 0: { 0: { v: 员工信息补录登记表 }, 1: { v: 填写日期 }, 2: { v: } }, 1: { 0: { v: 姓 名 }, 1: { v: }, 2: { v: 部 门 }, 3: { v: } }, 2: { 0: { v: 入职日期 }, 1: { v: }, 2: { v: 薪资等级 }, 3: { v: } }, 3: { 0: { v: 联系方式 }, 1: { v: }, 2: { v: 备注 }, 3: { v: } }, }, columnCount: 6, rowCount: 20, }, ], });这里的cellData是 Univer 原始数据模型里最常用的入口外层键是行号里层键是列号v代表单元格的值s代表样式。比如{ 0: { 0: { v: 员工信息补录登记表, s: { bg: rgb(241, 245, 249), bl: 1, fs: 14 } }, } }其中bl表示加粗fs表示字号bg是背景色。这些字段在不同版本里会有演化但只要你锁定了版本直接在类型定义里查远比记忆可靠。把模板数据放进去之后一张看起来有内容的表就出来了。但这时所有单元格都还是可编辑的离只能填指定区域还差关键一步。3. 核心环节把全表可编辑变成仅指定区域可填3.1 需求拆解哪些单元格要锁哪些要放回到业务本质一张员工信息补录表第一行是标题第二行到第四行是字段名和输入框交替分布。我们希望做到标题行、字段名列完全只读用户不能选中这些单元格输入区单元格允许用户点击并编辑内容用户复制粘贴时如果目标是只读区域粘贴必须被拦下用户拖拽填充时同理不能跨越只读边界这其实和 Excel 里的允许用户编辑区域Allow Users to Edit Ranges是一回事。Univer 的表格保护功能正好对应这个场景。3.2 正规方案工作表保护加允许编辑区域Univer 较新的版本通过 commandService 执行保护相关命令大致思路是先给工作表上一把锁再配置允许编辑的 range 列表const workbook univer.getUniverSheetInstance(emp-fill-2025); const sheet workbook?.getSheetBySheetId(fill-sheet); if (!sheet) return; // 第一部分启用工作表保护 univer.getCommandService().executeCommand({ id: sheet.command.protect-sheet, params: { sheetId: fill-sheet, protection: { enable: true, password: , lockCells: true, }, }, }); // 第二部分将输入区加入白名单 univer.getCommandService().executeCommand({ id: sheet.command.add-protection-range, params: { sheetId: fill-sheet, range: { startRow: 1, endRow: 3, startColumn: 1, endColumn: 1 }, }, });我这里的命令 ID 是按我锁定的版本写的。必须坦白一个现实Univer 的版本迭代确实快不同版本里保护功能的命令 ID 可能从protect-sheet变成set-worksheet-protection参数结构也可能调整。最靠谱的定位方法是在 node_modules 里搜你版本对应的源码或者直接在编辑器里搜ProtectCommand这类类名。这个建议对任何 Univer 的新手都适用——官方文档更新速度跟不上代码演进速度源码才是最准的文档。3.3 兜底方案拦截修改单元格数据的命令工作表保护是正规军但我还做了一层兜底统一拦截会修改单元格数据的命令。Univer 的所有操作都走命令流包括粘贴、拖拽、导入这些高权限操作。这意味着禁止编辑这件事可以在命令层做全局判断import { CommandType } from univerjs/core; const EDIT_COMMAND_IDS [ sheet.command.set-range-values, sheet.command.set-cell-content, sheet.command.paste, ]; univer.getCommandService().beforeCommandExecute((command) { if (command.type ! CommandType.MUTATION) return true; if (!EDIT_COMMAND_IDS.includes(command.id)) return true; const { range, ranges } command.params ?? {}; const targetRanges ranges ?? (range ? [range] : []); for (const r of targetRanges) { if (!isEditable(r)) { console.warn(拦截了一次越界编辑, r); return false; } } return true; });isEditable函数把模板里定义的输入区整理成一组坐标区间再判断命令的目标 range 是否完全落在输入区内。这里有个细节容易被忽略像粘贴这种命令它携带的 range 可能是一个起点加上行列数实际覆盖区域往往比你以为的大判断时一定要按覆盖的完整矩形来算不能只看起点单元格。我把方案一和方案二叠加使用效果是正常情况下用户根本进不了只读区被保护拦下万一保护配置出了漏洞命令拦截还会二次兜底。这套双保险的思路建议你在接入生产环境时保留。3.4 界面层的视觉降级与选中控制锁定的单元格在界面层也要有明确的视觉反馈。我在模板里做了三件事只读单元格统一设置浅灰背景色输入区保持白色用户一眼就能区分点击只读单元格时用选中态变化事件判断坐标若落在只读区则提示该单元格不可编辑同时清空选中态给输入区的关键列设置数据校验下一节细讲减少无效输入视觉反馈的意义不只是好看它能显著减少用户的挫败感。你拦住了人家的操作总得让人家知道为什么。4. 数据校验与取值让填进来的数据直接能用4.1 下拉选项约束枚举字段像薪资等级这种字段绝不能放任用户自由输入文字。Univer 的 Data Validation 插件支持在指定区域设置校验规则。注册下拉选项的命令大致长这样import { DataValidationOperator, DataValidationType } from univerjs/sheets-data-validation; univer.getCommandService().executeCommand({ id: sheet.command.add-data-validation, params: { sheetId: fill-sheet, ranges: [{ startRow: 2, endRow: 2, startColumn: 3, endColumn: 3 }], rule: { type: DataValidationType.LIST, formulaValues: [P1, P2, P3, P4], operator: DataValidationOperator.EQUAL, allowBlank: false, showErrorMessage: true, errorMessage: 请从下拉列表中选择薪资等级, }, }, });下拉选项会让用户直接从列表里选从源头规避了拼写错误、大小写不一致、部门名称不统一这类数据质量问题。4.2 日期、数字、文本长度的校验规则除了枚举下拉表单化的表格还需要数字和文本长度的约束。比如入职日期必须是一个有效日期联系方式必须是 11 位数字开头。Data Validation 的规则本质上是一组条件 操作符 阈值你可以把 Excel 里大部分常用校验搬到 Univer 里来。我为日期字段配置了日期类型校验为手机号字段配置了正则校验这样提交到后端的数据基本不需要再清洗。有一点要提醒校验规则只能约束在 UI 界面里的正常编辑通过命令 API 直接写入的数据可能会绕过校验逻辑。这也是为什么我坚持在服务端再做一层参数校验的原因。4.3 读取用户填写的结果这是很多人卡住的最后一步。用户填完了前端如何拿到数据提交给后端在 Univer 里最直接的方式是通过工作簿实例读取单元格const workbook univer.getUniverSheetInstance(emp-fill-2025); const sheet workbook?.getSheetBySheetId(fill-sheet); const result: Recordstring, string {}; const FIELD_MAP [ { label: name, row: 1, col: 1 }, { label: department, row: 1, col: 3 }, { label: hireDate, row: 2, col: 1 }, { label: level, row: 2, col: 3 }, { label: phone, row: 3, col: 1 }, ]; FIELD_MAP.forEach(({ label, row, col }) { const cell sheet?.getCellData(row, col); result[label] cell?.v?.toString() ?? ; }); console.log(result); // 输出如 { name: 张三, department: 技术部, hireDate: 2020-03-12, level: P3, phone: 13800000000 }也可以使用getRangeValues一次性读取一个矩形区域返回结果直接交给后端接口。要注意的是如果模板里某些字段设了公式比如自动计算总分读取时应读取计算后的值而不是原始公式字符串。5. 实测中的坑、兜底策略与性能建议5.1 版本演进带来的连锁反应Univer 最让我觉得理解它需要勇气的地方就是版本演进速度。同一个类名在 0.1.x 和 0.2.x 里可能是完全不同的角色命令 ID 更是说变就变。我的建议是一开始就锁死精确版本号不要用^或~把官方仓库里的 example 目录当作第一手参考资料它和你的版本是对应的遇到 API 报错先在 node_modules 里搜类型定义一般半小时内能定位这也是我前文反复强调以你锁定版本的源码为准的原因。网上很多 2023 年甚至 2024 年初的文章拿到现在的版本上大概率跑不起来这不代表文章错只是版本变了。5.2 初始化性能与大数据量体验Univer 是 Canvas 渲染不是 DOM 表格所以上千行的数据不会有太多 DOM 节点压力。但初始化时如果一次性把超大 cellData 塞进去解析和建索引还是会有明显卡顿。我的经验是模板表数据量通常不大尽量把静态内容放在初始化数据里动态用户输入通过命令写入如果真有万行级初始化考虑用快照接口导入而非逐格 set。另外在低端机器上首次加载时UI 插件的渲染会有几百毫秒的白屏期建议给容器加一个 loading 态等univer.getSheetBySheetId返回有效实例后再显示表格。5.3 权限约束不能只靠前端最后一句忠告前端锁定单元格只是用户体验层面的约束不是数据安全层面的屏障。真实环境里用户完全可以通过浏览器控制台直接调用 Univer 实例改写数据或者绕过前端直连后端接口。所以服务端必须保存一份哪些字段允许提交、哪些字段需要校验的规则前端锁定是防止误操作后端校验才是防线。这个原则我之前做过很多表格类项目它从来没有变过。这次用 Univer 落地可填模板表的整个过程我自己最深刻的体会是它确实把在线表格的复杂度封装得不错但复杂度并没有消失只是转移到了对命令体系、数据模型和版本差异的理解上。如果你正准备做类似的东西我的建议是先花半天时间把官方 example 目录跑一遍再动手写业务代码。等你的模板表上线后可以在我的思路基础上继续加东西——比如把允许编辑区域按岗位权限动态下发、把填写记录做成操作日志、用公式自动汇总填写结果。表格这东西一旦能嵌进业务里而不仅仅是展示能发挥的空间其实比想象中大得多。
返回列表