ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Univer在线表格引擎:架构解析与前端集成实战

Univer在线表格引擎:架构解析与前端集成实战 上个月接手一个内部系统改造客户要求很直接浏览器里打开就能编辑表格要支持Excel导入导出还要预留多人同时编辑的能力。我们的技术栈是纯前端没有现成的Office服务。第一反应是找成熟的Web表格组件但对比了一圈要么闭源收费要么只支持展示不支持编辑协同要么社区项目维护乏力。后来同事翻到Univer这个项目我们花了一个下午跑通Demo又花了两周把它嵌入业务系统。Univer是一个开源的Web办公套件项目核心是一套基于TypeScript和Canvas渲染的电子表格引擎同时面向在线协同场景设计了插件化架构。这篇文章就围绕Univer聊清楚它凭什么能做这件事、实际集成怎么操作、哪些章节藏着坑适合正准备自建在线表格或把现有表格功能迁到Web的前端团队。1. 为什么是Univer给自研表格找一个合理的起点1.1 自研在线表格先算清这笔账想做一个像样的在线表格远不止画一个网格输入数字那么简单。公式引擎、条件格式、图表、透视表、Excel文件双向兼容、撤销重做、协同冲突处理这些模块单拎出来任何一个都能写几个月。自己从零写时间成本不现实直接套商用控件又会在授权和定制上受限。我把常见的方案粗略分成了三类第一类是SheetJSxlsx库这类解析工具它们擅长读写Excel文件但本身不做界面、不做编辑交互你要自己搭编辑器、自己处理渲染最后基本等于自研。第二类是SpreadJS这类商业控件功能确实齐全但价格不低而且源码闭源出了问题只能提工单做深度业务定制时经常感觉绑手绑脚。第三类是Luckysheet、OnlyOffice的开源分支这类社区产品。Luckysheet的渲染和交互思路可以借鉴但维护频率和工程化成熟度放到生产环境里总有点心虚。Univer属于第三类里的新选项但它解决了我最关心的几个问题前后端技术栈统一、模块化做得彻底、开源协议商业友好、底层设计就考虑了协作。更重要的是它不是一个单纯的表格控件而是一个会持续演进的办公套件框架表格只是当前最成熟的模块之一。1.2 它和纯表格组件的本质区别大多数Web表格组件解决的是“怎么把表格数据展示在浏览器里”Univer解决的是“怎么在浏览器里复刻桌面Office的编辑体验”。这意味着它必须处理键盘输入、选区操作、公式计算、剪贴板、右键菜单、拖拽填充这些交互细节而不是简单地把HTML表格铺出来。Univer的项目结构也说明了这一点。核心包只负责数据结构、插件机制、命令系统这些中枢逻辑UI层分开表格能力单独作为Plugin注册渲染层基于Canvas独立实现。这种拆法对业务集成有实际意义你不需要的模块可以不装需要扩展的业务规则可以写成自己的插件而不是去改别人的源码。另外它的技术选型很对工程口味TypeScript全栈、依赖注入的IoC容器、命令模式统一操作入口、类似游戏引擎的Canvas绘制方案。整套设计让二次开发的门槛没有想象中那么高。2. 从插件到协同Univer的架构到底解构了什么2.1 一切操作都是命令这是协同的基础接触Univer源代码之后你会注意到一个设计习惯用户在界面上做的每个动作最终都会转化为一条条Command。插入行、设置单元格值、修改样式、合并单元格都有对应的命令对象。我最初不理解为什么要把简单操作搞得这么重后来想明白了一个道理命令模式天然适合协同和撤销。每条命令可以记录执行前后的状态差异可以进撤销栈可以在广播给其他客户端时被序列化传送。也就是说协作场景下你广播的不再是“整个Excel文件”而是“谁在哪个位置改了什么东西”。这个粒度比同步整份数据高效得多冲突处理也相对容易。这也引出了第二个好处命令是可以通过API调用的。业务系统想在某个按钮点击后往表格写入数据本质上就是触发一堆命令。第三方插件也可以监听命令流转在特定动作前后插入自己的逻辑。这种统一入口的设计是Univer适合二次开发的关键。2.2 数据模型、渲染层、公式引擎彼此独立Univer的表体在Canvas上绘制而不是DOM。第一次跑起来的时候我在浏览器调试器里看到整个工作表都是一块画布拖拽和框选都非常顺滑。用Canvas做表体渲染的原因很实在一个工作表动辄上万行如果用DOM去渲染单元格节点浏览器根本扛不住滚动和状态刷新。Canvas是把自己的绘制逻辑掌握在开发者手中可以精确控制哪些区域需要重绘配合视口按需渲染这是桌面级编辑体验的基础。数据模型和渲染层是分开的。工作簿的数据结构维护在核心模块里渲染层只负责把数据画出来。业务代码读写数据时不需要关心当前视图显示的是哪一块直接操作模型即可。公式则被独立成公式引擎有自己的依赖计算链单元格值变化时只触发受影响的公式重算而不是全表扫描一遍。这种解耦在协同场景下尤其重要。远端来了一条更新命令先落到数据模型再触发局部视图重绘。如果你的业务逻辑要读取计算后的值也可以不依赖UI直接面向数据层取数。2.3 协同方案不是可选项而是架构内置的方向Univer最初设计的目标就是在线协同办公所以协作相关能力不是后续打补丁塞进去的而是从一开始就融入了命令系统和数据层。它的思路是所有修改先在自己的数据副本上应用然后生成操作说明通过协同网络同步给其他人另一端收到操作后在自己的副本上应用同样的操作并通过CRDT或类似并发控制技术处理同时编辑的冲突。对做集成的开发者来说这意味着你不需要从零设计协同协议。你要做的更多是接入传输层、持久化层和房间管理——比如把操作流通过WebSocket推给其他人把工作簿快照存进数据库把历史版本管理起来。Univer提供的是协同的基础逻辑但实际的在线运行环境仍然需要自己搭这一点后面我会专门展开说。3. 30分钟集成把电子表格嵌进现有前端工程3.1 最小依赖跑起来先说明Univer的版本迭代速度比较快下面的依赖包名和API以你当前拉到的版本为准但总体思路不变。官方推荐的做法是按需安装核心包和表格相关插件。npm install univerjs/core univerjs/sheets univerjs/sheets-ui univerjs/ui univerjs/design然后写一个最简单的启动入口。先准备一个容器节点注意容器必须显式设置宽高否则Canvas区域初始化后会变成0x0看起来就像表格没加载出来。import { Univer } from univerjs/core; import { UniverUiPlugin } from univerjs/ui; import { UniverSheetsPlugin } from univerjs/sheets; import { UniverSheetsUIPlugin } from univerjs/sheets-ui; import { defaultTheme } from univerjs/design; const univer new Univer(); univer.registerPlugin(UniverUiPlugin, { container: app, theme: defaultTheme, }); univer.registerPlugin(UniverSheetsPlugin); univer.registerPlugin(UniverSheetsUIPlugin);这段代码的作用是初始化一个Univer实例注册UI基础能力、表格数据能力和表格交互界面。跑起来之后你会在容器里看到一个完整的表格编辑界面包括工具栏、单元格画布、底部Sheet标签和公式栏。如果只想用数据层、不想要UI可以只注册UniverSheetsPlugin然后通过API直接创建和工作簿交互。很多后台数据处理场景其实用不到编辑器界面这种灵活拆分非常实用。3.2 通过Facade API读写工作簿Univer提供了一层给开发者使用的门面API叫Facade。它的设计目的是隐藏内部复杂的依赖注入和插件调用细节让你像操作普通对象一样操作工作簿。import { FUniver } from univerjs/facade; const univerAPI FUniver.newAPI(univer); const workbook univerAPI.getActiveWorkbook()!; const sheet workbook.getActiveSheet()!; await sheet.getRange(0, 0, 5, 3).setValue([ [编号, 日期, 数量], [A-001, 2025-01-01, 12], [A-002, 2025-01-02, 8], [A-003, 2025-01-03, 15], ]);注意getRange的前四个参数是行、列、行数、列数全部从0开始。这个方法拿到一个区域对象后可以链式调用设置值、设置样式、读取值等操作。一次写入的数据会以命令方式进入数据层自动进入撤销栈。读取数据同样走区域对象const value await sheet.getRange(0, 0, 5, 3).getValue(); console.log(value);这里返回的是一个二维数组结构和你写入时一致。因为Facade方法大多返回Promise在业务代码里要处理好异步时序。这是我在实际集成中踩过的一个小坑拿到Workbook后立刻getActiveSheet可能取不到因为初始化是异步完成的建议等首次渲染完成或加一个简短的重试机制。3.3 Excel文件导入导出Univer默认支持xlsx格式的导入导出。导入的思路是前端读取文件为ArrayBuffer然后交给Univer的导入API它会解析文件内容并生成工作簿数据替代当前内存里的工作簿。导出的方向类似。把当前工作簿序列化后导出为xlsx文件下载。要注意的是导入导出对Excel高级特性的支持是有损的。常规的数据、公式、样式、合并单元格问题不大但某些透视表、复杂图表、宏、条件格式的特殊规则可能无法做到100%还原。如果你的业务流程严重依赖这些特性务必在集成前做一次完整兼容性测试拿真实业务Excel文件挨个试。我的建议是在导入导出前后各做一次数据校验确认关键字段没有被解析成错误类型。比如日期在Excel内部是数字序列处理不当会显示成乱码或偏移一天。4. 业务化改造自定义工具栏、公式与操作权限4.1 给编辑器加一个自己的按钮集成了基础表格之后下一步通常是加业务按钮。比如一个“从接口刷新数据”的按钮或者“提交当前表格”的按钮。Univer的UI是可定制的工具栏上可以注册新的按钮项。实现思路是写一个自定义插件在插件初始化阶段向UI层添加按钮并指定按钮点击后执行的回调。伪代码大概是这样的class MyBusinessPlugin extends UniverPlugin { onMounted(): void { this.addToolbarButton({ label: 提交数据, onClick: () { this.exportAndSubmit(); }, }); } async exportAndSubmit() { // 读取全部单元格数据调用业务接口 } } univer.registerPlugin(MyBusinessPlugin);这里我不贴死某一版本的API因为工具栏注册方式在不同版本里有过调整。实际开发时去查对应版本的示例代码即可重点是理解这种扩展模式你的业务按钮本质上是插件的一部分它拥有访问Univer实例和操作工作表的能力。4.2 自定义公式把计算规则写进单元格产品经理通常会要求“在单元格里直接写公式”。Univer支持注册自定义公式函数注册后用户就能像使用SUM一样使用你的函数。自定义公式要处理两件事一是定义计算逻辑二是声明参数规则。比如做一个“准时率”函数它会读取某段区域内按时完成的数量再返回占比。registerFunction(ON_TIME_RATE, { description: 计算准时率, parameters: [ { name: total, type: number, required: true }, { name: onTime, type: number, required: true }, ], calculate: (total, onTime) { if (total 0) return 0; return onTime / total; }, });参数类型可能是单元格引用、区间、常量或者布尔值计算公式引擎会先解析这些引用把对应单元格的值取出来再传给计算函数。这里有个容易踩坑的地方如果你希望公式在某个单元格变化时自动重算就要确保依赖关系被正确声明否则引擎可能不会触发重算。4.3 操作权限不能只靠前端隐藏UI在线表格类系统的权限控制往往是安全审查的重点。你可以在界面上禁用某些用户的编辑按钮但这只是体验层面的保护不能当作真正的权限边界。敏感数据场景下后端必须在每条写命令到达时做二次校验不能信任前端传来的任何指令。实操上可以这样设计前端根据用户身份动态生成可用的工具栏按钮列表只读用户不显示编辑按钮同时命令层做一个本地拦截检测到写操作时先向后端询问权限。后端则维护一份“工作簿-用户-操作类型”的权限矩阵收到写命令时解析命令内容判断该用户是否允许修改指定单元格区域。之所以要坚持后端校验是因为命令是可以伪造的。前端隐藏按钮防君子不防小人合规审计需要的是服务端留存操作记录和权限判定结果。5. Univer在线的两种打开方式平台体验与私有化部署5.1 先分清“能在网页上编辑”和“协同在线”是两回事搜索热词里的“univer在线”我理解有至少两层含义。第一层指的是Univer本身是纯Web技术栈打开浏览器就能编辑不需要安装桌面软件。这一层你通过前面章节的集成代码就能实现。第二层指的是类似Google Sheets那种多人实时协同的在线办公体验。这层能力需要额外的服务端支撑WebSocket推送、工作簿持久化、版本管理、房间管理、权限校验。Univer提供了协同编辑的客户端基础但完整的“在线部署”还需要你自己构建服务后端。如果只是想快速体验产品可以访问Univer官网的在线工作台注册账号后直接在浏览器里创建和编辑表格如果要落地到业务系统则是私有化部署的活。5.2 最小化的协同后端架构我给一个可落地的最小协作后端设计不一定复杂但足够支撑中小团队使用一是客户端启动时创建或加入一个协同房间房间里包含一份共享的工作簿状态。二是用户每次操作生成一条命令客户端先把命令应用到本地再把命令推送到后端。三是后端异步把命令转发给房间内其他在线客户端同时把命令写入命令日志表。四是对端客户端收命令后应用到自己本地并重绘视图。五是定期或手动保存全量快照到数据库作为恢复和审计基线。存储选型上命令日志可以用普通关系型数据库快照可以存对象存储或文档数据库实时推送用自带WebSocket的服务端即可。不需要一开始就上很重的协同计算框架做好命令顺序编号和重复幂等处理就能撑起几十人同时编辑一个工作簿的场景。5.3 在线部署的权限与身份打通在线版和单机嵌入版最大的差异在于用户体系。嵌入版本通常跟母系统共享登录态而在线版本需要自己管理会话。建议在接入层统一签发短期令牌后端会话服务校验令牌后返回协同连接凭证。连接凭证里至少包含用户ID、用户昵称、所属租户、可编辑的工作簿列表。协同连接建立后前端每次发送命令都会附带令牌服务端解析命令目标并比对权限。这种设计把身份认证和操作授权分开后续接入OAuth、企业微信、飞书或者自建账号体系都比较好改。还要注意操作审计。在线办公场景里“谁改了什么”经常是需要追查的合规需求。每次命令日志都落库配合全量快照可以做到从任意时间点恢复工作簿。这个能力在自建系统里非常加分也是很多商业表格产品收费的功能点。6. 上线前绕不开的六类实际问题6.1 大数据量场景下不能无脑写入Univer的Canvas渲染能承载很大的数据量但不意味着你可以用低效方式写数据。我见过有人在循环里一行一行地setValue结果几万行数据写完页面卡了好几分钟。正确做法是批量写入一次调用setValue放入一个二维数组或者把需要修改的区域一次性构建好再提交。公式多的表格写入后计算引擎要重算依赖链数据量过大时仍然会有明显卡顿。这时候可以把公式计算模式切换成手动等数据全部写入后再触发一次全量计算。6.2 中文字体和样式的小细节Canvas绘制文字不依赖DOM字体加载未完成时可能只显示空白或方块。上线前务必检查字体资源的加载策略确保业务常用的中文字体在入口阶段就已经可用或者在字体加载完成后主动触发一次视图重绘。导入Excel文件时中文字体和行高列宽的单位换算也要实测。特别是列宽Excel内部有一套基于字符宽度的计算方式和Canvas像素并非一一对应。不同版本之间可能存在细微偏差建议拿几份真实业务文件测试对比。6.3 资源包体积需要管理Univer功能完整代价是代码体积明显大于普通前端组件。首次加载时核心包加UI再加表格插件打包出来后动辄一两百KB甚至更多这在内部系统可以接受但如果面向公网用户还是要做构建优化。实际操作建议能按需注册插件就按需注册不使用不加载UI层和核心层拆分路由级代码块关闭无用的语言包有条件的用CDN长期缓存静态资源。打包工具链上可以调大分包阈值避免所有代码都挤进一个chunk导致首屏白屏时间过长。6.4 版本升级节奏比较快Univer还处在快速迭代阶段API变化的频率比成熟开源项目高。这意味着你在网上搜到的某个版本写法可能半年后就不适用了。我们项目里的做法是把版本号精确记录升级前逐一读迁移文档并且给关键流程写自动化冒烟测试。另外不要在插件里依赖核心模块的私有对象尽量走公开API。私有变量的改动不受语义化版本保护一旦升级经常直接编译报错。保持插件和核心的边界清晰后续升级能省掉很多排查时间。6.5 协同连接断开后的处理策略在线编辑最怕的是一方网络抖动另一方还在专注编辑。要让体验足够好前端要处理连接断开状态本地编辑可以先正常写入并保存到本地缓存连接恢复后再追补命令。这里的关键是命令序号。每条命令在后端按顺序排号重连后客户端拉取自己缺失的命令区间重新执行。只要命令设计得足够小且幂等这个追补过程对用户基本无感。千万不要在断线时直接锁定整个表格这种粗暴方案会影响所有用户的体验。6.6 不是每个场景都需要协同最后泼一点冷水。如果你的业务场景是“单人编辑、多人只读查看”其实不需要那么重的协同后端。单机嵌入模式加一个定时快照到业务后台成本低得多稳定性也高得多。我们把Univer接入内部系统时第一阶段只做了单机嵌入加Excel导入导出第二阶段才逐步加入协同、权限和审计能力。先把一个跑得稳的基础版本交付出去再根据实际使用反馈决定要不要上协同这个节奏在实际项目中是最务实的。还有一个小技巧接入正式项目前先花半天时间把Univer官方仓库的Demo跑熟尤其是协作相关例子。不要直接照着旧博客的代码抄因为版本演进的坑官方文档和更新日志永远是最新的等你把初版跑通之后再回头看这些经验总结吸收效率会高很多。
返回列表