news 2026/10/2 9:53:50

用Univer开源表格引擎实现Web端指定单元格可编辑与只读控制

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
用Univer开源表格引擎实现Web端指定单元格可编辑与只读控制

从去年开始,我一直在找一个能嵌入Web项目、又足够灵活的表格方案。需求其实很简单:让业务方自己定义一张表格,给用户去填其中一部分单元格,剩下的格子全部锁死,不能碰。市面上在线表格不少,但要么太封闭,要么集成成本极高。直到我试了univer,一个开源的在线表格引擎,才算是把这件事彻底跑通了。这篇文章就聊聊univer是什么、怎么在项目里落地,以及我为了实现"指定单元格可编辑、其余只读"这个需求,踩过哪些坑、用了哪些招,希望能给同样在做表格类功能的人一点参考。

1. 为什么我会在项目里盯上univer这个开源表格引擎

1.1 从"用户自定义表格"这个需求说起

先还原一下我当时的场景。运营团队要做一个小型的数据收集页面,参与方需要填写每个人的周报内容。如果用普通表单,字段一多就非常死板,遇到临时要加一列、改一列的情况,后端接口和前端模板都要跟着动。运营同事随口说了一句"能不能就像填Excel一样,只填需要的几列,其他列不让动"——这句话让我开始关注在线表格类工具。

市面上的方案我大致盘过一轮:直接用Google Sheets嵌入,国内访问不稳定,而且权限体系跟我们的用户系统对不上;用腾讯文档或金山文档,集成自由度低,界面没法跟产品风格一致;自己用Canvas手写一个表格,工作量太大,公式、样式、合并单元格这类基础能力开发起来至少要一两个月。然后我搜到了univer,发现它是一个原生为Web打造的表格引擎,可以打包进自己的前端项目,而不是一个SaaS应用。这就意味着,我可以把它当作一个组件来用,接口逻辑自己控制,权限规则自己写,UI也能一定程度上定制。

1.2 univer的核心定位和架构优势

univer的定位其实不局限于"中国版Google Sheets",它更是一个"表格即组件"的开发平台。整个项目基于TypeScript编写,核心渲染引擎在Canvas上重绘了表格界面,交互体验接近原生桌面软件,同时又在架构上把UI层、数据层、插件层拆开了。用官方的话来说,它支持多端,但对我最实在的意义是:我可以只引入一个Sheet模块,不需要把所有功能都加载进来。

我比较看重的架构特性有三个。第一,univer的数据模型是响应式的——表格里的值、样式、公式、行列信息都对应一套可序列化的数据流,这让我们可以很自然地把表格内容存到后端,或者从后端恢复。第二,univer的插件体系非常开放——菜单栏、工具栏、右键菜单、单元格事件都能通过插件机制拦截和扩写,后面我要做的"可编辑区域控制",本质上也是靠这套机制去处理的。第三,univer默认支持公式引擎,这意味着用户在表格里填数据时可以用SUM、IF这类函数,不用我们自己在后端再造一遍计算器。

当然,它也有缺点,比如文档还在持续完善中、发布节奏比较快,刚上手时可能会觉得API变动有些频繁。但整体来说,对于想"自建表格能力"的团队,univer是现阶段我认为最合适的基础件。

2. 从零搭建一个univer表格实例

2.1 安装与依赖引入

我使用的是univer的Sheet模块,版本是当前最新的稳定版。安装非常简单,npm项目里直接跑:

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

这里需要另外说明一下,univer采用monorepo结构,核心包是@univerjs/core,但真正让表格出现在页面上,还需要引入UI层和Sheet的UI层。在我用的版本里,还额外引入了@univerjs/sheets-formula来启用公式功能,但如果你只需要填数据,不带公式,那可以不装这个包,减少体积。

如果项目是Vue或React,univer官方也提供了对应的封装包,但我这边是直接用了原生方法,逻辑完全在JS里控制,反而更透明。顺带说一句,univer要求浏览器支持Canvas和ResizeObserver,目前主流浏览器都没有问题。

2.2 创建表格并挂载到页面

初始化univer实例的代码非常直白。我把整个流程写一遍,方便第一次接触的人照抄:

import { Univer } from '@univerjs/core'; import { defaultModule } from '@univerjs/core'; import { UniverSheetsModule } from '@univerjs/sheets'; import { UniverSheetsUIModule } from '@univerjs/sheets-ui'; import { UniverUIModule } from '@univerjs/ui'; const container = document.getElementById('my-table'); const univerInstance = univer.createNewUniver({ locale: 'zhCN', modules: [ defaultModule, UniverSheetsModule, UniverSheetsUIModule, UniverUIModule, ], configurations: { ui: { container, toolbar: true, statusbar: true, formulaBar: true, }, }, });

