首页 / 资讯中心 / 文章详情

Univer 实战:Canvas 渲染引擎与插件架构下的电子表格开发

Univer 实战:Canvas 渲染引擎与插件架构下的电子表格开发 ★ FEATURED ARTICLE
电子表格这个东西看起来简单做起来要命。我最早接触这类需求是在做一个内部数据看板当时想的是不就是个表格嘛用个开源库渲染一下就行结果真正上手才发现单元格合并、公式计算、协同编辑、大数据量渲染每一个都是深坑。后来接触到 Univer才算找到了一套相对完整的解法。Univer 是一套开源的表格与文档协同编辑解决方案核心能力覆盖电子表格、文档、幻灯片三大场景底层用 Canvas 做渲染引擎上层用插件架构组织功能模块同时提供了完整的 SDK 供 Node.js 和浏览器端调用。它适合谁如果你正在做在线表格、协同文档、数据填报系统或者单纯想研究一个成熟 Canvas 渲染引擎是怎么设计的Univer 都值得花时间啃一啃。这篇文章我会从架构设计、渲染机制、插件体系、SDK 集成、性能优化几个角度把我在实际项目里踩过的坑和总结的经验完整分享出来。1. 为什么电子表格比你想的难做1.1 一个单元格背后藏着多少东西很多人对电子表格的认知停留在行列网格这个层面觉得无非就是画线、填字、支持编辑。但真正拆开来看一个单元格承载的信息量远超想象。它需要处理数据类型文本、数字、日期、布尔、公式、错误值、样式字体、颜色、边框、对齐、换行、旋转、格式货币、百分比、科学计数、自定义格式串、状态选中、编辑中、只读、禁用、错误高亮、以及交互行为点击、双击、拖拽填充、右键菜单、键盘导航。这还只是一个单元格。当你有十万个单元格的时候问题就从功能实现变成了渲染性能和内存管理。我见过太多项目用 DOM 表格硬扛几千行就开始卡顿因为每个单元格都是一个 DOM 节点浏览器的布局和重绘开销直接爆炸。Univer 选择 Canvas 作为渲染底座本质上就是为了绕开 DOM 的性能天花板。Canvas 的好处是所有绘制指令直接作用于像素缓冲区没有 DOM 树的结构开销也没有 CSS 样式计算和布局回流。但代价是你需要自己实现命中检测、滚动、选区、文本排版、光标闪烁这些浏览器原生帮你搞定的事情。这就是为什么我说电子表格难做——你不是在做一个表格你是在做一个微型操作系统。1.2 Univer 的定位与它解决的问题Univer 不是第一个做在线表格的开源项目但它有几个设计决策让我觉得值得深入研究。第一它把渲染层和逻辑层做了彻底分离渲染层只负责画逻辑层负责算什么两者通过命令和事件通信。第二它采用插件架构核心包非常薄公式引擎、条件格式、数据验证、协同这些能力都是独立插件按需加载。第三它同时支持浏览器和 Node.js 环境这意味着你可以在服务端做公式计算、导入导出、批量数据处理而不必把压力全放在客户端。这三点加起来解决的是一个很实际的问题企业级表格应用需要可扩展、可定制、可服务端处理。你不可能用一个黑盒组件去满足所有业务需求总会有我们要加一个自定义函数我们要在单元格里嵌入图表我们要做权限级别的单元格锁定这类需求。Univer 的插件架构就是为这种场景准备的。提示如果你只是想要一个简单的表格展示不涉及编辑和协同其实没必要上 Univer直接用轻量级的虚拟滚动表格库就够了。Univer 的价值在于复杂场景。2. Canvas 渲染引擎的核心设计2.1 分层渲染与脏矩形机制Univer 的渲染引擎不是每帧重画整个表格那样性能会非常糟糕。它采用的是分层加脏矩形的策略。所谓分层是把表格拆成背景层、网格线层、内容层、选区层、悬浮层等多个 Canvas 层叠在一起。背景和网格线变化频率低内容层变化频率中等选区和悬浮层变化频率最高。分层之后当用户只是移动鼠标产生悬浮效果时只需要重绘悬浮层其他层保持不动。脏矩形机制则是进一步优化。当某个单元格内容变化时引擎会计算出受影响的最小矩形区域只重绘这个区域内的像素。这个计算过程需要精确知道每个单元格的边界坐标以及哪些层在这个区域内。我实测下来在万级单元格的场景下脏矩形能把单次重绘的耗时从几十毫秒压到几毫秒以内。但这里有个坑要注意脏矩形的合并策略很关键。如果用户连续快速输入每次输入都产生一个脏矩形如果不去重合并重绘次数会非常多。Univer 内部有一个调度器会把同一帧内的多个脏矩形合并成一个大矩形然后统一重绘。这个机制在快速输入和批量粘贴时效果特别明显。2.2 文本排版与字体度量的难点Canvas 渲染文本和 DOM 渲染文本最大的区别在于Canvas 不会自动帮你处理换行、省略、对齐。你需要自己测量文本宽度自己决定在哪里断行自己计算垂直居中位置。Univer 在这方面做了不少工作它实现了一套文本排版引擎支持自动换行、强制换行、溢出省略、旋转文本等。字体度量是这里最容易被低估的难点。不同字体、不同字号、不同字重下同一个字符的宽度是不一样的。而且中英文混排时还需要处理全角和半角的差异。Univer 的做法是维护一个字体度量缓存把测量过的字符宽度缓存起来避免重复调用 Canvas 的 measureText 接口。measureText 这个接口在频繁调用时性能并不好缓存能显著降低开销。还有一个实际问题是字体加载。如果页面使用的自定义字体还没加载完Canvas 测量出来的宽度是错的会导致排版错乱。我的经验是在字体加载完成之前先用系统默认字体渲染等 document.fonts.ready 之后再触发一次全量重排。Univer 的渲染层有对应的字体就绪回调机制你可以挂上去做重排。2.3 滚动与虚拟化的配合Canvas 本身不提供滚动能力你需要自己实现滚动条和滚动逻辑。Univer 的做法是维护一个视口模型记录当前滚动位置和可视区域范围然后只渲染可视区域内的单元格。这就是虚拟化。虚拟化的关键是要快速定位到可视区域的起始行列索引这需要维护行高和列宽的索引结构。如果所有行高列宽都是固定的那索引计算就是简单的除法。但电子表格允许用户拖拽调整行高列宽甚至支持自动行高根据内容撑开这就让索引计算变复杂了。Univer 维护了一个前缀和数组记录每一行之前所有行的累计高度通过二分查找定位起始行。这个结构在行高变化时需要更新更新的复杂度是 O(n)但实际使用中行高变化不频繁所以可以接受。滚动还有一个细节是惯性滚动和滚动边界。在触控板上快速滑动时需要模拟惯性衰减否则体验会很生硬。Univer 的滚动模块支持配置惯性参数我一般会把摩擦系数调到 0.95 左右感觉比较接近原生体验。3. 插件架构能力解耦的关键3.1 插件注册与生命周期Univer 的插件体系是整个框架的骨架。每个插件都是一个独立模块通过统一的接口注册到核心容器中。插件可以声明自己依赖哪些其他插件容器会负责按依赖顺序初始化。这个设计让功能扩展变得非常干净——你想加一个单元格批注功能就写一个批注插件注册进去不需要改动核心代码。插件的生命周期大致分为注册、初始化、启动、销毁几个阶段。注册阶段只是把插件类登记到容器里初始化阶段会注入依赖和服务启动阶段才会真正挂载事件监听、注册命令、初始化状态。这个分阶段的设计很重要因为它允许插件在初始化时访问其他插件提供的服务而不必担心顺序问题。我在实际项目里写过几个自定义插件最大的体会是插件之间的通信一定要通过命令总线或者服务接口不要直接互相引用。直接引用会导致耦合后期想替换或移除某个插件时非常痛苦。Univer 的命令系统就是为此设计的所有操作都封装成命令插件通过派发命令来触发行为通过监听命令来响应变化。3.2 命令系统与撤销重做命令系统是 Univer 里我觉得设计得最巧妙的部分之一。每一个会改变文档状态的操作都必须封装成一个命令。命令包含执行逻辑和撤销逻辑执行时把操作压入历史栈撤销时反向执行。这样撤销重做就是天然支持的不需要每个功能单独去实现。命令的粒度控制是个经验活。粒度太粗撤销时会一次性回退太多操作用户体验差粒度太细历史栈会膨胀得很快内存占用高。我的做法是把一次用户意图作为一个命令。比如用户粘贴一片区域这是一个命令用户拖拽填充这是一个命令用户连续输入文字每次输入算一个命令但如果在短时间内连续输入可以合并成一个命令。Univer 的命令系统支持合并策略你可以配置合并窗口。还有一个容易忽略的点是命令的幂等性和可重放性。在协同场景下命令需要被序列化传输到其他客户端重放。如果命令依赖了本地状态重放时可能会出错。所以命令的参数必须是自包含的不能引用外部可变状态。这一点在写自定义命令时要特别注意。3.3 服务注入与依赖管理Univer 的依赖注入机制让插件可以方便地获取其他插件提供的服务。比如公式插件需要访问单元格数据服务它不需要知道数据服务是谁实现的只需要声明我需要一个 IDataService容器会负责注入。这种面向接口的编程方式让插件的可替换性大大增强。但依赖注入也有它的代价。过度使用会让代码的调用链路变得不直观你看到一个方法调用不知道它最终执行的是哪个实现。我的建议是核心服务用依赖注入业务逻辑内部的方法调用还是直接调用不要为了解耦而解耦。Univer 本身也是这么做的它的核心服务走注入插件内部的辅助函数就是普通函数。4. SDK 集成从浏览器到 Node.js4.1 浏览器端初始化的完整流程在浏览器端使用 Univer大致流程是引入核心包和需要的插件包创建一个 Univer 实例配置渲染容器和初始数据注册插件然后启动。听起来简单但实际配置时有几个地方容易出错。首先是容器元素的尺寸。Univer 需要容器有明确的宽高如果容器高度是 0 或者 auto渲染会出问题。我一般会给容器设置一个固定的高度或者用 flex 布局让它撑满父容器。其次是 DPR设备像素比的处理在高分屏上如果不做适配渲染出来的文字会模糊。Univer 的渲染层会自动读取 devicePixelRatio 并调整 Canvas 的实际像素尺寸但你需要确保 CSS 尺寸和逻辑尺寸一致。再就是初始数据的格式。Univer 使用自己的工作簿数据结构如果你从后端拿到的是 Excel 文件需要先用导入模块解析成 Univer 的数据格式。这个转换过程在大文件时可能比较耗时建议放在 Web Worker 里做避免阻塞主线程。import { 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; const univer new Univer({ theme: defaultTheme, locale: zhCN, }); univer.registerPlugin(UniverRenderEnginePlugin); univer.registerPlugin(UniverFormulaEnginePlugin); univer.registerPlugin(UniverUIPlugin, { container: document.getElementById(app), }); univer.registerPlugin(UniverSheetsPlugin); univer.createUnit(UniverInstanceType.UNIVER_SHEET, { id: sheet-001, name: 数据表, sheetOrder: [sheet-1], sheets: { sheet-1: { id: sheet-1, name: Sheet1, rowCount: 1000, columnCount: 26, cellData: { 0: { 0: { v: 姓名 }, 1: { v: 年龄 } }, 1: { 0: { v: 张三 }, 1: { v: 28 } }, }, }, }, });这段代码是最小可运行示例。注意插件注册的顺序渲染引擎和公式引擎要在 UI 插件之前注册因为 UI 插件依赖它们。Sheets 插件要在 UI 插件之后因为它需要 UI 提供的容器。4.2 Node.js 环境下的服务端计算Univer 支持在 Node.js 里跑这一点对做数据处理的场景非常有用。你可以把公式计算、数据校验、导入导出这些重活放到服务端客户端只负责展示和交互。服务端使用 Univer 时不需要注册渲染相关的插件只需要核心包、公式引擎和数据模型。Node.js 环境下最大的坑是缺少浏览器 API。Univer 的某些模块可能间接依赖了 window、document 这些对象在 Node.js 里会报错。解决办法是在入口处做 polyfill或者只引入不依赖 DOM 的模块。我一般会先跑一个最小测试确认哪些模块能在 Node.js 里正常工作再逐步加功能。服务端计算的典型场景是批量公式求值。比如用户上传了一个几万行的表格里面有大量 VLOOKUP 和 SUMIF如果在客户端算浏览器会卡死。放到服务端用 Univer 的公式引擎算完再返回结果体验就好很多。实测下来一万个公式单元格在 Node.js 里计算大概需要几百毫秒具体取决于公式复杂度。const { Univer, UniverInstanceType } require(univerjs/core); const { UniverFormulaEnginePlugin } require(univerjs/engine-formula); const { UniverSheetsPlugin } require(univerjs/sheets); async function computeSheet(data) { const univer new Univer({ locale: zhCN }); univer.registerPlugin(UniverFormulaEnginePlugin); univer.registerPlugin(UniverSheetsPlugin); const workbook univer.createUnit(UniverInstanceType.UNIVER_SHEET, data); const sheet workbook.getActiveSheet(); // 触发公式计算 const formulaEngine univer.__getInjector().get(IFormulaService); await formulaEngine.calculate(); return sheet.getSnapshot(); }注意Node.js 端使用 Univer 时版本要和前端保持一致否则数据结构可能不兼容。我在项目里吃过这个亏前端用的 0.1.x服务端装了 0.2.x快照格式对不上排查了半天。4.3 导入导出与格式兼容Univer 支持 Excel 文件的导入导出但需要额外的插件包。导入时它会解析 xlsx 文件的 XML 结构转换成 Univer 的内部数据模型。导出时反过来。这个过程涉及大量细节比如合并单元格、条件格式、数据验证、图表、图片不是所有 Excel 特性都能完美还原。我的经验是导入导出功能要尽早测试用真实的业务文件去测不要用自己造的简单样例。真实文件里往往有各种奇怪的格式比如隐藏行列、自定义数字格式、跨表引用、数组公式这些才是考验兼容性的地方。遇到不支持的格式要么在导入时做降级处理要么在导出时给出提示不要让用户以为数据丢了。还有一个性能问题大文件导入。一个几万行的 xlsx 文件解析加转换可能要好几秒。我的做法是分片处理先解析出工作表列表和基本信息让用户选择要导入哪个表再按需解析具体数据。这样用户不用等整个文件解析完就能看到界面。5. 协同编辑的底层逻辑5.1 操作变换与冲突解决协同编辑的核心难题是冲突解决。两个人同时修改同一个单元格谁赢如果只是简单的后写覆盖那先写的人的操作就丢了。Univer 采用的是 OT操作变换思路每个操作在应用到本地之前会先和远程操作做变换保证最终一致性。具体来说当本地产生一个操作时它先应用到本地文档同时发送到服务端。服务端收到后广播给其他客户端。其他客户端收到远程操作时需要把这个操作和本地尚未确认的操作做变换然后应用变换后的结果。这个变换逻辑是协同正确性的关键写错了会导致数据不一致。Univer 的协同模块把这块封装好了你只需要接入它的协同服务端或者按照它的协议自己实现服务端。它的协议是基于 WebSocket 的消息格式是自定义的二进制或 JSON。我在实际项目里用的是它的开源协同服务端部署起来不算复杂但要注意网络断线重连和操作队列的持久化。5.2 光标同步与在线状态协同编辑不只是数据同步还有用户感知层面的同步。你需要让用户看到别人在哪里、在编辑什么。Univer 支持光标位置同步和选区同步远程用户的光标会以不同颜色显示在表格上。这个功能看起来简单实现起来要考虑不少细节。比如光标位置的表示。你不能只存一个行列号因为如果别人插入了行行列号会偏移。正确的做法是存一个相对位置或者带版本号的绝对位置在应用前做变换。Univer 的光标同步是基于操作变换后的位置来计算的这样插入删除行之后光标还能跟着走。在线状态的管理也需要考虑。用户断线后他的光标应该在一段时间后消失但不能立即消失因为可能是网络抖动。我一般设置 30 秒的超时超过 30 秒没收到心跳就认为离线。同时用户重新连接后要能恢复到之前的状态包括光标位置和未提交的编辑。5.3 离线编辑与重连策略网络不可能永远稳定用户可能在电梯里、地铁上编辑表格。离线编辑能力就很重要。Univer 的协同模块支持离线操作队列断网时的操作先存在本地联网后按顺序发送。但这里有个问题离线期间远程可能已经发生了很多变化你的本地操作需要和这些变化做变换变换的结果可能和你预期的不一样。我的建议是离线编辑要限制范围。可以允许用户查看和编辑但在重连时要给用户一个提示告诉他哪些操作被合并了、哪些被覆盖了。不要静默处理否则用户会发现数据莫名其妙变了信任度会下降。重连策略上我采用的是指数退避加随机抖动。第一次断线后 1 秒重连失败后 2 秒再失败 4 秒以此类推最大间隔 30 秒。加随机抖动是为了避免大量客户端同时重连把服务端打垮。这个策略在实测中比较稳既能快速恢复又不会造成惊群。6. 性能优化的实战经验6.1 大数据量下的渲染调优十万行、一百列的表格如果全量渲染再强的机器也扛不住。Univer 的虚拟化能解决大部分问题但还有一些细节需要调。首先是行高列宽的缓存如果每次渲染都去计算行高开销会很大。Univer 会缓存行高列宽只有在明确变化时才重新计算。你要确保自己的自定义插件不要去频繁触发行高重算。其次是单元格样式的复用。如果每个单元格都存一份完整的样式对象内存占用会非常高。Univer 内部有样式池的概念相同的样式只存一份单元格存样式 ID。你在写自定义渲染逻辑时也要注意复用样式对象不要每次创建新对象。再就是渲染帧率控制。不是每次数据变化都要立即重绘可以合并到下一帧统一处理。Univer 的渲染调度器支持配置帧率上限我一般会限制在 60fps超过这个频率人眼也感知不到反而浪费性能。在低端设备上可以降到 30fps换取更流畅的交互响应。6.2 公式计算的性能陷阱公式计算是电子表格里最耗性能的部分。一个 VLOOKUP 可能还好但如果有一万个 VLOOKUP每个都扫描几千行那就是几千万次比较。Univer 的公式引擎做了依赖图优化只重算受影响的单元格而不是全量重算。但依赖图的维护本身也有开销如果依赖关系频繁变化性能会下降。我的经验是尽量避免在公式里做全列引用比如 SUM(A:A)。全列引用会让依赖图变得巨大而且每次插入行都要更新依赖。用具体的范围比如 SUM(A1:A1000)性能会好很多。另外易失性函数比如 NOW、RAND要少用它们会导致每次重算都触发依赖链上的所有单元格重算。还有一个技巧是把复杂公式拆成辅助列。一个嵌套了七八层的 IF 公式拆成几个简单的中间列不仅可读性好计算性能也往往更好因为中间结果可以被缓存和复用。6.3 内存占用与垃圾回收长时间运行的表格应用内存管理是个大问题。我遇到过页面开了几个小时之后越来越卡的情况最后发现是历史栈和缓存没有清理。Univer 的历史栈默认会保留一定数量的操作超过之后会丢弃最老的。你可以配置这个数量我一般设成 100既能满足大部分撤销需求又不会占用太多内存。缓存也要注意失效策略。字体度量缓存、公式结果缓存、渲染缓存这些如果只增不减内存会一直涨。Univer 的缓存大多有 LRU 淘汰策略但你要确保缓存的大小上限配置合理。太小会导致频繁重算太大则占用内存。我一般会根据表格规模动态调整小表格给大缓存大表格给小缓存。垃圾回收的触发时机也值得关注。在批量操作时会产生大量临时对象如果频繁触发 GC会造成卡顿。我的做法是在批量操作期间暂停一些非必要的缓存更新等操作结束后统一处理减少 GC 压力。7. 自定义扩展的实操路径7.1 写一个自定义函数插件Univer 的公式引擎支持自定义函数。假设我们要加一个函数叫 DOUBLE把输入值乘以 2。你需要定义一个函数描述对象包含函数名、参数定义、计算逻辑然后注册到公式引擎。import { IFunctionInfo, FunctionType } from univerjs/engine-formula; const DOUBLE: IFunctionInfo { functionName: DOUBLE, functionType: FunctionType.Math, description: 将输入值乘以2, abstract: DOUBLE(value), parameters: [ { name: value, detail: 要翻倍的值, type: number }, ], calculate: (value) { if (typeof value ! number) return 0; return value * 2; }, }; // 注册 formulaEngine.registerFunction(DOUBLE);注册之后用户在单元格里输入 DOUBLE(5) 就能得到 10。自定义函数的难点不在注册而在于处理各种边界情况参数是数组怎么办参数是错误值怎么办参数是空单元格怎么办这些都要在 calculate 里处理。我的建议是参考内置函数的实现把边界情况都覆盖到。7.2 自定义渲染与单元格类型Univer 允许你自定义单元格的渲染方式。比如你想在单元格里画一个进度条或者嵌入一个小图标就需要自定义渲染器。自定义渲染器需要实现绘制接口接收单元格的坐标、尺寸、数据然后在 Canvas 上绘制。这里的关键是命中检测。你画了一个进度条用户点击进度条的时候你要能判断出点在了进度条上而不是单元格的其他区域。Univer 的渲染层提供了命中检测的注册机制你需要为自定义元素注册一个命中检测函数返回是否命中以及命中的区域信息。还有一个要注意的是重绘触发。自定义渲染的内容如果依赖外部状态当外部状态变化时你需要主动触发重绘。Univer 提供了标记脏区域的方法你可以在状态变化时调用让渲染引擎知道这块区域需要重画。7.3 插件间通信的推荐模式写多个插件时插件之间难免需要通信。我推荐三种模式命令模式、服务模式、事件模式。命令模式适合请求做一件事比如插入一行服务模式适合获取一个能力比如获取公式计算服务事件模式适合通知一件事发生了比如选区变化了。选择哪种模式取决于通信的性质。如果是同步的、需要返回结果的用服务模式如果是异步的、不需要返回结果的用事件模式如果是会改变状态的、需要撤销重做的用命令模式。Univer 本身三种模式都支持你可以根据场景灵活选用。我踩过的一个坑是事件监听没有及时移除。插件销毁时如果忘记移除事件监听会导致内存泄漏而且已销毁的插件还会响应事件引发奇怪的 bug。所以写插件时一定要在销毁生命周期里清理所有监听器和定时器。8. 部署与工程化的注意事项8.1 打包体积的优化Univer 的插件包不少如果全部引入打包体积会比较大。我的做法是按需引入只引入实际用到的插件。比如不需要协同就不引入协同包不需要导入导出就不引入对应的解析包。另外公式引擎里的函数库也可以按需加载只注册用到的函数。代码分割也很重要。把 Univer 相关的代码单独打成一个 chunk利用浏览器的缓存机制。如果 Univer 版本没变用户第二次访问就不用重新下载。配合 HTTP 缓存策略能显著提升加载速度。还有一个细节是 Worker 的使用。公式计算和文件解析可以放到 Web Worker 里避免阻塞主线程。Univer 的部分模块支持 Worker 模式但配置起来稍微麻烦需要处理好主线程和 Worker 之间的通信。如果表格规模不大其实没必要上 Worker反而增加复杂度。8.2 版本升级的兼容性处理Univer 还在快速迭代中版本之间的 API 可能有变化。升级时要注意看变更日志特别是破坏性变更。我的经验是不要跨多个大版本升级一步一步来每个版本都跑一遍测试用例。数据结构的兼容性也要注意。如果你把 Univer 的快照存到了数据库升级后可能读不出来。解决办法是在快照里存一个版本号读取时根据版本号做迁移。或者更简单粗暴一点升级时做一次全量导出再导入把旧格式转成新格式。依赖冲突也是常见问题。Univer 的不同插件可能依赖不同版本的公共包如果版本不匹配会出现运行时错误。用包管理器的时候注意看有没有 peer dependency 警告有的话要手动对齐版本。8.3 监控与错误上报生产环境一定要有监控。Univer 在运行时的错误如果不捕获用户只会看到白屏或者卡死你什么信息都拿不到。我的做法是全局捕获错误和未处理的 Promise 拒绝把错误信息、用户操作路径、表格规模这些上下文一起上报。性能监控也很重要。记录渲染耗时、公式计算耗时、操作响应时间这些指标当指标超过阈值时告警。这样你能在用户投诉之前发现问题。我一般会设置几个关键指标首屏渲染时间、单次操作响应时间、内存占用。这些指标能反映大部分性能问题。错误上报要注意脱敏。表格数据可能包含敏感信息上报时不要把单元格内容带上去只上报错误堆栈和操作类型就够了。这一点在合规上很重要不要等出了问题才想起来。9. 我踩过的几个典型坑9.1 容器尺寸变化导致的白屏这个问题我遇到过两次。一次是容器初始高度为 0Univer 初始化时读到的尺寸是 0Canvas 尺寸也是 0后面即使容器变高了Canvas 也不会自动调整。解决办法是监听容器尺寸变化调用 Univer 的 resize 方法。另一次是容器在 Tab 切换里初始不可见切回来之后渲染错乱。这个需要在 Tab 激活时手动触发一次重绘。9.2 公式循环引用的处理用户不小心写了循环引用比如 A1 引用 B1B1 引用 A1。如果不处理公式引擎会陷入死循环。Univer 的公式引擎有循环引用检测会返回一个错误值。但我在自定义函数里曾经写出过循环调用因为我的函数内部又触发了公式重算。教训是自定义函数里不要触发全局重算只做纯计算。9.3 协同场景下的时间戳问题协同编辑依赖时间戳来判断操作顺序。如果客户端的时间不准会导致操作顺序错乱。解决办法是不要用客户端时间用服务端下发的逻辑时钟。Univer 的协同协议用的是逻辑时钟不依赖物理时间这一点设计得比较好。如果你自己实现协同服务端一定要注意这个问题。9.4 移动端的触摸事件处理移动端和桌面端的交互差异很大。桌面端有 hover移动端没有桌面端有右键菜单移动端是长按桌面端的滚动是滚轮移动端是触摸滑动。Univer 的 UI 插件对移动端做了一些适配但自定义插件如果不注意很容易在移动端出问题。我的建议是自定义交互逻辑时同时考虑鼠标事件和触摸事件做好兼容。10. 一些实用建议与后续方向如果你打算在项目里用 Univer我的建议是先做一个最小可行原型把核心流程跑通再逐步加功能。不要一上来就想着把所有插件都用上那样复杂度太高出了问题也不好定位。先跑通渲染和基础编辑再加公式再加协同一步一步来。学习路径上我建议先看官方文档的快速开始跑通 Demo然后读核心包的源码理解它的架构设计。插件源码可以按需读用到哪个读哪个。社区里有一些实践分享可以参考但要注意版本差异老版本的写法在新版本可能不适用。后续如果要深入可以研究几个方向一是自定义渲染引擎的扩展比如支持图表、图片、富文本二是协同算法的优化比如支持更复杂的冲突解决策略三是服务端能力的扩展比如支持大数据量的导入导出和批量计算。这些方向都有实际需求也有技术深度。最后分享一个小技巧调试 Univer 的渲染问题时可以在 Canvas 上叠加一个调试层把每个单元格的边界、脏区域、命中区域画出来。这样能直观地看到渲染引擎在做什么比看日志高效得多。我在排查渲染错位问题时这个方法帮了大忙。
阅读完成 · 觉得有帮助?
咨询建站