1. Univer 到底是个什么项目
第一次听到 univer 这个名字,是在一个前端的闲聊群里,有人贴了个 GitHub 链接,说“又一个开源的在线表格轮子”。我点进去看了一眼,第一反应是:这项目野心不小。
简单来说,univer 是一套基于 TypeScript 的在线办公套件,核心能力是电子表格(Spreadsheet),但架构上它是奔着“文档 + 表格 + 幻灯片”全套去的。它在界面上用 Canvas 自绘网格,交互手感接近原生桌面端表格软件,支持公式、条件格式、图表,还内置了协同编辑能力。和传统基于 DOM 层的表格组件不同,它把渲染层和业务逻辑层做了严格分离,这套设计让它在大数据量场景下依然能保持流畅。
那“支持用户定义表格,然后让用户去填写一些单元格,其他单元格无法修改”这个需求怎么理解?这其实是企业在用表格类产品时最常见的诉求之一。比如财务部门做预算填报,表格结构是总部固定好的,各地分公司只能往指定的空白单元格里填数字,成本科目、地区维度、公式列都不允许动。又比如项目管理里的进度跟踪表,模板由 PMO 统一发布,项目成员只能更新自己负责的任务行。这类场景的底层能力就是“单元格保护 + 区域锁定 + 按用户粒度授权”。
Univer 本身提供了完整的数据结构和渲染层,但锁定的具体交互和权限控制逻辑,需要自己结合业务去实现。这篇文章我就拿 univer 做一次完整的实战拆解,讲讲我实际用它做在线填报系统时的设计和踩坑记录。
先说结论:如果你只是想找一个能嵌入网页的表格组件,类似 SheetJS 那种,univer 可能偏重了,它更适合做成一个独立产品;但如果你要做的就是一个“模板表格 + 受限填写”的在线工具,univer 的插件体系和数据模型会帮你省掉大量造轮子的时间。
2. Univer 的核心架构与运行机制
2.1 渲染层:Canvas 自绘不是炫技
Univer 官方把渲染层叫 UniEngine,它在底层封装了 Canvas 2D 的绘制能力,表格的每一格不是 DOM 元素,而是由渲染引擎批量绘制的矩形区域。这个选择不是为了显得高大上,而是为了解决一个非常现实的问题:浏览器里能承载的 DOM 节点数量是有上限的。
普通网页表格如果用 div 或者 table 渲染,1 万行 × 20 列就意味着 20 万个单元格节点,页面的滚动、选择、输入都会变得迟钝。Univer 用 Canvas 渲染,单元格数量只影响绘制指令的复杂度,不会让 DOM 节点爆炸。我实测过在 univer 里打开一个十万行级别的表格,滚动和选择仍然保持流畅,这点在数据量大的企业管理场景里非常关键。
Canvas 渲染带来的另一个优势是样式统一。表格里最常见的“边框线”“选区高亮”“冻结窗格”,在 Canvas 里就是几行矩形绘制代码,性能和效果都容易控制。而 DOM 方案里,每一条边框线都是一个 CSS 样式,很容易出现累积误差和渲染卡顿。
2.2 数据层:统一的命令系统
Univer 的业务逻辑层也是我比较欣赏的设计,它把表格里所有的操作都抽象成了“命令(Command)”。用户输入一个值、删除一行、设置列宽、合并单元格,底层都会生成一个对应的数据结构变更,这些变更可以同步到协同服务端,也可以被插件拦截。
这个命令系统的好处是:权限控制可以做得非常干净。单元格锁定不需要偷偷监听用户的键盘事件,再判断“这个格子能不能输入”,因为所有修改操作都经过命令层,拦截命令本身就是最可靠的权限控制点。你自己的业务逻辑可以在命令执行前介入,决定是放行还是阻断。
那么问题来了,怎么判断一条命令影响到了哪些单元格?这就需要理解 univer 的选区(Selection)模型。每次用户在界面上操作,比如输入一个值,Univer 会产生一个针对某个 Sheet 片段的命令,命令里带有完整的 Range 信息。只要拿到这个 Range,再和服务端返回的锁定规则做交集判断,就能知道这次修改是否越权。
2.3 协同模型:CRDT 还是 OT
Univer 的协同框架底层用的是 Yjs 的 CRDT 方案,超论文里也提过他们自己改进了冲突处理逻辑。对普通使用者来说,CRDT 和 OT 的区别不用太深究,只需要知道:CRDT 天然支持离线编辑和合并,协同过程中不容易出现因为操作顺序不同而产生的“死锁”状态。
在开发基于 univer 的填报系统时,协同模型意味着“多人同时在线填表”是开箱即用的能力。总部财务和分公司会计可以同时打开同一张表,所有人都能看到实时更新。但我个人在做生产环境方案时并不会直接暴露公共协同网络,Univer 支持自建协同服务器,这样数据才完全可控。
3. 项目选型与整体方案设计
3.1 为什么选 Univer 而不是 SheetJS 或 Luckysheet
我参与这个项目时,技术选型其实经过了比较激烈的讨论。备选方案有三个:SheetJS、Luckysheet、Univer。三者的定位完全不同,这里可以做个对比:
| 维度 | SheetJS | Luckysheet | Univer |
|---|---|---|---|
| 交互能力 | 弱,偏纯数据处理 | 中等,有在线编辑体验 | 强,桌面端级体验 |
| 协同编辑 | 无 | 部分支持 | 原生支持 |
| 自定义插件 | 无 | 一般 | 强大的命令插件机制 |
| 大数据量渲染 | 依赖 DOM | 依赖 Canvas | 依赖 Canvas + 引擎优化 |
| 学习成本 | 低 | 中 | 高 |
SheetJS 本质上是 Excel 解析器和数据生成器,它能帮你读 xlsx、写 xlsx,但是要做单元格级交互,几乎要自己从零搭一套编辑器。Luckysheet 在国产开源社区里热度很高,交互和格式支持都比较成熟,但它后续的维护方向基本都转向了 Univer,做新项目更建议直接上 Univer。
还有一点很实际:Univer 的架构里,大量逻辑是用 TypeScript 写的,类型定义很全。我们在多人协作开发时,改代码的信心强很多,不会出现“某个配置参数找不到类型,只能看源码”的尴尬情况。
3.2 填报系统的功能拆解
明确了用 Univer 之后,我们把需求拆成了四块:
- 模板定义:管理员创建表格模板,设置哪些单元格允许填写、哪些锁定、哪些隐藏。
- 数据采集:用户在线打开模板,在允许填写的区域录入数据,实时保存。
- 权限控制:不同用户看到的内容范围不同,比如省级用户只能填自己省份的数据行。
- 数据汇总:提交后数据进入后台,做汇总和导出。
其中模板定义和权限控制是核心难点。Univer 本身并没有直接提供“锁定单元格”的 UI,但它提供了命令拦截能力,我们可以自己实现一套锁定规则。
第 2 节里我说过,Univer 支持“用户定义表格”和“锁定单元格”。具体实施时,我把它拆成了两个层次:界面层的标记和逻辑层的强制。界面层用条件格式给锁定单元格加底色,让用户一眼看出哪些区域能填;逻辑层拦截所有修改命令,把越权操作挡在门外。
3.3 锁定规则的存储设计
锁定规则本身也是一张表,但它不需要存进 Univer 的单元格数据里,而是独立存到自己的数据库。我设计的锁定规则模型大概长这样:
{ "sheetId": "sheet-001", "rules": [ { "name": "基础信息区", "ranges": ["A1:B10", "D1:D5"], "locked": true, "editableRoles": ["admin", "hr_manager"] }, { "name": "填报区", "ranges": ["C11:C100"], "locked": false, "editableRoles": ["employee"] } ] }将锁定规则与电子表格数据分离存储,好处是模板和规则可以独立迭代,管理员修改模板不需要动数据,调整锁定范围也不用复制整个表格。而且这样权限校验逻辑更简单,规则是集中的,而不是散落在每个单元格的数据里。
4. 实操:Univer 的集成与基础配置
4.1 环境准备与项目初始化
Univer 目前推荐直接在 React 或 Vue 项目里用 npm 包引入,我这边项目栈是 Vue 3 + Vite + TypeScript,初始化步骤很简单:
npm create vite@latest univer-demo -- --template vue-ts cd univer-demo npm install @univerjs/core @univerjs/sheets @univerjs/sheets-ui @univerjs/uiUniver 的包拆分做得比较细,core 是核心数据模型,sheets 是电子表格业务逻辑,sheets-ui 是表格的 UI 层,ui 是通用 UI 框架。为了跑起来,还需要安装一套默认主题和图标:
npm install @univerjs/design @univerjs/icons @univerjs/engine-render装完之后,在 Vue 组件里注册一个基础的 Univer 实例:
import { Univer, UniverInstanceType } from '@univerjs/core'; import { defaultTheme } from '@univerjs/design'; import { UniverSheetsPlugin } from '@univerjs/sheets'; import { UniverSheetsUIPlugin } from '@univerjs/sheets-ui'; import { UniverUIPlugin } from '@univerjs/ui'; const univer = new Univer({ theme: defaultTheme, locale: 'zhCN', }); univer.addPlugin(UniverSheetsPlugin); univer.addPlugin(UniverSheetsUIPlugin); univer.addPlugin(UniverUIPlugin);这里有个容易踩的坑:Univer 的插件注册顺序有讲究,一般先注册核心插件,再注册 UI 插件,尤其不能把UniverSheetsUIPlugin注册在UniverUIPlugin之前,否则界面组件可能找不到对应的渲染引擎类型,白屏后在控制台才能看到报错。
4.2 创建并加载表格模板
Univer 启动之后,默认会带一个空白工作簿。生产环境里不能让它空着,得加载我们设计好的模板。模板可以是后端存储的 JSON 数据,也可以是一个 xlsx 文件。我这边是后端管理平台导出的 JSON,用univer.createUniverInstance来创建实例:
const instance = univer.createUniverInstance(UniverInstanceType.UNIVER_SHEET, templateJson); const activeSheet = instance.getActiveSheet();这里templateJson的结构可以直接参考 Univer 官方文档里工作簿的数据格式,核心是sheets数组和cellData数据块。单元格数据里包含了每个单元格的值、格式和样式索引,结构比较直观。
模板加载完成后,马上要做的一件事是隐藏网格线、关闭行选择等,让页面看起来像一个填报表单而非自由的表格工具。这些可以通过setWorksheetOption之类的命令完成:
univer.getCommandService().executeCommand({ id: 'sheet.operation.set-worksheet-option', params: { sheetId: activeSheet.getSheetId(), option: { showGridLines: false, rowHeaderVisible: false, columnHeaderVisible: false, }, }, });4.3 用初始化钩子实现“首次填入默认值”
模板里的有些单元格会希望预填一些基础信息,比如填报人姓名、填报日期、所属部门。这些值不希望在用户打开表格瞬间由前端硬编码写入,因为写到表格数据里就会变成脏数据。
我的做法是:在加载模板后、用户看到表格前,拦截一次初始化事件,把当前用户的基本信息写入指定单元格。Univer 提供了一个很好用的入口,叫beforeCommandExecute,可以在命令真正执行前做数据预处理。
操作并不复杂,就是执行一次赋值命令:
univer.getCommandService().beforeCommandExecute((command) => { if (command.id === 'sheet.operation.set-range-values') { // 这里可以注入默认值 } });不过要注意,beforeCommandExecute拦截的是所有命令,所以注入默认值时必须加判断条件,比如只在特定 sheet、特定 range 时注入,否则用户的每一个操作都会被偷偷塞进默认值,造成数据错乱。
5. 核心功能实现:单元格锁定与受限填写
这一节是整个项目最关键的部分,目标是让用户只能修改指定单元格,其他单元格均锁定。
5.1 确定锁定区域和权限策略
我们把权限分成了三层:
- 第一层是管理员,拥有全部单元格的编辑权,负责模板维护。
- 第二层是填报人,只能修改填报区域,即
editable: true的单元格。 - 第三层是只读用户,比如审计人员,所有单元格都不能改。
这些角色的权限信息存在后端,前端初始化时通过接口一次性拉取,得到一个权限规则数组。前端拿到规则后,会把规则写到 Univer 实例的一个全局状态里,供命令拦截时读取。
5.2 前端拦截:核心防线
Univer 的修改类命令有很多种,常见的有:
sheet.operation.set-range-values:批量设置单元格值sheet.operation.insert-rows:插入行sheet.operation.delete-rows:删除行sheet.operation.merge-cells:合并单元格sheet.operation.set-range-format:设置格式内容
其中用户手动输入值,底层是set-range-values;删除内容实质也是设置值。插入行、删除行、合并单元格这些操作则直接影响表格结构,更容易越权。
我的拦截思路是:定义一个protectedRanges数组,保存所有锁定区域的 Range 信息,然后用一个统一的hasWritePermission()函数来判断“这次命令涉及的 Range 是否越权”:
function hasWritePermission(commandId: string, params: any): boolean { // 1. 如果用户是管理员,直接放行 if (currentUser.role === 'admin') return true; // 2. 解析命令涉及的区域 const ranges = extractRangesFromCommand(commandId, params); if (!ranges) return true; // 3. 判断这些区域是否属于允许编辑的集合 return ranges.every(range => isRangeInEditableSet(range)); }extractRangesFromCommand的实现需要根据命令类型做分支处理,但整体逻辑简单直接。关键环节是注册拦截器:
univer.getCommandService().beforeCommandExecute((command) => { if (!hasWritePermission(command.id, command.params)) { // 阻止命令执行,并给出提示 return { success: false, message: '该区域已被锁定,无法编辑', }; } return { success: true }; });这个方案覆盖了所有用户路径。不管用户是键盘输入、右键菜单、工具栏操作,还是通过脚本调用命令,最终都会经过beforeCommandExecute拦截。这也是 Univer 命令模式对比“监听 DOM 事件来做权限控制”最大的优势:没有漏网之鱼。
5.3 视觉反馈:锁定区域和高亮显示
拦截只是保证数据安全,但用户体验也必须要处理好。如果用户点了半天,才发现区域是锁定的,操作反馈会很差。
我的做法是在模板渲染完成后,用 Univer 的样式 API 给锁定区域加一个浅灰色的底色,并给可编辑区域加一个淡黄色的底色:
const editRange = { sheetId, startRow: 10, startColumn: 0, endRow: 200, endColumn: 5 }; univer.getCommandService().executeCommand({ id: 'sheet.operation.set-range-format', params: { sheetId, ranges: [editRange], style: { fill: '#FFF9E8', }, }, });同时,为了使交互更友好,我监听了选区变化事件,在用户选中锁定区域时,在状态栏里提示“只读区域,请在黄色区域填写”。
univer.getCommandService().onCommandExecuted((command) => { if (command.id === 'sheet.operation.set-selection') { // 解析当前选区,判断是否落在锁定区 // 更新状态栏提示 } });这一步很重要,好的用户界面必须让用户知道哪里能写、哪里不能写,而不是等他输入失败之后才告诉他。
5.4 服务端二次校验
前端拦截只是第一道防线,真正生产环境里,数据提交到服务端时还必须做二次校验。原因很直白:绕过前端拦截很容易,恶意用户可以直接调用后端 API 传数据,不会经过 Univer 的代码。
服务端校验的思路和前端一致,后端存的是模板 ID 和锁定规则。收到一份提交数据时,遍历每个单元格的坐标,检查它是否属于允许填报的区域:
// Node.js 伪代码 function validateCellChange(sheetId, cellId, role, rules) { const rule = rules.find(r => r.sheetId === sheetId); const list = rule.editableRanges; return list.some(range => isPointInRange(cellId, range) && rule.roles.includes(role)); }如果校验失败,整份提交丢弃,并返回错误码。我在生产环境里线下测试中,前端校验和服务端校验双保险的效果非常好,基本杜绝了越权写入。
5.5 处理 Excel 文件导入导出时的锁定状态
单元格锁定与否,本身是 Excel 文件里的“保护 (Protection)”概念。Univer 的内部模型里对这个概念的支持深度是够的,但我在做导入导出时发现,社区版默认模板生成的 xlsx 文件在 Excel 里打开后,锁定状态不一定完全符合预期。
解决办法很直接:不在模板文件层面依赖“单元格保护”,而是在应用层用命令拦截实现锁定,导出的 Excel 文件默认带一份保护规则。具体做法是:
- 模板 xlsx 里先不设置保护,方便后台解析和修改。
- 导出时,后端程序读取数据库里的锁定规则,动态生成
worksheet.xml里的sheetProtection节点,再写入 xlsx 的压缩包。
这样即使不通过 Univer 客户端,直接拿 Excel 打开这份导出的文件,也能看到区域被锁定。
6. 协同编辑与实时数据保存的实战细节
6.1 多人编辑的冲突处理
Univer 的协同能力基于 CRDT,这让我省去了处理冲突合并的大量工作,但在实际接入时还是有几个细节需要注意。
首先,协同服务需要把 Univer 内置的文档操作同步到其他客户端。如果项目只有前端部分,不接协同服务,Univer 单机模式下也能正常使用,只是没有多人同步。我们项目最初就是单机模式起步,后面因为要支持“财务多人同时填报”才引入了协作服务。
Univer 官方的协作方案是把服务端做成一个 Websocket 服务,客户端通过@univerjs/network发送操作消息。里面有一个比较核心的机制叫Awareness,用来感知当前有哪些用户在编辑哪些区域,类似 Google Docs 里右上角显示“张三正在编辑”的功能。
我在接入时做了一个简单的“区域锁”机制,当一个用户正在某个填报区域输入时,服务端会广播一个锁事件,其他人看到的该区域边框会变成红色,避免同时编辑同一批单元格造成数据冲突。这个机制对我们的填报场景非常有用,因为填报数据通常一行数据会同一个人填写,很少出现多人协同编辑同一个格的需求。
6.2 自动保存与提交动作分离
在线填报和文档编辑有个本质区别:文档编辑需要持续实时保存,而填报更强调“提交时点一致性”。如果每敲一个单元格就触发保存,不但服务端压力大,而且可能在填报中途产出一份不完整、不合法的一行数据。
我采用的方案是:
- 本地每 5 秒做一次自动保存(此时提交的数据打“草稿”标记)。
- 用户点击“提交”按钮时,前端先做一遍校验,再提交整表数据。
- 服务端收到提交请求后,用事务把草稿状态改为“正式”,并将版本号递增。
这里的“事务”概念保证了不会出现一半数据已提交、一半仍是草稿的中间态。对于财务场景,这个模型非常关键。
6.3 回放与审计
协同编辑会引入一个问题:如果用户和历史版本之间的操作记录存在差异,怎么定位谁在什么时候改了哪个单元格?Univer 的操作日志天然就是审计日志,我在前端把所有命令操作记录下来,附带用户 ID 和时间戳,上传到服务端存一条operation_logs表。
审计日志的价值在于,一旦出现数据错误,不用看懵懂的数据快照,直接从操作日志里回溯。这在很多企业场景里属于合规性要求,早期不做好,后面补会很麻烦。
7. 常见问题与排查技巧实录
7.1 单元格输入时内容不更新
这是新手遇到最多的坑之一。Univer 的 Canvas 渲染意味着你按下一个键,数据层更新的同时,渲染引擎需要知道“这个区域重新绘制了”。如果拦截器里没有正确返回{ success: true },命令就不会执行;但如果返回了{ success: true }且数据没变,渲染层也可能会被跳过。
我的排查思路是:先看数据模型是否更新,再看渲染层是否被改动。用openInBrowser打开控制台,执行univer.getActiveSheet().getCellValue(1, 1)看值有没有变。如果值变了但界面没变,是渲染问题;如果值都没变,是命令执行的问题。
7.2 锁定的区域仍然可以双击输入
这个问题看着矛盾,但真实出现过。原因是 Univer 的输入在命令系统之外还有一个即时输入流程,当用户双击一个单元格时,会进入轻微的编辑态,此时输入会先写在本地编辑器里,回车后才真正触发命令。双击进入编辑态本身不算“修改数据”,但如果没在此时拦截,用户会看到一个看起来可以输入的闪烁鼠标,这会让用户产生“已解锁”的错觉。
解决方案是:在双击事件里做一次区域检查,如果点击的是锁定区域,直接不进入编辑态,并给出提示。
const onCellDoubleClick = (cellInfo: { row: number; col: number }) => { if (!isEditable(cellInfo)) { // 阻止进入编辑态 return false; } return true; };这类细节对体验的影响非常直观,不处理的话,即使最终数据没有被改掉,用户也会疯狂吐槽“锁了个寂寞”。
7.3 模板修改后旧数据不匹配
班表和管理员修改模板,表格增减了几列,但填报人提交的旧数据仍然按旧模板的位置保存,导出时会出现集体错位。为此我在保存提交数据时记录了一个templateVersion字段。后端在接收数据时,用当前模板版本和templateVersion比较,如果不一致,进入“数据迁移”流程:按坐标映射关系,把旧数据搬到新模板对应的位置上。
如果无法完成自动迁移,就让管理员手动确认,而不允许数据直接静默写入,避免后续汇总出错。
7.4 大数据量下滚动卡顿
虽然 Canvas 渲染本身很高效,但如果设置了大量条件格式、自定义边框,绘制指令数量会增加。我在测试中试着给一万个单元格都设置不同的背景色,滚动时明显能感觉到帧率下降。
Univer 的文档里提到,很多格子的样式应该抽成“样式表”里的公共样式,而不是每个格子单独存样式。我在做筛选时,尽量通过控制cellData里的s属性(样式索引)来引用公共样式,减少重复数据。实测一万个格子用公共样式后,滚动恢复流畅。不要在单元格数据里塞重复的全格式 JSON。
7.5 协同状态偶尔出现“自己看到的数据是旧的”
协同场景下,偶尔会因为我们接的 Websocket 服务出现了消息重放,导致本地状态被旧消息覆盖。排查时我打印过 Univer 内部的操作时间戳,发现确实是消息乱序导致的。
解决办法是启用服务端消息序列号(类似单调递增 ID),客户端按序列号顺序应用消息,旧的直接丢弃。Univer 的协同框架本身有版本概念,如果你的服务端实现比较简单,可能需要自己补一层队列缓冲。
8. 界面定制与用户体验优化
8.1 自定义工具栏:去掉用户不需要的功能
Univer 的 UI 插件默认会渲染一个完整的工具栏,包含插入图表、数据透视、筛选、排序等。填报表单不需要这些,保留反而增加误操作风险。
Univer 的工具栏配置支持通过传入一个toolbar配置项来裁剪。我这边只保留了撤销、重做、上下标、颜色选择这四组:
new UniverSheetsUIPlugin({ toolbar: { items: ['undo', 'redo', 'color', 'bold', 'italic'], }, });字体选择、边框线、合并单元格等全都不放。这很关键,用户不会在界面上看到根本用不到的功能,学习成本会下降很多。
8.2 状态栏:展示当前用户的权限与区域提示
底部状态栏,放了三块信息:
- 当前角色(管理员 / 填报人 / 只读)
- 当前选中区域的填写状态(可编辑 / 只读)
- 版本号(第几版模板)
这些信息对填报用户特别重要。以前没有提示时,用户总是问“这里能不能填”。有了状态栏文案后,这个问题基本消失了。
8.3 移动端适配的取舍
Univer 官方对移动端的支持还在完善中,我在测试机(iPhone + Android)上验证过,基本能用,但手势操作没有桌面端流畅。选择单元格、拖动填充这类交互在手机小屏上体验很勉强。
考虑到我们的填报场景大多在桌面端进行,移动端只做了只读展示。也就是说,手机浏览器打开表格时,能查看,但不能编辑。这样不是妥协,而是合理分工:移动端定位为“看数据”,桌面端定位为“录数据”。
9. 生产部署与性能优化
9.1 打包体积优化
Univer 的 npm 包并不小,开发环境下首次加载可能需要几秒钟。生产环境一定要做代码分割,把univer相关代码拆成一个单独的 chunk,避免主业务包被拖慢。
Vite 里的配置方式:
build: { rollupOptions: { output: { manualChunks: { univer: ['@univerjs/core', '@univerjs/sheets', '@univerjs/sheets-ui'], }, }, }, }打包后univerchunk 体积虽然不小,但因为它基本不会变,可以做长效缓存,用户第二次打开就不会重新下载。
9.2 渲染线程与 Web Worker
Univer 内置了把数据计算放到 Web Worker 的能力,如果你的表单里有大量复杂公式,可以考虑开启。但实践中发现,开启 Worker 会引入一点调试复杂度,而且填报表单里公式用得少,我们最终没有启用 Worker,计算结果仍同步渲染。这个按业务场景取舍即可,不用盲目追新。
9.3 服务端接口设计要点
和 Univer 配套的后端接口,按我的经验至少要提供这五类:
- 模板列表接口:返回模板元信息,比如 ID、版本号、锁定规则。
- 模板数据接口:返回工作簿 JSON。
- 保存草稿接口:接前端自动保存数据。
- 提交接口:校验并写入正式数据。
- 导出接口:服务端根据模板和提交数据生成最终 Excel 文件。
其中导出接口要特别注意“模板和数据的版本匹配问题”,不要从数据库里随便拿一个模板然后糊数据。要让旧报告模板也能通过映射规则转出可读的 Excel,这比你想的要复杂。
10. 个人实践后的几点感受
把 Univer 真正落地到业务系统,并不像在 demo 里拖一个表格那么简单,但它提供的底座确实足够扎实。
其一,命令系统帮了大忙。以前用老派的表格组件做权限控制,得监听各种键盘事件、鼠标事件、输入框事件,逻辑绕来绕去,还总有漏网之鱼。Univer 里直接拦截命令,思路瞬间清爽。
其二,锁定规则的优先级要早设计好。我在项目初期就定了一条原则:任何单元格的权限,都从规则表里查,查到再说,查不到默认拒绝。这样新加一个 sheet 或者新改一版模板,只要规则没配置好,就绝不会出现“偷偷可以编辑”的意外状态。
其三,社区版在协同上还有很多“生产化”工作要做。想直接接官方协同服务做商用,成本远高于一个 npm install。如果只是单机填表,完全没问题;如果要多人实时协作,预算和人力都要提前预留。
Univer 这个项目还在高速迭代,它的插件生态、命令系统、Canvas 渲染引擎都是我比较欣赏的设计。如果你正在为团队做在线填报、数据收集这类工具,先把“模板 + 锁定 + 填写 + 汇总”这条链路跑通,后面会发现它带来的体验升级非常明显。