从去年开始,我们团队就在做内部的数据登记系统。试过手写表格、用过在线文档、也开过表单工具,最后前端交互的主力引擎落在了 Univer 上。Univer 是一款开源的在线表格引擎,嵌进 Web 应用以后,用户不需要安装 Office,开个浏览器就能直接编辑表格。真正让我决定用它的原因,是它能把“用户自定义表格”和“单元格级填写权限”一起解决:业务人员可以自己搭表头、配模板,然后把表格发布出去,其他人只能在指定单元格里填内容,剩下的行列哪怕一个字符都改不了。这个需求听上去不算复杂,但真做起来背后牵扯到权限模型、数据回写、协作冲突一堆问题。这篇就把我们选型、落地、踩坑的全过程梳理一遍,给同样要做“在线表单式表格”的朋友当个参考。
1. 先搞清楚需求:你要的是“表格”还是“填单系统”
1.1 用户自定义表格到底在自定义什么
很多朋友收到需求时,第一反应是“这不就一个在线 Excel 吗”。但真正跟业务方聊完,你会发现他们需要的不是 Excel,而是一种更轻、更可控的数据收集工具。用户自定义表格,自定义的不是线性的字段列表,而是一张二维的、带行列关系的模板结构:哪些列是表头、哪些列是数据项、哪些区域要合并单元格、哪些列要加下拉校验、哪些单元格需要固定成公式计算。
举个例子。我们市场部每个季度要收集代理商的回款计划,过去靠微信群收 Excel 文件,再人工合并,又慢又容易漏。后来希望业务人员自己新建一张表格模板,列做成“代理商名称、回款月份、计划金额、实际金额、备注”,再把“实际金额”和“备注”开放给代理商填写,“计划金额”汇总列由公式自动算。这整张表的结构,就是“用户自定义表格”的核心。它跟普通表单的差别在于:表单是一行一条记录的录入界面,表格则是面对一个完整的二维数据区域,用户可以直观看到行列关系、复制粘贴、批量操作。
所以我在理解需求时,一定会先问几个问题:表头由谁定、数据由谁填、填完之后谁来做统计分析、需不需要多批次数据放在同一个表里。这几个问题确定以后,才轮到技术方案选型。
1.2 单元格级权限:业务场景里的硬需求
有了自定义表格,紧接着就会遇到权限问题。整张表全部锁定很简单,但业务上几乎不可能这么用。真正要控制的是:哪些单元格允许编辑,哪些单元格只能看,哪些单元格连看都要限制。
常见的场景有这几类。一是“计算列保护”,比如总金额、平均值、进度百分比是由公式算出来的,用户如果手改,数据就不再可信;二是“结构化列锁定”,比如表头、单位、版本号这些元信息,谁都不许动;三是“部分开放填报”,比如一张会议签到表,参会人只能改自己的那条记录,最多把自己的姓名、手机号、到达时间填进去,其余列一律灰色不可点。
这本质上是一个区域级别的权限控制,不是整体开开关关的布尔权限。Univer 在这块提供了很灵活的授权模型,可以把“查看权限”和“编辑权限”拆开,权限作用范围可以精确到某一个 Range(单元格区域)。这个能力直接决定了业务能不能落地,也让我在评估方案时把它放在了最高优先级。
2. 为什么选型落在 Univer:开源表格引擎的对比手记
2.1 主流开源方案各有什么脾气
做技术选型的时候,我把市面上的方案都快速过了一遍。每个方案都有自己的定位,不能简单说谁好谁坏,关键看匹配度。
最先看的是 Luckysheet。它的 UI 同样类 Excel,社区热度不低,日常编辑体验还不错。但它给人最大的顾虑是项目维护节奏不稳定,文档和版本之间经常对不上。如果你只做内部表格编辑器,不涉及复杂的权限和协作,Luckysheet 是能用的,但要做深度的业务定制时,需要花不少力气去读源码。
SheetJS 则完全是另一个方向。它把精力放在 Excel 文件的解析和生成上,读写能力强、兼容性好,但基本不提供 UI 编辑能力。可以把 SheetJS 当成“数据处理引擎”,做服务端解析、导入导出、格式转换都很合适,但要拿它做“用户点击单元格填数据”的前端交互,等于要从零造轮子。
Handsontable 和 AG Grid 属于数据网格 / 数据录入控件,对键盘导航、单元格类型、校验规则支持得不错,适合做复杂的数据录入界面。但它们的定位不是“Excel 模拟器”,公式解析、工作表结构、图表这些能力相对薄弱,如果你需要的是一张能算公式、能做汇总的在线表格,这两个方案就显得不够了。
另一类是完全商业化的产品,比如 SpreadJS。功能非常完整,稳定性也经过大量验证,Excel 兼容度很高,技术支持完善。缺点是授权费用不低,且前端技术栈相对封闭,想基于它做深度定制和二次开发,受到的限制更多。
最后落到 Univer。它是基于 TypeScript 开发的开源项目,定位就是一套完整的工作簿 / 工作表 / 文档 / 幻灯片引擎。相比前面几个方案,Univer 最打动我的是架构的开放性和权限模型的精细度。它不是一个只能展示表格的控件,而是一个可以编程的表格内核。
2.2 Univer 在架构上做对了什么
Univer 能承接“用户自定义表格 + 单元格级保护”这种需求,背后有几个关键架构特点,我觉得值得单独拿出来讲。
第一个是命令系统。Univer 里所有编辑操作都会包装成一条条 Command,统一的命令入口意味着任何改动都有迹可循,可以被撤销、重放,也可以在执行前插入校验逻辑。权限控制正好可以挂在命令执行这一层:用户确实点了编辑,但命令校验不通过,操作就会被拦截。这比纯粹靠 UI 置灰要扎实得多。
第二个是数据模型与 UI 解耦。工作簿、工作表、单元格这些核心对象是独立的数据结构,不依赖界面上哪个按钮是否显示。也就是说,模板可以保存成一份 JSON 快照,后端存一份、前端恢复一份,界面重新渲染之后依然是一张一模一样的表格。这个特性恰好是“用户自定义表格”的基石:用户定义的每个表头、每个合并单元格、每条公式,本质上都是在修改这份数据快照。
第三个是区域(Range)模型和权限点设计。Univer 里的权限不是简单的工作簿级开关,而是把“工作表”和“区域”分开处理。你可以给整张表设置默认不可编辑,再单独把某个 Range 授权给某个人或某个角色。这样实现的最终效果就是:全部单元格锁死,但指定区域可以填数,甚至不同区域可以授权给不同人,实现一个表多角色分头填报。
3. 核心机制拆解:模板、锁定、填数和数据回收
3.1 一张“用户定义表格”在 Univer 里是怎么组织的
要做自定义表格,首先得理解 Univer 对数据的分层。最外层是 Workbook(工作簿),里面可以有多个 Worksheet(工作表),每个工作表由若干行列交叉形成的 Cell(单元格)组成。单元格除了记录填写的值,还保存样式、格式、校验规则等附属信息。
模板定义的过程,本质上是往这些空白单元格里写入“初始数据”。写表头就是给第一行单元格 setValue;合并单元格就是设置跨行跨列的样式;下拉校验是给某个 Range 配置 data validation;公式列则是往单元格里写入“=SUM(...)”一类的表达式。所有这些操作完成后,整个工作表的状态可以序列化成 JSON。
持久化时,我们把这份 JSON 当作模板快照保存到后端。业务人员下次想创建类似表格,直接复制快照再调整;填报人打开表格时,前端拿到快照后重新加载,界面上和业务人员编辑时看到的一模一样。由于数据模型是纯数据,不掺杂 UI 代码,快照恢复的体验非常稳定,这一点我们实际测试下来深有体会。
3.2 保护与解锁:权限点的两层设计
“让用户填写指定单元格,其他单元格不可修改”这句话,在 Univer 里对应着两层配置,理解清楚这两层,才不会在代码里来回踩坑。
第一层是工作表级别的默认保护。可以理解为给整张表上了一把锁,默认把所有单元格都设成不可编辑。开启这一层之后,不管数据模板什么样,普通用户点哪个格子都进不了编辑状态。
第二层是区域级别的授权。我们需要在保护之上,把某些 Range 加进白名单。白名单里的区域,普通用户才能正常输入。这样组合出来的效果就是“默认全部锁定,白名单局部开放”。
实际操作时,我们会把两个动作拆开执行。管理员创建模板时,先以管理员身份打开表、写入表头、设置公式,然后开启默认保护;再显式把“B2:D100”这类数据填写区加入可编辑白名单。这里要特别提醒:如果忘记开启默认保护,白名单就是无效的,因为默认情况下所有区域都可编辑,权限模型不会自动把其他区域锁住。
3.3 从表格编辑器到“只让填一部分”的交互改造
Univer 自带的是完整表格编辑器,工具栏、公式栏、工作表切换都具备。但给普通用户用的时候,我不建议把完整工具栏直接晾出来。一是不好看,二是容易误操作。
我们做了一层“填表模式”的交互改造。普通用户进入的页面只显示数据和必要的提交按钮,工具栏隐藏,公式栏关闭,单元格默认不进入编辑态。用户点击一个空白可填格时,才会唤起输入状态,一旦焦点离开,立刻恢复展示态。这样做的好处是减少了干扰,也让非技术背景的用户能快速理解“我只需要填这部分”。
这块改造可以用 Univer 的上层 UI 裁剪能力,也可以自己写一套外层容器,把表格当嵌入式区域挂在中间。我们选择的是后者,因为整体页面风格统一、按钮自定义空间更大。要注意的是,隐藏工具栏只是 UI 层面的事情,真正兜底防线还是前面说的权限点校验。UI 可以防君子,权限模型才能防那些试图绕过界面的操作。
4. 实操落地:搭一个“可填写指定单元格”的在线收数表格
4.1 环境准备与基础初始化
下面我用一套最小可运行的前端 Demo 来演示整个思路。技术栈选择 Vite + TypeScript,轻量、上手快。
先创建项目并安装依赖:
npm create vite@latest univer-demo -- --template vanilla-ts cd univer-demo npm install @univerjs/core @univerjs/sheets @univerjs/ui需要说明的是,Univer 版本迭代很快,不同版本的包名和初始化方式会有差异。我这里给的是基于 0.x 早期版本常用的写法,如果你拿到的是更新的版本,建议直接参考官方文档的入门模板。核心思路不变,就是把一个 Univer 实例挂到页面容器上:
import { Univer } from '@univerjs/core'; import { UniverSheetsPlugin } from '@univerjs/sheets'; import { UniverUIPlugin } from '@univerjs/ui'; import '@univerjs/ui/lib/index.css'; const univer = new Univer({ container: 'app', plugins: [ UniverSheetsPlugin(), UniverUIPlugin() ] });这个阶段应该能看到一个空白的表格编辑器渲染出来。初始化成功以后,我们再做模板填充和权限设置。
4.2 定义模板并导入 Univer
有两种方式生成模板。第一种偏技术,直接在代码里逐格写入表头和数据列:
const workbook = univer.createUniverSheet({ id: 'template-001' }); const sheet = workbook.getActiveSheet(); sheet.getRange('A1').setValue('姓名'); sheet.getRange('B1').setValue('部门'); sheet.getRange('C1').setValue('本月绩效'); sheet.getRange('D1').setValue('备注');第二种更贴近业务人员:先让管理员用 Excel 把模板做好,然后通过 Univer 的 XLSX 导入能力把文件加载进来,最后把当前工作表快照 JSON 保存到后端。之后每次创建新收集任务,后端只需要下发同一份模板 JSON,前端 load 一下就能恢复表格。
我倾向于第二种,因为业务人员对 Excel 更熟悉,让他们用 Excel 排好格式、写好公式再上传,学习成本最低。而且这种模板天然就带好了列宽、背景、合并单元格等样式,省去在前端代码里逐个设置的工作。
4.3 锁定只读区域、开放填写区域
模板加载完成之后,就要进入权限设置环节。核心代码如下:
// 伪代码示意,具体API名称以实际使用版本为准 const sheet = workbook.getActiveSheet(); // 第一步:开启工作表默认保护,所有单元格默认不可编辑 sheet.protectWorksheet({ editable: false }); // 第二步:把填写区域加入白名单 sheet.protectRange('B2:D100', { editable: true });这里有几个细节需要留意。第一,锁定动作应该以管理员身份完成,因为权限通常是绑定用户身份的,如果普通用户也带管理权限,等于白设了。第二,保护要写进模板快照一起保存,不能只在前端内存里设置。如果模板 JSON 里没带权限信息,用户重新打开表格后,保护就丢了。
还有一点,解锁的区域要预留足够的行数。比如预估有 100 个人要填写,白名单就开 100 行。后期不够用了,管理员可以再次调大范围,已提交的数据不受影响。
4.4 提交数据与后端对接
数据回收有两种方案,取决于业务需要。
一种是把表格内容直接导出为 XLSX 文件,提交到后端保存。Univer 提供导出能力,前端拿到文件流后通过 FormData 上传:
const blob = await univer.exportXlsx(); const formData = new FormData(); formData.append('file', blob); fetch('/api/collect', { method: 'POST', body: formData });这种方案适合管理员确实需要把结果拿回 Excel 里做二次分析的场景,和线下表格的兼容性最好。
另一种方案是遍历所有单元格,把填写区域的数据抽成 JSON 提交:
const data = sheet.getRange('B2:D100').getValues(); await fetch('/api/collect', { method: 'POST', body: JSON.stringify({ data }) });JSON 方案适合后续要对接数据库、做实时统计的系统。我们的内部系统用得比较多的是第二种,因为提交完以后马上就能在汇总面板上看到记录,不需要再经历一次 Excel 解析入库。
4.5 完整业务闭环怎么跑通
把前面几段串起来,就得到一套完整的业务流程。管理员在后台新建一个表格模板,导入 Excel 或在线编辑,然后设置默认保护和填写白名单,保存快照并生成一个分享链接或二维码。普通用户打开链接,进入填表模式,看到的是已经被锁好的表格,只有白名单里的单元格可以输入。填写完成以后点击提交,数据进入后端,管理员在汇总页面实时查看填写进度,也可以一键导出所有结果。
这套流程我们跑了半年多,覆盖了十几个收集任务,稳定度比我预想中要好。最关键的体验改善是:业务方不再用微信发 Excel 了,数据不会被改乱,也不会出现同一个文件被多人改版本的问题。
5. 实战中踩过的坑:权限、性能和兼容性
5.1 保护哪里都锁死了:权限配置的三类失误
权限这块是踩坑重灾区,我们第一次上线时就遇到三种典型问题。
第一种,开启保护以后所有人都不能编辑。原因通常是白名单没有正确生效,或者用户身份没有传入权限系统。Univer 在做权限判断时需要知道当前是谁在操作,如果前端没有注入用户标识,默认会把它当匿名用户处理,而匿名用户往往没有区域授权。解决方法是把当前用户 ID 传给权限上下文,并且确保白名单里的授权对象和这个 ID 对应上。
第二种,受保护区域虽然不能改,但复制粘贴能绕过。纯 UI 层的禁用拦不住浏览器剪贴板操作。要彻底堵住这个口子,必须在命令系统层面拦截“粘贴到受保护区域”的行为,服务端提交时再做一次二次校验。客户端只是体验层,服务端校验才算最终防线。
第三种,模板保存后再打开权限丢失。问题出在保存快照时只保存了单元格数据和样式,没有把工作表保护状态和区域授权一并持久化。解决方式是建立一套“模板快照 = 表格数据 + 权限配置”的组合结构,保存和恢复都走同一个序列化接口。
排查权限问题时,我习惯按这个顺序:先看用户身份有没有传对,再看保护状态是否写入快照,最后看白名单范围和用户授权是否完全匹配。只要这三层对齐,绝大多数锁不住或者全部锁死的问题都能解决。
5.2 大模板加载慢与渲染优化
表格模板一旦加上大量样式、合并单元格和批注,JSON 体积很容易上去。我们有一个模板因为包含了全年的周报格式,快照接近 1MB,在低配电脑上打开要等三秒多,体验非常差。
后来做了三件事。第一,模板 JSON 走后端缓存并做压缩,网络传输时间明显下降。第二,前端按需加载资源,把 Univer 相关代码拆成独立 chunk,不让它拖慢首屏。第三,对模板里的样式做清理,一些无用的空样式都会被脚本去掉。优化之后,打开速度降到一秒以内,完全够用。
如果后续数据量继续变大,还有一个思路是只加载当前可视区域附近的数据,滚动到哪加载到哪,也就是虚拟化方案。Univer 本身用 Canvas 渲染,对大数据量相对友好,但初始化阶段解析全部单元格数据依然有开销,要根据实际行数做取舍。
5.3 移动端填表体验与软键盘问题
内部用户不可能永远坐在电脑前,移动端填表是绕不开的。我们最初直接把桌面端页面套到手机上,效果不太好:工具栏太占空间、单元格点不中、软键盘一弹就遮住内容。
改进方案是重做移动端交互。手机上进入的不是完整表格编辑器,而是一张精简的“逐格填单页”:展示表头和其他只读区域,下方列出所有可输入字段,用户像填表单一样逐个输入,最终统一写入表格并提交。这样既保留了表格的结构化优势,又避开了小屏上的复杂操作。
这里有一个细节:iOS 下 Canvas 渲染的表格在快速滚动时会出现轻微模糊或白块,需要检查是否开启了合适的 CSS 属性,并建议测试时多拿中低端安卓机和旧款 iPhone 过一遍。
5.4 版本升级带来的“惊喜”
Univer 目前还处于快速迭代阶段,API 变动频繁。我们从 0.1 左右版本开始接入,中间升级过几次,每次都伴随着一些破坏性变更。常见的有插件注册方式改了、权限 API 参数格式变了、命名空间换了地方。
应对这种问题,最有效的方法是锁定依赖版本,不要轻易跟随最新版。我把package.json里的 Univer 相关包版本固定下来,升级时单独开一个分支做回归,重点测试导入导出、权限保护、公式计算三个核心链路。没有充分测试之前,绝不在生产环境盲目升级。
另一个建议是不要在业务代码里散落太多 Univer 的底层 API 调用,尽量封装一层自己的服务接口,比如createTemplate、lockWorksheet、submitData。这样 Univer 升级导致底层 API 变化时,我们只需要改封装层,而不是改所有业务页面。
5.5 常见问题速查表
| 问题现象 | 可能原因 | 解决办法 |
|---|---|---|
| 所有单元格都不能编辑 | 工作表保护开启但白名单未配置,或用户身份未注入 | 检查权限上下文是否传入 userID,确认白名单授权范围 |
| 受保护区域仍能粘贴写入 | 客户端 UI 禁用不拦截剪贴板事件 | 在命令系统层面拦截粘贴到受保护区域,服务端二次校验 |
| 模板恢复后保护失效 | 快照只保存了单元格数据,没有保存权限配置 | 扩展模板 JSON,同时保存权限快照 |
| 手机端打开表格很卡 | 完整编辑器渲染开销过大 | 使用逐格填单页,按字段列表引导输入 |
| 升级版本后初始化报错 | Univer API 不兼容 | 固定依赖版本,拆分升级,先跑权限与导出回归 |
| 提交数据偶尔有乱码 | 前端 JSON 编码或后端解析不一致 | 统一使用 UTF-8,后端解析前做格式校验 |
6. 一点心得:别把它当 Excel,要当表格内核用
项目做到现在,我最大的感受是:Univer 带来最值钱的不是“又一个在线 Excel”,而是把复杂表格能力封装成了一个可编程的内核。用户自定义表格、单元格保护、公式计算、数据导入导出,这些能力单独拎出来每一个都不算简单,但 Univer 把它们组合在了一套标准的插件架构里,我们只需要把业务灌进去就行。
真正需要投入时间设计的,反而是业务层面:哪些区域是可填区、哪些角色能改哪一行、提交的数据以什么形态回收、模板的版本怎么管理。这些想得越早,后期的返工越少。我们目前还在沉淀一套内部的模板库,让各业务方直接复用成熟模板,同时准备接入协作能力,让多人同时填报时也能保持数据一致。如果你正准备用在线表格做数据收集,我建议先花一天时间把业务规则写成一份简单的授权矩阵,然后再动手写代码,会省掉后面大量的权限调试时间。