我一直觉得,市面上很多“在线 Excel”项目,都把自己定位成了“更强大的表格编辑器”,但实际业务里真正的刚需往往是反过来的:不是让你去做表,而是让别人按你画好的表来填数据。Univer 这个开源项目最近在 GitHub 上热度涨得很猛,本质上就是一个用 TypeScript 从零写的 Web 办公套件,支持表格、文档、幻灯片,底层渲染直接用 Canvas 搞定,核心代码全部开源。我在一个实际业务里,就是用它做了“管理员自定义表格模板 -> 用户在网页上填写部分单元格 -> 其他单元格锁定不可修改”的完整闭环。这篇文章不聊概念,直接讲方案、代码和我在实施过程中踩过的坑。
1. Univer 是什么:比 Excel 更灵活的“在线表格底座”
1.1 需求起点:填表比做表更值得投入
先还原一下业务场景。运营同事手里有一个 Excel 模板,模板里有一些固定的表头、公式、说明文字,比如“产品名称”“报价金额”“是否含税”,然后要把这个文件发给几十家供应商去填。传统流程是这样的:发文件、等反馈、回收文件、打开看格式有没有乱、手工复制粘贴汇总。遇到几个不按格式填的,整个过程直接失控。
我当时做的工作就是把这套流程搬到网页上:管理员在系统里用表格画好模板,指定某些区域是开放填写的,其余区域一律只读,然后生成一个链接发给供应商,供应商打开浏览器就能填,填完自动回传数据。这个需求天然就需要一个前端表格组件,Univer 正好是这一类里目前最值得选的开源方案。
一开始我考虑过几种替代品:直接用 HTML table 加 input 自己画,样式和交互全部手写,工作量巨大;用 Luckysheet,组件确实成熟,但代码已经很久没有实质更新,而且作者转向了闭源商业化;用 x-spreadsheet,又太轻,公式、协同、样式控制都跟不上。Univer 最大的优势不只是功能全,而是它的架构从第一天起就是按“可嵌入、可扩展、可编程”设计的,这一点对做业务集成的人来说非常关键。
1.2 Univer 的核心定位与技术底牌
Univer 不是一个挂靠在某个大厂下面的私有项目,而是一个标准的开源项目,MIT 协议,代码在 GitHub 上可以完整看到。它对外提供的核心能力包括:Sheet 表格、Doc 文档、Slide 幻灯片三类编辑器,其中表格模块最成熟,也是大多数人集成时真正用到的东西。渲染层用的是 Canvas,而不是传统的 DOM 表格,这在处理大批量单元格时优势很明显,滚动、选中、缩放的手感都接近原生桌面软件。
从技术架构上看,Univer 有几个底层的设计非常值得一说。首先是插件化,整个编辑器是按插件拼起来的,核心内核只负责文档模型和命令分发,渲染、UI、公式、导入导出、协同、权限都是独立插件。你要轻量使用,完全可以只拉表格内核,不带任何多余功能。其次是命令系统,用户在界面上的每一个操作,最终都会转化成一个 command 对象,这个设计给了开发者非常大的拦截和扩展空间,我后面做单元格只读限制,就是靠这个命令系统实现的。
还有一个底牌是协同编辑能力。Univer 内部自带了协同相关的数据结构和服务,不是简单地同步整份文件,而是基于操作做冲突处理和合并。我在这个项目里因为填报场景的特殊性没有直接启用协同,但如果未来要做多人同时编辑一张表,这个能力可以直接在上层扩展,不需要换技术方案。
1.3 为什么是 Univer:与同类开源方案的对比
把几个主流开源表格方案摆在一起对比,差异会非常明显。我当时整理了一个内部选型表,简化之后大概是这样的:
| 对比项 | Univer | Luckysheet | x-spreadsheet |
|---|---|---|---|
| 开发语言 | TypeScript | JavaScript | JavaScript |
| 渲染方式 | Canvas | Canvas | DOM + Canvas 混合 |
| 公式引擎 | 自研,支持大量 Excel 公式 | 内置公式 | 基础公式 |
| 协同编辑 | 架构内置支持 | 需额外自配服务 | 不支持 |
| 插件/扩展机制 | 完善,命令可拦截 | 有限 | 无 |
| 社区维护活跃度 | 高,持续发版 | 基本停滞 | 长期未更新 |
| 对开发者的集成友好度 | 高,API 设计现代 | 偏传统调用方式 | 简单但能力弱 |
这里面最打动我的不是某一个单项功能,而是维护活跃度。一个开源表格组件,如果你的业务要长期跑在它上面,维护状态比功能列表更重要。Luckysheet 的功能其实已经很全面了,但两年不更新,遇到 bug 只能自己改,改完还要跟上游冲突。Univer 所在的团队明显把项目当长期产品在做,版本迭代、文档更新、issue 响应都比较及时。
还有一个点是 Univer 对中文场景的适配。在线表格这个品类,中文用户对 Excel 的既有习惯很强,小数点、日期格式、中文函数名、人民币符号这些细节都必须到位。Univer 在本地化方面做得比较完整,至少我目前遇到的中文场景没有出现需要自己修补的硬伤。
2. 整体设计:让一个表格拥有“模板”和“填报”两种身份
2.1 设计者视角:管理员用 Univer 定义表格模板
我最后实现的系统里有明确的角色区分,管理员和填报用户看到的虽然是同一个 Univer 表格,但权限和交互完全不同。
管理员进入“模板管理”页面时,打开的是一个完整功能的 Univer 编辑器。他可以自由编辑表头、合并单元格、设置边框背景色、写公式、插入数据校验下拉选项、调整列宽行高,就像在本地用 Excel 一样。编辑完成后,“保存模板”按钮会把当前工作簿的数据结构整体序列化成一个 JSON,存入后端数据库。这个 JSON 包含了工作表的所有信息:单元格的值、样式、合并区域、公式、行高列宽、数据校验规则等等。
关键点在于:模板不只是一张静态图片,而是一份带“语义”的结构化数据。比如某个单元格是公式,它在 JSON 里是有公式字段的;某个区域设置了数据校验,校验规则也存在于对应位置。这样管理员在后台配置好的规则,会原样传递给前端填报页面,不需要开发在代码里再写第二套配置。
我当时建议运营人员把所有需要填写的区域统一用一个背景色标出来,同时在表头里写明“黄色区域请填写”,配合校验规则,能极大降低用户的出错率。这个习惯也成了后文权限管理的一个辅助手段。
2.2 填报者视角:只开放指定单元格给用户修改
用户的填报页面,本质上是一个经过裁剪的 Univer 实例。加载模板 JSON 后,前端要做三件事:第一,把管理员标记为“只读”的区域锁定,让用户无法修改;第二,把工具栏、公式栏、右键菜单等编辑功能尽量隐藏或禁用,消除多余入口;第三,把允许填写的单元格正常开放,并且应用相应的数据校验规则。
这里最核心的技术难点就是“按区域控制编辑权限”。Univer 支持单元格级的权限点设置,你可以给一个 Range 指定是否可以编辑、是否可以查看、是否可以复制等动作。我在实际项目里用它实现了:开放区域完全可编辑,信息公示区只能看不能改,隐藏列连看都看不到。
但权限点配置只解决了“编辑器内操作被禁止”的问题,用户仍然可以通过浏览器的开发者工具绕过前端逻辑,向后端提交伪造数据。所以我的方案里还有第二道防线:前端只读限制只是体验层的,所有数据的合法性校验,在提交时由后端根据模板 JSON 里的规则再执行一遍。这个思路在后面单独展开讲。
2.3 数据流:模板、实例、结果三分离
整个填报系统的数据模型,我一开始差点做错。最开始我想的是“模板里每条记录对应一行,用户在同一张表上填自己的行”,这个方案听上去简单,但对模板升级的兼容性极差。后来我改成三分离的设计:
- 模板表(template):存储管理员编辑的原始工作簿 JSON,结构是稳定的,字段包含模板编号、版本号、模板内容。
- 实例表(instance):管理员发布一次填报任务时,基于模板内容生成一份填报实例,相当于模板的一次“快照”。每一条填报记录对应一份实例 JSON,里面存储用户填写后的完整工作簿数据。
- 汇总表(result):把各实例里指定区域的单元格值抽出来,按固定字段汇总成结构化记录,方便后续统计、导出、对接业务系统。
这个设计的价值在于版本隔离。运营后期一定会改模板,比如加一列“是否含税”,但历史填过的那些数据不能被新的模板结构带跑。因为每条填报数据都有独立的实例 JSON,无论模板怎么变,老数据的原始表现形态都可以还原。这相当于是给数据加了“空间快照”,虽然存储成本高一些,但在填报类业务里非常值得。
数据流也不复杂:模板创建 -> 发布生成实例 -> 用户打开实例填写保存 -> 提交后从实例抽取结构化数据入汇总表 -> 运营从汇总表导出 Excel。整个过程里,Univer 只管最上游的“表格形态”和“填写交互”,业务数据最终是以结构化字段存在的,不会绑架在 UI 组件上。
3. 实操:把 Univer 跑起来,并与业务代码对接
3.1 环境与安装:一个 Vite 项目就够了
我实际使用的技术栈是 Vite + Vue 3,后面也试过用在 React 项目里,Univer 对框架没有强绑定,它就是通过一个 Univer 实例挂载到 DOM 节点上,框架层面的接入成本很低。新建一个 Vite 项目后用 npm 安装 Univer 的预设包,基础环境就够了。
我当时安装的是@univerjs/presets这个包,它会把几大核心模块打包在一个入口里使用,对快速搭建和二次开发都很友好。Univer 的版本发布节奏比较快,API 有过一些调整,不同版本之间的初始化方式可能会有细微差异,你阅读的时候如果发现跟官方文档不一致,以你拉取到的那个版本对应的文档为准。
安装命令很简单:
npm install @univerjs/presets如果对体积敏感,也可以不用 presets,改用更底层的@univerjs/core、@univerjs/sheets、@univerjs/ui等独立包按需组合,但前期建议先跑通 presets,再考虑瘦身。
3.2 初始化一个最简表格:把编辑器挂到页面上
Univer 提供了快速创建工作簿的 API。我在测试环境里写的最小初始化代码如下,注释写得很清楚,新增一个容器节点,然后创建 Univer 实例:
import { Univer, UniverInstanceType } from '@univerjs/presets'; // 创建一个 Univer 实例并挂载到页面节点 const univer = new Univer({ // 这里可以是预设的一些基础配置,比如语言、主题 }); // 在指定 DOM 节点上初始化一个工作表 univer.createUnit(UniverInstanceType.UNIVER_SHEET, { // 这里传工作簿数据 // 不传任何内容,就生成一个带默认 Sheet 的空白工作簿 });如果你只是想要一个自带完整 UI 的在线表格,初始化到这里其实已经能用了:工具栏、公式栏、工作表标签、右键菜单、单元格编辑,全都自带了,用户可以在网页里直接做表。
不过注意一点,这个createUnit的方式创建出来的是“编辑器形态”的表格,适合管理员端使用。如果要做用户填报端,我们需要把编辑器 UI 藏掉一部分,只留下网格区域和必要的操作按钮。
我在项目里做过一次极端裁剪:隐藏工具栏、公式栏、工作表标签切换,只保留表格主体,同时把右键菜单里的编辑类操作也禁用掉。这样用户看到的页面就是一个纯粹的“在线表单”,不会误触任何玩坏模板的入口。
3.3 从模板 JSON 恢复表格:让每次打开都从同一份配置开始
填报业务的核心不是创建空白表格,而是加载管理员预先定义好的模板。Univer 的工作簿数据是一份可序列化的 JSON,所以模板加载逻辑非常简单:从后端拿到模板 JSON,把它作为 createUnit 的数据源传进去,前端表格就完整还原成管理员保存时的样子。
一份模板 JSON 里通常会包含这些关键结构:工作簿里的每个 Sheet 配置、每个 Sheet 里的单元格数据、合并单元格信息、列宽行高、数据校验规则、条件格式等。这里有一个细节:公式和值在 JSON 里通常是分开存储的,Univer 在渲染时会自动根据公式计算结果并显示,重新加载后也能正常重算。
我建议把所有模板相关的数据都放在后端管理,前端不直接拼装工作簿结构,而是后端把模板 JSON 原样返回给前端渲染。这样模板的维护入口是唯一的——只有管理员在后台用编辑器改出来的版本才有效,前端代码里不存在第二份模板定义,避免了两边不一致的问题。
3.4 给模板加数据校验:下拉列表和必填校验是填表刚需
纯开放一个单元格让用户随便填,生产环境里基本会收到一堆格式五花八门的脏数据。Univer 在模板 JSON 里支持数据校验规则配置,管理员可以给指定 Range 设置类型,比如列表、数值范围、日期、自定义公式等。在填表场景里,我最常用的两类是下拉列表和必填校验。
下拉列表相当于给用户一个固定选项集合,比如“是否含税”这一列,只允许填“是”或“否”,用户点进单元格时会出现下拉箭头,选一个就行。这比让用户自由输入干净得多。在 Univer 中,数据校验配置本质上是一段结构,大致长这样:
{ "ranges": [ { "startRow": 2, "startColumn": 3, "endRow": 200, "endColumn": 3 } ], "type": "list", "formula1": "\"是,否\"", "allowBlank": false, "showErrorMessage": true, "errorMessage": "请选择是或否" }必填校验则可以通过allowBlank: false来实现,如果用户留空,单元格校验不通过,Univer 界面上会给出提示。配合模板底部的提交按钮,我们还额外做了一层检查:提交时扫描所有开放区域,如果存在未填写的必填项,直接阻止提交并定位到第一个空单元格。这一步不是在 Univer 里实现的,而是我们自己在提交按钮的 click 事件里遍历工作表数据做的,逻辑简单但很实用。
4. 实现“用户只能填指定单元格”的完整方案
4.1 方案 A:用 Univer 权限点控制 Range 的编辑动作
Univer 的权限系统是按“权限点”设计的。一个权限点通常绑定一个动作、一个区域、一个配置结果。比如“在 1 到 3 列范围内禁止编辑”,就是一个典型的权限配置。我使用的思路是:默认整张表不可编辑,然后显式地给开放填写区域加上“允许编辑”的权限点。
这样就规避了一个常见问题:只锁禁填区域的话,用户新建行列时可能会把不可编辑区域“搬”走,导致锁定失效。而从“全部锁定 + 局部开放”的思路上发,新建行列这种操作本身就不允许,安全边界要清晰得多。
代码层面的示意大概是这样的:
// 给某个 Range 设置权限 // 这里按我的理解给出核心思路,具体 API 名称以版本文档为准 const permissionService = univer.getPermissionService(); permissionService.addPermission({ range: { startRow: 2, startColumn: 1, endRow: 100, endColumn: 2 }, action: 'edit', result: 'allow' });权限点配置是声明式的,推荐在加载模板后立即执行,这样用户打开页面看到的就已经是锁定完毕的状态,不存在一个“先可编辑后锁定”的中间闪现。同时,权限点也能控制复制、导出等动作,如果某些列连“复制出去”都不允许,也可以在这里配置。
实际生产环境中我建议对所有锁定区域的历史操作做一遍监控。虽然 Univer 权限点能拦截 UI 操作,但不同版本对边界情况的覆盖程度不一样,稳妥起见,我在表单提交时还会再对最终数据做一次“只读区值未被篡改”的比对。
4.2 方案 B:用命令拦截兜底,覆盖权限点的盲区
权限点解决的是“声明式”的管控,但 Univer 是一个命令驱动的架构,用户在界面上操作的每一个修改动作,本质上都是触发了一条 command。Univer 提供了命令拦截机制,可以在某个命令执行前插入一个拦截函数,如果函数返回 false,命令就不会被执行。
我在方案 A 之外又加了一层命令拦截,主要目标是防住那些权限点没覆盖到的修改路径。比如某些版本里,通过单元格填充、拖拽、粘贴、查找替换等方式修改数据,走的是不同的命令通道,如果只配置 range 权限,可能漏掉其中一两个入口。
核心逻辑是拿到命令里的目标 Range,检查它是否完全落在开放区域内,如果包含了禁止区域,就返回 false 阻止整条命令:
const commandService = univer.getCommandService(); commandService.before({ id: '*', // 拦截所有命令,再按命令参数过滤 preExecute: (command) => { const range = extractRangeFromCommand(command); if (range && isProtectedRange(range)) { return false; // 阻止执行 } }, });这里的extractRangeFromCommand是一个我们自写的解析函数,因为不同的命令参数结构不一样,需要从setRangeValues、insertRow、paste等命令里分别提取出受影响的区域。写完这层拦截之后,我还在控制台里专门模拟了全选粘贴、拖拽填充、跨表复制等操作,确认全部被挡住了才放心。
不过要强调一点:这层命令拦截只是增强了编辑器内的体验保护,它不是安全工具。浏览器环境下用户有无限手段构造请求,真正防篡改必须靠后端校验。这一点后面细说。
4.3 方案 C:后端二次校验,数据可信度的最后防线
前端不管做了多少层锁定,本质上都只是“用户体验层”的控制。用户可以通过开发者工具直接调用 fetch 接口,绕过 Univer,伪造一份表格数据提交到服务器。所以我在设计提交接口时,坚持了一个原则:后端收到的不是“整个工作簿 JSON”,而是“一组单元格写入值”。
具体做法是:前端遍历开放区域,把用户填写过的单元格提取成键值对,比如{ row: 2, column: 3, value: '含税' },连同模板版本号一起 POST 到后端。后端拿到这份写入列表后,会读取数据库里的模板 JSON,根据模板中定义的开放区域和数据校验规则,逐条校验这些写入值是否越界、是否必填、是否满足类型。只要有一条不合规,整单拒绝提交。
这么一搞,前端无论怎么篡改,都绕不过后端对模板定义的重新解释。我把模板 JSON 视为服务端唯一可信的数据源,前端 Univer 只是这份数据源的一个“展示器”,这个思路是整个填报系统能安全上线的基础。
方案 A、B、C 的关系建议理解成“三明治”:权限点是第一层,体验上直接锁死;命令拦截是第二层,堵住漏网命令;后端校验是第三层,也是最终底线。只做前两层,系统演示没问题,真实上线不放心;只做最后一层,用户操作体验会很差,到处都在报错。
5. 多用户填报与数据收集的工程细节
5.1 一个实例一场填报:如何隔离不同用户的填写结果
用 Univer 做填表功能,最容易犯的错误是让多个用户共用同一个前端实例。比如管理员发布了一个填报任务,全公司 100 个人打开同一个页面,如果大家都在同一个 Univer 实例里填写,你会面临两种结局:要么开启协同编辑,大家一起改一张表,互相能看到对方的数据,这通常不是填表业务想要的;要么不开启协同,但数据存储变得混乱,根本没有干净的边界去区分谁填了什么。
我采用的方案是“按人隔离实例”,每个用户打开链接时,后端动态生成一份独立的填报实例。它的基础数据克隆自模板,但实例之间互不影响。更准确地说,每个用户在服务端对应一条待填写记录,保存时直接把这份实例快照写回自己的记录里。用户重新打开页面时,看到的是自己上次保存的内容,不会串到别人的数据。
这个方案看起来每个用户都要存一份完整的工作簿 JSON,存储成本比较高,但在几十到几千人的填表场景里完全扛得住。一个几千单元格的工作簿 JSON 一般只有几十 KB,单表几万条记录也不成问题。关键是换取到了非常干净的隔离语义和几乎为零的并发冲突。
5.2 导出与收集:把 Univer 数据变成运营能用的表格
用户填完数据后,业务闭环的最后一环是把数据收集起来,给运营做导出和统计。我在系统里做了两条导出路径。
第一条路径是前端直出。因为实例本身就是一个 Univer 工作簿 JSON,我们可以加载这个实例,然后调用 Univer 的导出能力,把它还原成一个 xlsx 文件。用户端可以随时下载自己填写的原始表格文件,格式跟模板完全一致,样式、公式都还在。这条路径适合给填报者自己留档。
第二条路径是后端汇总导出。因为前端提交时已经把填写区域的数据结构化抽出了,后端汇总表里有干净的字段记录。运营在管理后台选择某次填报任务,系统直接用 SheetJS 之类的库按汇总表生成一个标准 Excel 文件,每行是一条填报记录,每列是一个字段。这条路径更适合做数据分析,比如统计报价均值、对比不同供应商的报价,直接拿字段做透视表就行。
我强烈建议把这两条路径分开,不要试图从那一堆实例 JSON 里做汇总分析,成本高且难以维护。实例 JSON 是“原始凭证”,结构化汇总表才是“账本”。
5.3 性能与大表格的取舍:Univer 不是用来炫技的
Univer 的 Canvas 渲染确实在打开大表格时表现不错,滚动流畅,选中、定位、编辑的响应速度都很好。但“渲染流畅”不等于“业务整体轻快”,在填表场景里真正影响性能的是模板复杂度、首次加载体积、以及前端单页多实例管理。
在模板设计阶段就要有意识控制复杂度。一个填报表单,通常几十行几十列就足够了,没必要把上万行的大宽表直接搬上线。我的经验是模板里公式数量守恒,公式越多,加载时重算时间越长。如果模板里有大量跨 Sheet 引用和 VLOOKUP,首屏体验会明显变差。解决思路是把需要做复杂计算的列放后台,不在用户端展示。
前端工程侧也要做懒加载。Univer 的加载体积本来就不小,如果你还需要导入导出、协同、幻灯片等模块,首包会更大。我的实践是:管理员端和填报端分别做路由懒加载,两端的 Univer 插件配置不一样,管理员端加载全套编辑器能力,填报端只保留表格渲染和单元格编辑所必需的最小插件集合,这样填报页的加载速度会比后台明显快。
6. 常见问题与避坑清单
6.1 版本升级是最大的不稳定因素
我在项目开发中期遇到过几次 Univer 版本升级,API 变化带来的工作量比想象中大。比如工作簿数据结构字段改名、初始化方式从“实例加插件”变成“实例加预设”、部分命令 ID调整等。整体趋势是越来越好,但如果你长期不升级,版本跨度一大,迁移成本就会成倍增加。
我的建议是把版本锁死在一个稳定版上,不要随手升最新。记录自己当前使用的版本号,升级前先查 ChangeLog,重点看工作簿结构、初始化方式、权限 API 这三块有没有破坏性变更。在这个项目里,我选择在一个长期稳定分支上深耕,只有当明确需要某个新功能时才做迁移,每次迁移都单独排期测试。
6.2 UI 裁剪不要留死角
填报端隐藏工具栏和公式栏之后,还有几个死角容易被忽略:右键菜单里的“插入行”“删除列”、单元格编辑状态下按 Tab 键跳转、双击自动填充、快捷键 Ctrl+C/V 粘贴进锁定区域。这些操作路径不经过工具栏,但确实存在。
我的做法是全局排查所有快捷键和右键菜单项,在填报模式下一律禁用编辑类动作。排查完还不放心,就在真机浏览器里把常见的“试图修改被锁单元格”的操作全部手动过一遍,同时配合命令拦截做兜底。这个工作挺磨人,但很必要。
另外有个体验细节:隐藏工具栏后,用户如果误点了一个只读单元格,最好能有一个轻提示,比如弹一个 toast 提示“该区域不允许修改”,而不是毫无反应。这个提示我自己接在命令拦截的preExecute里,命中禁止区域时同步触发,体验比静默拦截好很多。
6.3 数据校验规则只信后端
Univer 的数据校验在前端运行得很好,但它的校验逻辑是浏览器内存里的东西,用户完全可以绕过。我在上线前专门做了一个测试:用后端 Fake 一个请求,把违反校验规则的数据强行提交,后端如果不做二次校验,数据就污染了。
所以再次强调:后端必须根据模板 JSON 重新执行校验,包括必填校验、下拉值校验、单元格值类型校验。前端 Univer 的校验规则可以复制一份到后端逻辑,或者后端直接解释模板 JSON 里的校验配置统一执行。推荐后者,因为规则只有一份,不存在同步不一致问题。
6.4 模板不要直接当数据载体
最后还有一个组织层面的坑:管理员习惯在模板里写示例数据,比如“张三、10000”这种,发布时忘了清理,用户打开看到示例,会误以为要覆盖掉或者参照着写。这些示例数据如果混进正式填报,很难排查。
我的模板管理里增加了一个“发布前校验”步骤:发布填报任务之前,系统自动扫描模板中开放区域是否已有数值或文本内容,如果有,提醒管理员确认清理。同时,模板本身建议只保留表头、说明、公式、样式和校验规则,所有示例数据一律清空,避免模板实例混杂。
写在最后
这个项目做下来,我最深的体会是:用 Univer 这类开源表格组件,真正的难点从来不在“把表格跑起来”,而在于怎么设计好业务边界。表格组件帮你解决的是交互层的问题——让用户能像填 Excel 一样舒服地录入数据;但一个填表系统能不能安全稳定地跑下去,取决于你怎么定义模板、怎么组织实例、怎么在后端做校验。
我后来再遇到类似需求时,已经形成一套固定的检查清单:模板是否由管理员统一维护、每个用户是不是独立的实例空间、前端锁定是否有多层兜底、后端是否对提交数据执行了独立的规则校验。把这四件事想清楚,Univer 只不过是一个表现力很强的工具,真正稳住系统的还是这些工程习惯。