☰
Univer在线表格实现可填写模板:单元格保护与数据填报实战
2026/11/2 9:50:26 网站建设 项目流程

1. 项目缘起:为什么自己搭一个“可填写的表格”系统

我大概是去年年末开始接触 Univer 这个开源项目,起因其实很朴素:客户要一个“内部数据填报系统”。说白了就是让各部门同事在网页上填一张表,表头、样式、必填项由管理员定,填报表的人只能往指定单元格里输入内容,其他区域看得到但动不了。一开始我想到的方案是现成的表单工具,但列出的限制很让人头疼:一是表格行数、列数、合并单元格、样式控制都太弱,二是数据拿回来还得二次清洗,三是无法做到“某些单元格可编辑、某些绝对锁定”的精细权限。

于是我开始搜在前端直接嵌入一个在线表格的方案。最初对比了 Handsonable、Luckysheet 这些,后面挖到了 Univer。Univer 是由国内团队打造的开源办公套件 SDK,用 TypeScript 编写,底层基于 Canvas 渲染,前端可以直接通过 npm 引入,做出现代化 Excel 类的界面和交互。而且它不是一个“死表格”,而是带一套完整的命令系统、公式引擎和插件机制,非常适合做“模板 + 填报 + 数据回收”这种场景。

这篇文章想把整个方案的思考过程、实现路径和踩坑记录都写出来,给准备做在线表格、数据填报、甚至未来做更大规模办公套件的同学一些参考。如果你是前端开发者,或者团队里正好需要一个轻量的在线表格组件,这篇应该对你有用。

Univer 的能力远不止渲染一张表。它拆分为 UniverSheet 表格、UniverDoc 文档、UniverSlide 演示三大核心模块,各自独立、也可以互相扩展。但即便只用到表格模块,也包含了公式、条件格式、数据验证、排序筛选、打印、协同编辑等一批高阶功能。很多办公室场景下我们以为要自己处理的东西,其实底层都已经实现了,只是需要你把它“打开”。

2. 需求拆解:把“用户定义表格 + 填报 + 只读保护”翻译成技术方案

2.1 三个关键需求怎么理解

标题里那行热词描述得非常精准:“univer 支持用户定义表格,然后让用户去填写一些单元格,其他的单元格用户无法修改”。拆开来就是三层需求:

第一层,管理员要先定义一张表格。这里包含表头、列宽、行高、背景色、边框、合并单元格、默认值,甚至还可以放几个下拉选项。对应到 Univer 里,就是初始化一张工作表,然后在指定区域设置样式和公式。这些配置在 Univer 里会被序列化为一份 JSON 快照,可以直接保存到后端,下次打开时原样还原。

第二层,填报人只能填写指定的单元格。Univer 天然支持单元格编辑,但关键在于“编辑权限”需要被收窄。这不能只做一个“前端按钮禁用”的效果,否则用户按 F2 或双击照样进编辑,而是要用 Univer 的“工作表保护(Protection)”能力,把整个工作表设为只读,再针对特定单元格区域开放编辑,或者反过来:默认只读、选中局部可写。

第三层,数据回收。填报完成后,我们需要拿到每个用户填了什么、哪些单元格有值、哪些不符合校验规则。Univer 给了一个统一的getSnapshot()方法,可以拿到整个工作表的数据快照,同时也有事件监听,可以实时获取用户每一次修改。后面我会重点讲这个。

2.2 为什么选 Univer 而不是其他方案

我在选型时把“在线表格”的候选方案分了几类:第一类是纯自研,基于 Canvas 或 DOM 自己画格子、自己维护选区、自己写公式解析,这个工程量不是一两个星期能做完的,换算成人力成本极高。第二类是商用组件,部分开源产品有稳定性优势,但问题在于授权、定制成本,以及团队硬编码改源码的难度。第三类就是 Univer。

为什么最终是 Univer?首先是它保留了 Excel 级别的交互细节:双击进入编辑、拖拽填充、Ctrl+Z 撤销、复制粘贴多单元格、自动换行、合并单元格、冻结行列快照,这些日常高频操作如果自己实现,每一件都可能花掉好几天。其次,Univer 提供了一套命令系统,所有操作都会经过CommandManager,这意味着你可以拦截、监听甚至阻止某个操作,这就为单元格保护和后端持久化提供了非常干净的入口。第三点,它支持中文环境,文档站点和社区都相对友好,遇到问题不至于只能去翻英文 issue。