跑起来之后,页面上会出现一个完整的表格界面,默认带一个sheet。不过这里只是"能跑",真正要做业务配置,还要通过univer自己的Command API去操作workbook和worksheet。

我印象特别深的是,univer默认生成的表格,任何单元格都可以编辑,这跟我们的需求差得远。所以光会初始化还不够,下一步就要落到"控制单元格读写"这件事上。

2.3 基础配置:工作表、字段、样式

为了满足"运营自己定义表格"的需求,我第一步是做一个后台配置页,让运营通过界面设定表头、列数、行数、列宽,然后把这些配置保存到后端。前端加载配置后,动态构建workbook。

univer创建workbook的数据结构大致是一个JSON,描述各个sheet的单元格值、样式、合并单元格、行高列宽等信息。比如一个最简单的三行两列表格,核心数据长这样:

{ "id": "sheet-1", "name": "周报表", "cellData": { "0": { "0": { "v": "姓名" }, "1": { "v": "本周完成" } }, "1": { "0": { "v": "张三" }, "1": { "v": "完成A项目" } } }, "rowCount": 3, "columnCount": 2 }

不过在实际项目中,我不会手动写这种JSON,而是通过univer的API来创建和填充。这样能保证数据格式始终与引擎内部一致,避免出现某个字段不生效的问题。样式方面,可以通过setRangeValues批量设置单元格的背景色、字体、对齐方式,运营在配置后台选的表头颜色会被映射成这些样式指令。

有了这些基础能力后,我就在后台做了一个"表格模板设计器",运营可以像画表格一样设计好表头,然后设定哪几列是可填写的。这个配置最终决定了前端渲染出来的页面长什么样,以及接下来的权限逻辑怎么写。

3. 让用户填写需要的单元格,其他格子保持只读

3.1 官方保护机制:工作表保护与单元格锁定

这是我实现"用户可以填的格子就那些,其他格子谁都别想改"需求的核心章节。univer内置了类似Excel的保护机制,支持对工作表设置保护,也支持对单个单元格锁定。两者的关系我们需要先理清楚:

  • 单元格有一个属性叫"锁定",默认是false还是true取决于引擎的设计。在Excel中,单元格默认是"锁定"的,但如果工作表未开启保护,锁定了也没用。
  • 工作表保护开启后,锁定的单元格就不允许用户编辑了。

univer的做法类似,但API表达略有不同。我们需要两步走:

第一步,把"可填写区域"的单元格锁定状态设为false,其他单元格设为true。第二步,对整张工作簿或工作表开启保护。这样,只有我们显式标记为"不锁定"的单元格可以编辑,其余全部只读。

在实际编码时,我封装了一个函数来应用"可编辑白名单区域":

