
1. 项目概述Univer是什么解决了什么问题第一次看到Univerhttps://github.com/dream-num/univer这个项目时我正好在一家做企业服务外包的公司做技术咨询客户提了一个特别常见又特别挠头的需求能不能做一张半开放的在线表格业务人员把模板定义好下游的人只能在特定列里填数据其他区域无论如何都不能改。这个需求听起来简单但实际操作起来非常折腾。用现成的在线Excel方案权限粒度不够要么整个表开放编辑要么整个表只读做不到这批单元格能填、那批单元格碰不了自研一个表格编辑器成本又高得离谱。直到我翻到Univer才发现原来这个问题的标准答案早就存在了。Univer是一个基于TypeScript构建的开源办公套件主打Web端的高性能电子表格、文档和幻灯片。它由Sheetbest团队孵化在GitHub上开源后Star涨得很快核心强项是电子表格文档和幻灯片是后补的模块。简单说你可以把它理解为一个能在浏览器里跑得很顺的Excel替代品但它不只是复制Excel的交互还做了一整层插件化和权限控制的基建正好解决了表格模板指定单元格填写这类真实业务场景。这博文我想聊的不是照抄一遍官方文档而是结合我实际拿它做可填写单元格模板的经验带你过一遍为什么选Univer而不是其他方案模板怎么定义单元格怎么锁定用户数据怎么收集回来以及那些文档里没写清楚的坑。适合谁看前端/全栈工程师想接一个靠谱的Web表格引擎自己控制交互和权限产品经理/技术负责人正在调研在线表格数据收集类功能怎么落地独立开发者想做一个门店填报、库存盘点、问卷收集类的小工具但又不想从零写表格。看完这篇你应该能自己跑起一个管理员定义模板、用户填写指定单元格的最小可复刻版本。2. 方案选型为什么是Univer而不是其他表格库2.1 从业务需求反推技术选型先把半开放表格的需求拆清楚。真实业务里这种表通常长这样总部下发一张《门店周经营数据表》列头是门店名称、周营收、客流、客单价、损耗率这些列是固定的每家店每天打开同一张表只需要填自己那一行的营收、客流等数据表头的说明文字、公式列、汇总区域、别人的数据都不允许改。拆完需求技术选型的核心矛盾就暴露了普通表格组件只负责画表格权限和可编辑区的控制得自己做。大多数开源表格库比如Handsontable、AG Grid虽然支持单元格编辑开关但要么是整行整列级别要么自己维护一套状态非常累尤其当可编辑区还是不规则区域时实现起来越发复杂。拿Handsontable举例它的cells回调里确实能按行列号返回isCellEditable但你要是想做成某些行可编辑、某些行只读、再叠加数据校验代码会越写越绕。而且Handsontable在非商业协议下对生产部署有限制很多公司评估完直接放弃。Univer的思路不同——它的底层被设计成一个文档/表格引擎不只是渲染组件还有一套完整的命令系统、插件系统和选区模型。编辑权限不再是你自己写的if判断而是贴近Excel原生体验的保护工作表可编辑区域机制定义一次整张表按规则生效。2.2 Univer的架构特点理解了才能用好Univer的架构是分层设计的我不讲太虚的概念就说你接项目时需要知道的核心三块。渲染引擎Univer用的是自己写的一套Canvas渲染层不是简单的DOM拼格子。这套渲染层让它在渲染大量单元格时性能很稳我拿它渲染过几万行数据滚动、框选、缩放都还跟手。对于给几百人用的内部填报表性能完全不是瓶颈。命令系统Command这是Univer的灵魂。你在界面上做的每个操作改单元格、合并、撤销、粘贴都会被封装成一个Command通过统一的执行器分发。这意味着你可以拦截、定制、编排操作。比如用户填完一格后自动触发公式计算这种需求就是靠监听Command完成的。插件系统PluginUniver很多功能都以插件形式存在用的时候按需引入。核心包只负责基础表格渲染和编辑公式、数据校验、协同编辑、导出这些全是插件。这带来一个现实好处你不需要把整套东西都塞进项目调试和打包体积都可控。这几个特点组合起来正好对应模板表格指定填报这个场景用插件只加载需要的能力用命令系统控制用户操作边界用选区/保护机制定义可编辑区域。注意选型时别被Excel在线版这个标签带偏。Univer的目标是做一个可编程的办公基座不是要100%复刻Excel。如果你的核心需求是兼容所有Excel函数且分毫不差建议老实上SheetJS或微软的Excel Online嵌入方案如果需求是自定义交互、可控填写流程、自己在上面做业务Univer很合适。3. 核心机制拆解Univer如何实现指定单元格可填写、其他单元格锁定3.1 保护与权限的基本模型这个看家功能底层其实对应Excel里的保护工作表Protect Sheet概念。Univer把这一套搬到了Web端而且做得更细。在Univer里一张工作表的保护状态由几个层级共同决定工作表级别整张表是否开启保护选区级别在开启保护的前提下哪些选区的单元格被设置为允许编辑单元格/区域级别的校验这里指的是数据校验规则可以挂在特定区域上校验不通过就禁止填值或给出警告。实际配置时通常就是先锁全表再开洞。默认情况下新建的工作表是所有人可编辑的你要把它变成只有指定区域可填逻辑上就是把整张工作表设为保护状态禁改全部单元格把允许用户填写的那个区域比如A1:C10加到例外列表设为可编辑用数据校验插件给可编辑区域加上约束比如周营收必须是数字且大于0。这套模型和Excel保护工作表的心理模型几乎一致熟悉Excel的用户上手很快开发者要维护的状态也很清晰。整个配置过程不会中途让表格进入全可编辑再全只读的抖动状态这一点在真实填报表里很重要——用户可不想打开表格的时候一不小心把你的模板公式给删了。3.2 为什么这比传统表单体验好市面上很多做填报表的场景最终做出来长得像问卷不像Excel。用输行的方式一条条录入和用Excel网格一行行列填效率差距是很大的。Univer这个方案的价值在于保留Excel的操作习惯同时用保护机制收敛用户的编辑范围。用户在熟悉的网格界面里按Tab可以跳到下一个可填单元格能看公式计算结果能筛选排序其他用户的填报行录入效率高培训成本低。打个比方传统表单像是每人发一张答题卡按顺序作答Univer保护单元格的表单像是发给你一张已经用Excel表做好的空表锁死所有不该动的区域只在留白的格子里填。后者对业务人员来说几乎零学习成本。如果你的业务是内部员工填写报表、经销商上报库存、门店提交日报这个体验优势非常明显。3.3 数据收集的另一种思路模板填写而不是页面表单还有一个容易被忽略的点Univer让表格模板本身变成了业务流程的载体。传统开发里做一个月度计划填报功能往往要做一套页面列表页、详情页、表单页、校验逻辑、状态流……一套下来工作量不小。但用Univer的思路业务人员直接在表格里把模板设计好——哪列是标题、哪列写公式、哪些区域锁定然后你用代码把这张表发布成待填写模板用户打开即是表格填完即交。后续如果你在Univer上接协同编辑支持多人同时编辑同一张表还能看到这个区域是谁在填、那个单元格是谁改的这类状态。如果我做的是几百人同时上报数据的业务多人协同本来就该是标配。4. 实操记录从零搭一个模板表格指定填写项目4.1 环境准备与最小工程搭建Univer目前对前端框架是框架无关的官方示例里有原生JS、React、Vue三种用法。我这边的实操就按最通用的React来做说明用的Univer版本是当前npm上的稳定版写这篇时约在0.x后期1.x已经完全可用了。先初始化一个Vite React项目npm create vitelatest univer-demo -- --template react-ts cd univer-demo npm install然后安装Univer核心包和需要用到的插件包npm install univerjs/core univerjs/sheets univerjs/sheets-ui univerjs/ui univerjs/engine-render univerjs/engine-formula npm install univerjs/sheets-formula univerjs/sheets-data-validation univerjs/sheets-exchange译核心概念univerjs/core是数据模型和命令中心univerjs/sheets是工作表逻辑univerjs/ui负责外框UI和工具栏univerjs/sheets-ui是把工作表渲染成可交互的界面。所以——你看到的带工具栏的表格界面是通过UI包把逻辑和渲染组装出来的。4.2 在React中初始化Univer实例Univer初始化的代码官方文档里有但我把它收敛成一个按照业务来的写法方便后续加保护配置。创建一个src/univer.tsimport { Univer, UniverInstanceType } from univerjs/core; import { defaultTheme } from univerjs/design; import { UniverFormulaEnginePlugin } from univerjs/engine-formula; import { UniverRenderEnginePlugin } from univerjs/engine-render; import { UniverUIPlugin } from univerjs/ui; import { UniverSheetsPlugin } from univerjs/sheets; import { UniverSheetsUIPlugin } from univerjs/sheets-ui; import { UniverSheetsFormulaPlugin } from univerjs/sheets-formula; import { UniverSheetsDataValidationPlugin } from univerjs/sheets-data-validation; export function createUniver(container: HTMLElement) { const univer new Univer({ theme: defaultTheme, locales: [en-US, zh-CN], }); // 核心渲染与UI univer.registerPlugin(UniverRenderEnginePlugin); univer.registerPlugin(UniverUIPlugin, { container, header: true, // 显示顶部工具栏 toolbar: true, footer: true, }); // 表格 univer.registerPlugin(UniverSheetsPlugin); univer.registerPlugin(UniverSheetsUIPlugin); // 公式填报表常见的使用场景比如小计、汇总、自动算比率 univer.registerPlugin(UniverFormulaEnginePlugin); univer.registerPlugin(UniverSheetsFormulaPlugin); // 数据校验限定用户填的值得合规 univer.registerPlugin(UniverSheetsDataValidationPlugin); return univer; }在组件里挂载import { useEffect, useRef } from react; import { createUniver } from ./univer; export default function SheetEditor() { const containerRef useRefHTMLDivElement(null); useEffect(() { const container containerRef.current; if (!container) return; const univer createUniver(container); return () { univer.dispose(); }; }, []); return div ref{containerRef} style{{ width: 100%, height: 600px }} /; }注意univer.dispose()在组件卸载时一定要调用否则会内存泄漏。如果页面里同时创建多个Univer实例每个实例都绑定一个容器DOM状态互不干扰这个设计在一张页面多个模板表的场景里也适用。4.3 创建初始工作簿并定义模板这时你看到的是一张空工作簿下一步是往里面塞模板内容。Univer里创建工作簿和写入单元格数据的API有两种方式一种是用命令流univer.getCommandService().executeCommand(...)更贴近底层另一种直接用数据API适合初始化场景。我这里用更简明的数据API思路先拿到当前工作簿的sheet实例然后按行列写入单元格。const univer createUniver(container); const workbook univer.getCurrentUnitForType(UniverInstanceType.UNIVER_SHEET)!; const sheet workbook.getActiveSheet();写入表头和说明文字sheet.getRange(0, 0).setValue(门店名称); // A1 sheet.getRange(0, 1).setValue(周营收); // B1 sheet.getRange(0, 2).setValue(客流量); // C1 sheet.getRange(0, 3).setValue(客单价); // D1 sheet.getRange(0, 4).setValue(自动汇总); // E1用公式给表头上个底色、加粗让用户一看就知道这行是标题别动const headerRange sheet.getRange(0, 0, 1, 5); // 第1行A~E headerRange.setBackgroundColor(#F2F2F2); headerRange.setFontWeight(bold);再给E列设置一个公式自动算当行营收假设用户填了B和D// E2单元格第2行第5列对应index 1,4 sheet.getRange(1, 4).setFormula(IF(AND(B2,D2), B2/C2, ));这里只是示意具体公式按你的业务写。自动汇总列就实现了——用户在B、C、D输入数据E列自动算且E列在后面的保护设置里也设为禁止编辑因为它的值应完全由公式推导不应该让用户手输。4.4 核心步骤保护所有区域只开放指定单元格这是正题。Univer里把一个选区设为可编辑和保护要用的是univerSheet里的保护命令。官方命令名称和参数我整理成稳定可用的写法。先获取Workbook的commandService然后依次执行import { CommandType } from univerjs/core; import { SetWorksheetProtectCommand } from univerjs/sheets; // 1. 开启整表保护 await univer.getCommandService().executeCommand({ id: sheet.command.protect-sheet, type: CommandType.COMMAND, params: { unitId: workbook.getId(), subUnitId: sheet.getSheetId(), protect: true, }, });这里我用的是字符串形式命令ID实际上如果你引入的包里导出了命令常量更推荐用常量引用避免字符串拼错。下面这段是设置允许编辑区域的等效做法本质是向保护规则里加入例外区间// 开放给用户填写的范围第2行到第20行B列到D列 const editableRange { startRow: 1, startColumn: 1, endRow: 19, endColumn: 3 }; await univer.getCommandService().executeCommand({ id: sheet.command.add-protection-range, type: CommandType.COMMAND, params: { unitId: workbook.getId(), subUnitId: sheet.getSheetId(), range: editableRange, permission: true, // 该选区内的单元格允许编辑 }, });执行完这一步你的工作表就会被锁住除了第2~20行的B~D列其余单元格用户无法修改。想继续开放别的区域就再追加一个add-protection-range。这里有个隐藏细节add-protection-range的名称在不同小版本里可能微调过。如果你在跑的时候发现这个命令ID执行没反应优先去看node_modules里univerjs/sheets的src/commands/commands/sheet.protection文件命令的真实ID以源码为准。我在好几个版本上试过整体稳定但命名确实有过小改动。为了更保险我这里再给出一个开发时调试的小技巧// 在某次配置后可以打印一下当前sheet的保护状态 const protection sheet.getProtection(); console.log(protection?.getRules());通过打印保护规则你能清楚地看到哪些范围是锁的、哪些是放开的避免以为开了结果没开的尴尬。4.5 加上数据校验把能填变成能填对用户能编辑了不等于填得对。我们在锁定逻辑之外再给B~D列挂上数据校验规则。数据校验插件的用法是拿到sheet上的数据校验管理器然后添加规则。以周营收必须是数字且大于0为例import { DataValidationType } from univerjs/sheets-data-validation; const dvManager workbook.getActiveSheet().getDataValidationManager(); dvManager.addRule({ type: DataValidationType.NUMBER, comparator: DataValidationComparator.GREATER_THAN, value1: 0, allowBlank: true, showErrorMessage: true, errorMessage: 周营收必须是大于0的数字, ranges: [{ startRow: 1, startColumn: 1, endRow: 19, endColumn: 1 }], });注意这个DataValidationComparator枚举可能有版本差异不同版本里比较器的类型定义和参数名可能会变你写的时候多参考当前版本的类型声明。为了不误导我就不把枚举的具体路径写死原理搞懂即可。用小版本不稳定的教训是数据校验的规则对象在Univer 0.2和0.3之间有变化如果你发现规则生效但样式不显示可以查一下univerjs/sheets-data-validation的更新日志把插件包升级到匹配版本。4.6 监听用户填写行为拿到提交的数据用户填完业务上总得把数据收回来。Univer的Command系统在这里就是事件总线我们监听每次修改单元格的命令就能捕获用户输入。常用的监听方式有两种一种监听Command执行前的派发前置拦截一种监听执行后的事件数据同步。收数据用后者。import { SetRangeValuesCommand } from univerjs/sheets; univer.getCommandService().onCommandExecuted((command) { if (command.id SetRangeValuesCommand.id) { // 拿到被修改的单元数据 const { unitId, subUnitId, range, value } command.params as any; console.log(用户修改了单元格, range, value); // 在这里把你关心的数据发送到后端接口 // handleSubmit(unitId, subUnitId, range, value); } });这个监听是每次改动都触发适合做实时保存或者操作日志。如果做成用户点提交按钮再全量保存那就更简单了——按钮点击时直接用API把sheet上某个区域的数据导出const values sheet.getRange(1, 1, 19, 3).getValues(); // 得到一个二维数组可以直接转JSON提交给后端数据到这就真正收回来了。从定义模板、锁单元格、开放填写、限制校验到同步数据一条链路闭环。5. 常见问题与排查技巧实录5.1 保护设置不生效用户还是能随便改这是我被问得最多的问题。排查思路依次是确认是否真的执行了SetWorksheetProtectCommand。最容易犯的错是把开启保护写在了创建工作簿之前命令发出去了但没有可操作的工作表自然无效。确认允许编辑区域的坐标是零基还是1基。Univer的行列索引是零基0开头但Excel用户习惯1开头的行列号。我第一次用的时候把{ startRow: 2, startColumn: 2 }当成了从第2行第2列开始实际上是从第3行第3列开始。建议先在界面上把可编辑区域高亮出来测试。确认UI层的编辑是否真的走了命令系统。如果你在外部强行用API改单元格值绕过命令系统保护不会拦你——因为保护本质上拦截的是用户交互命令不是数据API。所以后端回调时不要用API直接改用户不该改的区域自己要有数。5.2 公式列不自动重算Univer的公式引擎默认是会重算的但有些场景比如预设公式时引用的单元格还没值会导致不显示结果。常规解决办法检查UniverFormulaEnginePlugin和UniverSheetsFormulaPlugin是否都注册了。只注册了UI或者核心公式引擎是缺的单元格里显示的是公式字符串而不是值。另外一个坑如果你在模板初始化时用setFormula写公式恰巧引用的单元格在公式执行时为空显示空值是正常的用户填入数据后就会自动计算。不用急着排查。5.3 多用户同时填同一张表数据会不会互相覆盖如果你没接协同编辑插件collaboration那Univer就只是单机版最后写入的人覆盖前面的数据。要支持几百人同时填写同一张表得接入协同插件和你的后端同步服务。官方有协作方案示例一般接好WebSocket服务和OT/CRDT逻辑。但注意单纯的填报表场景不一定非要上协同。如果业务只是每人填自己那一行、内容相对独立完全可以用各自打开模板 → 本地填 → 提交到后端合并的架构。这样更简单也更稳适合90%的填报需求。等真需要多人同时互看互改时再上协同也来得及。5.4 自定义工具栏按钮加一个提交按钮很多模板表格需要填完点提交这个按钮最好就在表格页面上。Univer的UI插件支持自定义工具栏原理是往UI插件里注册命令按钮。可以在初始化UI插件时传入配置univer.registerPlugin(UniverUIPlugin, { container, toolbar: { customizer: { onBeforeRender: (list) { const submitBtn { label: 提交数据, tooltip: 提交当前填写内容到服务器, commandId: my.submit.command, }; list.push(submitBtn); return list; }, }, }, });然后监听自定义命令univer.getCommandService().registerCommand({ id: my.submit.command, type: CommandType.COMMAND, handler: async (command) { const values sheet.getRange(1, 1, 19, 3).getValues(); // await fetch(/api/submit, ...) }, });按钮的样式和位置可以按需调核心是把业务动作注入到表格里这个模式。5.5 浏览器端性能优化如果一张表有几千上万行数据要预填渲染会慢。三个优化方向只用核心插件。没用到的公式、筛选、协同插件别注册减少渲染和内存开销。面板数据分页显示。真正展示给用户的待填写区域只是整张表的子集数据录入区外的历史数据可以做懒加载或只读引用。避免给超大区域设置单元格样式。样式本身会放大序列化后的数据体量。给表头、汇总行做样式就好不要一个5万行的表每行都设置背景色。6. 扩展玩法从填报表走向智能数据底座6.1 钉钉/企微内嵌我实际交付过的一个项目是把填报表嵌到企业微信的工作台里。Univer输出的是纯Web页面直接做iframe嵌入完全没有样式冲突问题。员工点开应用就是一张表填完提交后端汇总入Oracle库再走企业内部报表系统。这比专门开发一个H5表单页面省了大力气而且业务部门看到是Excel样式满意度极高。6.2 模板市场业务人员自己设计表Univer有一个很前卫的能力它把模板设计和数据收集拆成了两个阶段。第一阶段业务员在Univer里设计模板用我们前面说的方式设定哪些单元格可编辑第二阶段系统把这个模板发布成一张在线填报单。如果你做一个后台允许不同业务部门上传自己的模板配置就成了自家内部的表格模板市场。这比你们提需求、开发慢慢做表单的协作模式高效太多。6.3 公式链与数据资产填报表最常被忽略的价值是公式列里的业务逻辑。比如毛利率 (营收 - 成本) / 营收你在模板里把这个公式定义好用户填了分子分母系统自动算毛利率。这等于把校验和计算规则前置到了表单层后端收到的已经是净化过的合规数据。一段时间后你再去汇总这些填报结果时数据口径是统一的因为公式是同一套。我常跟团队讲填报表的价值不在那张表单而在公式和校验规则沉淀下来的数据规范。Univer让这些规则可以在表格上可视化地表达而不是写在代码判断里这个抽象层级的变化才是它真正值钱的地方。7. 写在最后这套方案的边界和下一个进阶点用Univer做可填写单元格模板我体验下来最大的感受是它把Excel的成熟交互和现代前端工程化结合得很好保护机制、命令系统、插件体系这套设计经得起真实业务打磨。但也要说清楚它不是一个开箱即用的填报表SaaS它需要你具备前端工程能力自己搭界面、自己写提交逻辑、自己把数据接回业务系统。如果你只是想快速验证这个方案适不适合我的业务建议照着我第4节的最小工程跑一遍花一晚上就能体会到核心体验。如果跑完你觉得确实比普通表单和裸调Excel库都更贴合业务那就可以往模板发布、填报权限、后端存储、数据看板这几个方向去扩展了。最后再分享一个小技巧在做模板定义的时候把保护区域配置也存一份JSON到数据库里和模板内容一起管理。不要只存单元格数据因为下次用户打开表时你需要重新执行一次保护流程。把模板数据和保护规则打包存才是完整的一张可复用填报表。