当然,Univer 也有一些潜在代价:包体不算小,主包的渲染引擎和核心逻辑往往会压到几百 KB 以上;版本升级比较快,API 有过变动,所以上线前要锁版本。但这和它能换来的时间收益相比,可以接受。后面实操部分我会再提一个“锁版本”的建议。

2.3 整体架构图:从界面到存储

虽然我不能画完整流程图,但可以用一段文字描述系统脉络:用户浏览器加载前端应用,前端引入 Univer SDK,初始化一份工作表,工作表的数据模型由 Univer 内部管理。当管理员需要生成一张新模板时,前端调用 Univer 的配置能力生成快照 JSON,然后通过后端接口存入数据库;当填报人打开页面时,前端请求模板 JSON,初始化出只读表格,然后在beforeCommandExecute钩子或者表单保护逻辑中放开特定区域。用户填完数据后,前端读取单元格值,提交到后端。

这套方案的好处是,模板和数据源分离。模板本身是“结构 + 样式规则”,数据是“格子里的值”。只要后端存好 JSON,前端无论如何刷新,表格都能无差别还原,这也天然支持同一张模板给不同部门、不同批次使用。

3. 环境准备与基础初始化:5 分钟跑起第一个 Univer 表格

3.1 初始化项目与安装依赖

我这边用的是 Vite + Vue 3,其实 React、纯 HTML 都可以跑,Univer 官方提供了对应的框架示例。假设我们新建一个 Vite 项目:

npm create vite@latest univer-demo -- --template vue-ts cd univer-demo npm install

接着安装 Univer 相关包。这里要留意:@univerjs/preset-sheets是一个聚合包,帮你把 sheet 所需的渲染器、交互、UI 都带上了,省得一个个手动引入,新手建议从这个入手。

npm install @univerjs/core @univerjs/preset-sheets @univerjs/ui

如果你的环境是 React 或纯 JS,安装命令差别不大,只是入口处初始化方式略有不同。核心原则是一样的:引入 Univer、注册插件、创建实例。

3.2 创建 Univer 实例并加载工作表

以一个最小示例来说,核心代码可以这样写:

import { Univer } from '@univerjs/core'; import { UniverPresetSheets } from '@univerjs/preset-sheets'; import { LocaleType } from '@univerjs/core'; const univer = new Univer({ locale: LocaleType.ZH_CN }); univer.registerPlugin(UniverPresetSheets);

初始化之后,你还需要创建工作簿和表格数据。Univer 的UniverSheet是工作表模块,如果你想快速创建一个带内容的表格,可以传入一个数据快照对象:

const workbook = univer.createUniverSheet({ id: 'workbook-1', name: '员工信息填报', sheets: [ { id: 'sheet-1', name: '基础信息', cellData: { 0: { 0: { v: '姓名' }, 1: { v: '部门' } }, 1: { 0: { v: '张伟' }, 1: { v: '市场部' } } } } ] });

这里cellData的 key 是行号和列号,从 0 开始。{ v: '姓名' }表示单元格的显示值。这样渲染出来的表格就已经有两条数据了。后面如果要动态渲染模板,只需要把从后端拿到的 JSON 作为createUniverSheet的参数传进去即可。

3.3 从模板快照还原表格

实际项目里,管理员建的模板不会是写在前端代码里的,而是存在后端数据库。Univer 的工作簿快照本质上是一个包含id、sheets、styles、dataValidations等字段的大 JSON 结构。你完全可以把createUniverSheet的参数整体序列化后存起来,下一次直接反序列化再传入。这也是 Univer 在设计上最让我省心的地方——不需要自己把 Excel 文件解析成某种私有格式。

说一下快速验证的方式:在页面初始化后,打开控制台,调用 Univer API 获取当前工作簿并打印快照:

const workbook = univer.getUniverSheetInstance('workbook-1'); const snapshot = workbook.getSnapshot(); console.log(JSON.stringify(snapshot));

这个快照就可以直接视为“模板数据”。你可以在后端写一个接口接收这份 JSON,并在用户访问时回传。

4. 核心功能实现:单元格保护、数据校验与动态模板