import { UniverInstanceType } from '@univerjs/core'; export function setEditableRange(workbook, sheetId, editableRanges) { // 假设 workbook 为当前 univer 实例获取到的 workbook const sheet = workbook.getSheetBySheetId(sheetId); const maxRow = sheet.getRowCount(); const maxCol = sheet.getColumnCount(); // 第一步:全部单元格设置为锁定,并关闭锁定的样式效果 sheet.getRange(0, 0, maxRow, maxCol).setLocked(true); // 第二步:对可编辑区域设置为非锁定 editableRanges.forEach(({ startRow, startCol, endRow, endCol }) => { sheet.getRange(startRow, startCol, endRow, endCol).setLocked(false); }); // 第三步:开启工作簿保护 workbook.enableProtection(); }

这里有个小细节:默认单元格的样式可能没有"锁定"这层含义,所以设置locked时,同时要注意单元格的样式状态是否需要刷新。我当时第一次写完发现不生效,排查后才知道需要主动调用一次UI刷新,或者通过sheet.getRange().setLocked()会返回对应的command,我们需要把command分发到引擎里,不能直接改数据。建议你直接使用univer提供的CommandService来派发设置锁定的命令,而不是绕开数据流直接set。

我在项目里的做法是引用@univerjs/sheets的SetRangeValuesCommand,用Command的方式修改单元格的锁定属性。这样引擎才能完整地感知变化,UI也会同步更新。

3.2 通过配置实现"白名单可编辑"

有了基础锁定函数,接下来就是把它和后台配置接起来。我们的数据模型大概是:每个模板包含一个editableAreas数组,里面存的是类似[{ "sheetId": "xxx", "startRow": 1, "startCol": 1, "endRow": 10, "endCol": 2 }]这样的区域。加载模板时,把区域传入上面的setEditableRange,就可以精确控制。

这种方案最让我满意的一点是,它天然支持"非连续区域"。比如运营让用户填写B2、C2、D2三个单元格,但E列是只能看的备注,而F列是系统自动计算的结果,不允许人改。我们可以定义两个白名单区域,一次调用搞定。

另外,univer对保护开启后的交互也有细节处理:只读单元格选中后,工具栏里的"编辑""删除"等操作会被禁用或忽略;但用户仍然可以选中、复制、查看公式结果。这对于数据展示类的场景很实用——用户能自由阅读整张表,只是改不了不该改的地方。

如果你还想进一步限制用户不能选中特定区域,就要另辟蹊径了,比如监听SelectionChange事件,一旦发现选中的范围超出了白名单,就用setSelection强制拽回某个合法区域。这个逻辑我后来只在少数严格场景下用了,因为大多数用户不会故意去选只读区域,他们只是填写的角色。

3.3 动态控制权限:根据用户身份切换可编辑范围

我的项目里还有另一层需求:同一张表,不同身份的人看到的可编辑区域不同。比如填写人只能编辑自己的周报内容,而管理者可以编辑所有人,或者管理者可以修改表头。

univer的数据流是响应式的,所以我可以非常方便地在用户切换角色时重新执行setEditableRange。具体做法是,每个用户登录后,后端返回当前用户在该模板下的权限范围,前端只把对应范围传给setEditableRange,然后调用一次工作簿保护刷新。

这里有几个要注意的点:

  • 如果当前用户拥有整张表的编辑权限,就不要开启保护,或者把editableRanges设为全部单元格,否则一些用户操作(如插入行)会被误伤。
  • 如果多个协作者同时在线,需要约定:保护状态以谁为准?我的建议是把"可编辑区域"作为模板级配置,而不是某个会话级的临时配置,这样大家看到的规则是统一的。
  • 动态切换时,如果用户正在编辑的单元格突然变成了只读,可能会造成输入中断。所以最好在切换前做一次输入校验,确保没有未提交的修改。

我在实际项目中用了简单的用户角色方案:访客、填写者、管理员。访客不能编辑任何单元格;填写者能编辑白名单区域;管理员可以编辑所有区域并且可以修改模板结构。角色的变化通过applyEditablePermission()函数统一处理,这样权限逻辑只收敛在一个函数里,排查问题非常方便。

4. 实际业务场景中的细节设计与联动

4.1 用表格做收集表单:校验、提交、重置

表格并不是表单,用户填完数据后,我们得想办法把数据"取出来",同时做基本的校验。我最初的设想是监听cellChanged事件,每次编辑都同步到前端状态。但这样容易产生大量的中间状态,反而麻烦。后来改成"提交时统一读取"的模式。

univer提供了获取当前sheet数据快照的API,我写了一个getFormData函数,遍历所有可编辑区域,取出每个单元格的值。因为白名单区域是已知的,所以我不需要遍历全表,只用遍历用户有权编辑的那些区域。每个单元格的ID我用row:col来命名,形成一个扁平的对象:

const formData = {}; editableRanges.forEach(({ startRow, startCol, endRow, endCol }) => { for (let r = startRow; r <= endRow; r++) { for (let c = startCol; c <= endCol; c++) { const value = sheet.getCell(r, c).getValue(); formData[`${r}:${c}`] = value ?? ''; } } });

校验逻辑也挂在这个函数后面。比如某列要求必填,某列要求是数字,某列要求是手机号形状。遇到不满足时,我们用univer的RangeSelector把焦点定位到对应单元格,并且用样式高亮标红,然后提示用户具体哪里有问题。这个体验很像表格里的数据有效性校验,但完全由我们控制,更灵活。

这里要提醒一句:univer的单元格值拿到手后,可能包含富文本对象,并不是所有时候都是字符串。我建议统一用cell.getValue(),如果返回的是对象,就取它的text或value字段。不同版本可能略有差异,需要看对应版本的接口说明。

4.2 与后端交互:保存、回填、协作

保存数据没有太多神秘的地方。当用户点击"提交"时,前端组装好formData,POST到后端接口,后端把这份数据作为一条记录存起来。如果运营想查看多条记录,可以在后台打开一个管理页面,用同一份模板,但把数据源切换为某条记录。

回填则是相反过程:后端返回这条记录的数据,前端在初始化workbook时直接把这些值填到对应单元格里。由于univer的整个数据快照都支持序列化,所以我可以把一整份记录直接映射为新sheet的cellData。但要注意,如果后端返回的字段与模板中的单元格并不是一一对应,需要先做一次映射。

协作方面,univer官方有一些协作能力,但我没有用到。因为我们这个场景本质上是"多个用户填各自的表格副本",而不是"多个人同时编辑同一张表"。如果你确实需要多人实时协作,可以去了解univer的协作方案,但要做好存储、消息同步、冲突处理等一系列工程化准备,复杂度会上升一个量级。

4.3 扩展:利用univer的插件能力定制按钮和事件

用户填写完数据后,我们得给他一个"提交"按钮。我不想把这个按钮放在表格外面,那样会破坏填写的沉浸感。univer的UI模块允许我们往工具栏注入自定义按钮,也可以注册右键菜单项。我选择了在工具栏右侧加一个"提交"按钮。

通过univer的插件机制,我可以注册一个MenuItem,然后绑定到自己的command上。这个command执行时,会触发上面的校验、组装、提交动作。无论用户修改了哪些单元格,最终提交时都从当前表格中读取数据,不会丢失。

另一个有用的扩展点是"提交成功后的重置"。用户填完提交后,我们可能会希望清空可编辑区域的内容,保留表头和公式。我封装了一个resetEditableAreas函数,遍历白名单区域并清空值。同时用setRangeFormattedValues把样式恢复成初始状态,防止上次提交的残留样式影响下一遍输入。

这些自定义能力如果不用插件机制,而是直接改univer内部代码,会非常脆弱。所以我建议所有扩展都尽量走官方暴露的API和事件通道。univer的命令系统跟redux有点像,所有操作都要派发command,这让行为可追踪、可撤销,但也需要时间去适应。

5. 集成univer时踩过的坑与优化建议

5.1 按需加载,避免首屏体积过大

univer全家桶加下来,构建时体积可能会让你吓一跳。我第一次做包体分析,发现主包达到1.2MB以上(gzip后大约200KB+),对于一个小型产品页面来说,这个代价偏高。好在我们有条件做代码分割。

我的做法是:把univer相关的依赖单独打成chunk,通过动态import加载,只有进入有表格的页面时才下载。这样首屏就不受影响。如果你还需要进一步压缩,可以去掉暂时用不到的模块,比如不启动公式功能就不引入@univerjs/sheets-formula;不需要评论功能就不引入评论区插件。univer的模块化设计允许我们做这种裁剪。

另外,univer在初始化时会创建一些Worker来做计算,本地开发时没有明显感觉,但在生产环境要确保Worker的静态资源路径配置正确。我遇到过部署后公式一直转圈的情况,排查到最后是CDN路径没有设对。

5.2 性能优化:渲染大数据量时的取舍

表格引擎最怕的是大表格。我在测试中用univer渲染了1000行、20列的表格,初始化速度还在可接受范围,但滚动时的流畅度会随着单元格样式复杂度下降。如果你只需要展示和填写白名单区域,完全可以缩窄实际的工作表大小。

我的建议是:如果不是业务需要,把表格的行列数控制在一个合理范围内。比如默认20行、10列,而不是无限行列。univer虽然支持稀疏数据,但越大的行列区域,滚动渲染时的计算量就越高。

如果你的产品确实需要大数据量表格,那么可以把虚拟渲染开启,univer默认就有虚拟滚动能力,但对复杂合并单元格、边框样式的开销依然存在。最好做一次基准测试,确定自己的性能红线。我在项目中采用了"模板限制行数"的办法,比如最多200行,超过后提示用户优化模板,用实际数据换体验。

5.3 版本和生态:什么时候该自己维护源码

univer的迭代速度很快,我进入项目时使用的0.1.x版本,一个多月后官方就发布了0.2.x,接口变化不小。如果你在社区里搜到一些示例代码,很可能是旧版本写法,直接复制会报错。我的建议是:以官方文档和对应版本的d.ts为准,不要依赖旧文章。

另外,univer的生态插件还不像成熟商业产品那么多。官方有的UI能力,我们可以直接享用;但一些特殊交互,比如"合并单元格时自动跳过只读区域"这类功能,官方没有,只能自己写。如果确实遇到阻塞问题,可以考虑fork源码,但那是下下策。更好的方式是通过univer的issue区提需求,或者在社区里寻求协助。至少我在使用过程中,社区响应速度还算积极,常见问题能搜到答案。

还有一个小提醒:univer的许可证是Apache 2.0,这意味着你可以自由商用,但要保留版权声明。我一般会在README里注明使用了univer,并在构建打包时保留许可证文件,这样既合规,也让团队知道我们依赖了哪个版本的引擎。

5.4 若干实战心得

最后分享几个我在实际开发中总结的经验。

第一,一定要在初始化univer之前准备好模板数据结构。如果你临时修改工作表结构,比如插入行删除列,很可能把已经设置的锁定期覆盖掉。我在后台模板设计器里,是先把表单结构设计好,再生成一份"初始快照"存到后端。前端渲染时严格使用这份快照,避免中途改结构。

第二,保护功能开启后,对于用户而言,可编辑单元格的视觉提示非常重要。我把可编辑格子设置为白底、加细黑边框,只读格子设置为浅灰底,这样用户一眼就能看出来哪里能填。样式可以在模板配置时一并设定,也可以在setEditableRange里统一应用。

第三,不要忘记处理粘贴行为。即使单元格是只读的,浏览器自身的粘贴事件仍然可能把数据粘到受保护区域。我在监听beforePaste事件时做了拦截,检查粘贴目标是否在白名单内。如果不在,就取消粘贴并给出提示。这一点容易被忽略,但用户一旦发现能粘贴进去,就会认为系统有漏洞。

第四,版本升级时要格外小心。我升级到0.2.x后,之前的setLocked写法变了,需要改用新的API。在这方面,我建议关注univer的CHANGELOG和迁移指南,每次升级都在一个分支里先跑通核心用例,再合入主开发线。

现在,我团队的这块功能已经上线稳定运行了小半年。从开发到交付,univer整体给我最深的感受是:它把表格引擎的能力开放出来了,但把业务逻辑的控制权也交到了开发者手里。如果你手头正需要一套可在Web中自由定制的表格方案,并且愿意花一点时间搞清楚它的数据结构,univer是一个很值得投入的选择。至少对我来说,它把那个"用户自定义表格、指定可编辑单元格"的需求,从设想变成了可交付的现实。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/2 9:53:28

天气数据爬虫实战:requests+JSON解析从城市编码到七日预报采集

1. 项目整体设计与选型思路1.1 目标网站与数据源选择先说点实在话。做爬虫&#xff0c;最忌讳一上来就爬那种加密参数满天飞、登录墙横着走的网站。天气数据是公开信息&#xff0c;结构化程度高&#xff0c;更新频率稳定&#xff0c;而且每个城市都有自己的独立标识&#xff0c…

作者头像 李华
网站建设 2026/10/2 9:52:58

Python演唱会数据分析可视化:大作业完整实战指南

简介&#xff1a;一套完整的Python演唱会数据分析与可视化大作业源码&#xff0c;面向高校学生、课程设计者及数据分析入门者&#xff0c;解决从数据获取到业务展示的全流程实践需求。项目以演唱会数据为对象&#xff0c;先用爬虫自动化抓取网页信息&#xff0c;再用pandas完成…

作者头像 李华
网站建设 2026/10/2 9:51:45

ReportService配置要点:数据源、模板路径与热更新全解析

做后端时间久了&#xff0c;总会遇到几个需要单独花半天时间去理清配置的服务&#xff0c;ReportService就是典型的一个。它不是那种装完就能忘的组件&#xff0c;而是和业务报表强耦合、动不动就因为在某个环境里少配了一个路径、漏了一条数据库连接而翻车的服务。这篇文章就是…

作者头像 李华
网站建设 2026/10/2 9:51:44

六类城市场景移动目标联合检测数据集与实战指南

简介&#xff1a;本资源是面向智能交通、智慧物流与无障碍设施管理等垂直场景的目标检测专用数据集&#xff0c;聚焦背包、自行车、行人、行李箱、手推车、轮椅六大类生态环境目标&#xff0c;专为YOLO系列模型训练优化设计。数据集共1615张高质量JPG图像&#xff0c;配套1615个…

作者头像 李华
网站建设 2026/10/2 9:51:27

Angular老项目升级全流程与避坑指南:从AngularJS到新版迁移实践

老项目升级这事儿&#xff0c;干过的人都知道&#xff0c;表面上是换个版本号&#xff0c;实际上是把一整套技术选型、依赖关系、写法习惯全盘翻新一遍。Angular 更是其中的硬骨头&#xff1a;从 AngularJS 1.x 到 Angular 2 是推倒重来&#xff0c;之后每个大版本又都带着一堆…

作者头像 李华