4.1 工作表保护:让“其他单元格”不可修改

标题中最核心的诉求是“用户填写一些单元格,其他单元格无法修改”。在 Univer 里,这对应的是“工作表保护(Worksheet Protection)”。

Univer 的protected配置在 Sheet 快照中的protect字段里。最简单的做法,是在创建表格的初始快照中直接声明:

const workbook = univer.createUniverSheet({ id: 'workbook-1', name: '员工信息填报', sheets: [ { id: 'sheet-1', name: '基础信息', protect: { sheet: true, objects: { insertRow: false, deleteRow: false, editContent: true } }, cellData: { ... } } ] });

但如果你是想动态控制——比如管理员先编辑模板,然后点击“发布”,表格从可编辑切换到只读,那纯静态配置就不好使了。这时候需要用命令接口。

Univer 的CommandManager提供了一套完整命令体系,你可以通过executeCommand动态修改保护的开关。下面是我在项目里实际用过的写法:

const workbook = univer.getUniverSheetInstance('workbook-1'); const sheet = workbook.getActiveSheet(); const commandManager = workbook.getCommandManager(); commandManager.executeCommand({ id: 'sheet.command.set-worksheet-protection', params: { workbookId: workbook.getId(), worksheetId: sheet.getSheetId(), protection: { sheet: true, objects: { selectLockedCells: false, selectUnlockedCells: true, editContent: true } } } });

这里有个细节需要注意:protect.sheet设为 true 只是启动了保护开关,真正控制“哪些格子不能改”的,是单元格本身的locked属性。在 Excel 的思维里,锁定和保护是两层概念:单元格默认是锁定状态,但 sheet 如果不保护,锁定没有任何效果;一旦开启保护,所有“锁定”单元格都不能编辑。想要让某些格子仍可填写,就要把它们标记为非锁定。

所以我们要做的其实有两步:

  1. 开启工作表保护。
  2. 把需要填写的单元格设为“未锁定”。

在 Univer 快照里,单元格的锁定状态在cellData对应格子的s(style)字段里。你可以给单元格加一个自定义样式,写入锁定位,或者更直接一点,在创建模板时就把可编辑单元格的样式设为 unlocked:

cellData: { 0: { 0: { v: '姓名', s: { locked: false } } } }

这样,即使工作表整体保护开启,“姓名”格依然可编辑,而其他格子全部只读。真正做到“不同格子,不同权限”。

4.2 数据校验:下拉选择与必填约束

除了锁定,填报场景另一个刚需是“下拉选择”和“数字范围校验”。例如“部门”这一列只能填市场部、技术部、财务部,不能乱填;“年龄”这一列只能在 18~60 之间。Univer 的数据校验模块可以满足。

添加数据校验的代码可以这样写:

const workbook = univer.getUniverSheetInstance('workbook-1'); const sheet = workbook.getActiveSheet(); const commandManager = workbook.getCommandManager(); commandManager.executeCommand({ id: 'sheet.command.add-data-validation', params: { workbookId: workbook.getId(), worksheetId: sheet.getSheetId(), rule: { uid: 'validation-1', type: 'list', ranges: [ { startRow: 1, startColumn: 1, endRow: 10, endColumn: 1 } ], formula1: '"市场部,技术部,财务部,销售部"', showErrorMessage: true, allowBlank: false } } });

看到formula1是不是有点眼熟?这正是 Excel 数据校验的规则语法。type: 'list'表示下拉列表,formula1里用逗号分隔候选值。给你的表格加上这个后,用户在填“部门”时,单元格右侧会出现下拉箭头,只能选择预设项。这在信息收集时能少掉一大半脏数据。

除了 list,Univer 还支持whole(整数)、decimal(小数)、date(日期)、custom(自定义公式)等类型。实际使用时,把type和formula1换成对应规则就行。

特别注意:如果你的模板是在前端用代码配置,数据校验也可以直接在快照中的dataValidations字段里写死。原理一样,只是入口不同。用命令执行的好处是,你可以在运行时动态给不同用户皮肤下发不同校验规则。

4.3 管理员编辑与用户填报的双模式切换

一个完整的填报系统,通常有两种角色:编辑模板的管理员、提交数据的普通用户。管理员进入页面时可以自由编辑,用户进入页面时只能填指定区域。这个“双模式”本质上是两个状态的切换:

  • 管理模式:关闭保护,所有格子可编辑。
  • 填报模式:开启保护,只有locked: false的格子可编辑。

我建议在初始化时先加载模板原始快照,然后根据角色权限决定是否开保护。如果用户是管理员,直接渲染;如果用户是填报人,就在渲染完成后动态执行一次打开保护的命令。

实际上,这两者甚至可以做成“同一份快照,前端根据角色修改保护字段后再渲染”的方式。也就是说,后端保存的是最原始的模板数据,不写入任何用户权限信息;前端拿到数据后,在内存中把protect改成true、把locked字段标记为 false,再交给 Univer 渲染。这样后端只负责数据存储,不关心业务角色,数据模型非常干净。

对于“用户修改了但在刷新后丢失”的场景,我有一次踩坑是:用户明明是管理员,表格还开着保护,导致怎么都改不了内容。后面才意识到,初始化时把protect写死在快照里了,没有根据角色动态覆盖。解决方案是初始化前判断身份,在调用createUniverSheet时就把protect字段设置好,不要等渲染完成后再去改状态。

5. 数据回传:监听修改、获取快照、保存到后端

5.1 监听用户修改:拿到每一次填写记录

数据填完只是第一步,怎么把数据拿回来才是重点。Univer 提供了一堆事件监听,最常用的是onChange。在 Univer 1.x 中,你可以这样注册监听:

const workbook = univer.getUniverSheetInstance('workbook-1'); const sheet = workbook.getActiveSheet(); sheet.onChange((command, params) => { console.log('用户操作了表格', command, params); });

这个监听会捕获单元格编辑、样式变更、行高列宽调整等操作。需要注意的是,事件回调里的command是 Univer 内部命令名,比如EDIT_CELL。如果你只是想拿到最终填写的值,与其逐条监听,不如在提交按钮里统一读取整个 sheet 的单元格数据。

5.2 读取单元格数据与快照

读取当前表格内容的两种主要方式:

方式一,读取指定单元格:

const workbook = univer.getUniverSheetInstance('workbook-1'); const sheet = workbook.getActiveSheet(); const cellA1 = sheet.getCell(0, 0); console.log(cellA1.v);

方式二,获取整张表的快照:

const snapshot = workbook.getSnapshot(); const sheetData = snapshot.sheets[0].cellData; const jsonString = JSON.stringify(sheetData);

在提交按钮里,我会先把cellData序列化成 JSON,再拼上用户 ID、提交时间等业务字段,发送到后端接口。后端拿到后可以直接存库,因为数据结构很简单,就是一个二维对象。

5.3 多 sheet 与模板复用

实际项目中,往往不止一张表。比如员工填报系统,可能有“基础信息表”“考勤记录表”“绩效评分表”三张 sheet。Univer 一个工作簿可以包含多个 sheet,而getSnapshot()会把所有 sheet 一起返回。你需要注意:不同 sheet 的数据要单独对应业务表,还是全部存成一个 JSON blob。

我的做法是:模板拆成多份独立 JSON,每个业务场景建一个工作簿,而不是把所有场景塞进一个工作簿。原因是权限和校验规则不一样。比如考勤表的可编辑区域是日期列,绩效评分表的可编辑区域是分数列,如果把它们放入同一个工作簿,保护规则的ranges定位就会变得很绕。拆开之后,每个工作簿各管各的,逻辑清晰很多。

另外,如果同一张模板要给多个人填报——比如 10 个部门各自填一张——后端存储时可以按template_id + user_id做维度。前端打开时传两个参数去查询,找到一个已存在的快照就直接渲染,没有则用模板默认快照初始化。这样自然就实现了“同一模板、多人独立填报、互不干扰”。

6. 常见问题与排查技巧实录

6.1 单元格无法编辑,但保护明明没开

这是我第一个遇到的问题:表格渲染出来了,双击单元格却怎么也进不了编辑态。排查下来发现是样式问题。Univer 默认表格的单元格是可以编辑的,但如果你在cellData里给某个格子写了s: { locked: true },即使保护没开,用户选择这个单元格后按钮状态仍是只读。

解决方案有两种:要么在创建模板时不给格子设locked,要么确保保护模式下只有需要填写的格子设locked: false。这里给大家一个经验法则:把“locked 字段默认不设,需要只读的格子才显式设 true”作为模板设计规范。

6.2 数据校验不生效,下拉选项显示为纯文本

我在配置 list 类型校验时,一开始只是把formula1写成了'市场部,技术部',没有加两边的双引号,结果单元格只是显示了字符串“市场部,技术部”,整个内容变成了一个文本值,而不是列表。查阅文档后才想起,Excel 的校验规则里formula1对直接量列表是有严格引号要求的。最终写成'"市场部,技术部"'才恢复正常。

如果你是在代码里拼字符串,一定注意这个引号嵌套。最好把选项数组先join(',')成字符串,再拼一对双引号。这个小细节,坑了我将近半小时。

6.3 锁定样式不生效:合单元格、行/列整体锁定的兼容技巧

在实际做模板时,很多人喜欢用“合并单元格”做表头。当整个表被保护后,默认合并区域内的格子全部锁定。有时候明明给“可填写区”设置了locked: false,但用户还是不能编辑。原因可能是你只改了合并区域左上角的格子样式,其他被合并的格子仍然是locked: true。

Univer 的合并单元格逻辑是,合并区域内的所有格子都会共享左上角格子的样式和数据。但如果你在快照数据里,先写了合并单元格,再给某个格子单独设置样式,覆盖顺序可能会出问题。我的建议是:先落模板样式,再设置可编辑区的 unlocked 样式,最后在打开保护之前,用命令行动态检查指定区域格子的 locked 状态。

6.4 大表格渲染卡顿与包体优化

Univer 的渲染基于 Canvas,正常几千行数据显示没问题。但有一次我导入了一份带大量样式和图片的模板,页面加载时白屏了好几秒。后来用性能面板分析,发现卡在 JSON 快照解析上。问题根源是模板里每个单元格都带有一个独立样式对象,而样式数量达到上万条时,序列化和反序列化开销极大。

应对办法是:模板尽量用简单的默认样式,不要给每个格子单独配置颜色、边框、字体;大批量生成样式时,尽量复用同一个样式 ID。Univer 的样式是采用样式表去重机制的,如果你能在快照中引用已有的样式 ID,而不是每次都新建一份样式,体积能缩减不少。这块尤其影响后端存储和前端加载性能。

6.5 版本升级导致的 API 变化

Univer 目前还在快速迭代中。我在 0.x 版本里写过的一些命令,比如setWorksheetProtection,到了 1.x 改成了set-worksheet-protection这样的短横线命名。如果你们团队打算长期使用,建议锁版本,并在立项时就在代码里封装一层 API 适配层,把所有 Univer 命令调用收敛到一个文件里。后面升级时只改这一处,其他业务逻辑不受影响。

我从这些坑里总结出一个技巧:任何 Univer 相关的能力,先看官方示例中的命令 ID,再去源码里搜对应 Mutation,不要凭直觉猜命令名。命令系统是它最灵活的地方,也是最容易出错的地方。

7. 一些经验和体会

整体来说,用 Univer 做在线填报系统,比我预想中省力很多。最直接的好处是你不需要从零维护格子、公式、样式、剪贴板这些基础设施,相当于你站在了办公套件的高度去做业务。但这句话的反面是:你要适应它的命令系统和数据模型,得花时间理解“快照到底是什么、命令到底发生了什么”。

如果你们团队要跑类似项目,我建议从最简单的“单 sheet 模板 + 锁定单元格 + 提交按钮”开始,先跑通最小闭环,再逐步加多 sheet、校验、协同。不要一上来就铺开所有功能,因为 Univer 的配置项太多,容易在细节里迷失。

关于数据存储,我之前也犹豫过要不要直接用官方自带的协同后端。但对于“管理员建模板、用户在网页上填”这种场景,自研后端反而更灵活,因为模板和填报数据天然分离,你完全可以按自己的数据库设计来。Univer 在这里更像一个“表格引擎”,帮你完成前端交互和解析,而不是一个强绑定的全栈框架。

如果你也打算做类似的表格填报系统,愿不愿意交流一下模板 JSON 的组织方式?尤其是多 sheet 权限和校验规则这部分,我最近还在琢磨更好的实践。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询