ARTICLE DETAIL

资讯详情

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

Univer开源表格引擎实战:架构解析、协同编辑与二次开发指南

Univer开源表格引擎实战:架构解析、协同编辑与二次开发指南 做 Web 应用的人多少都逃不过“表格”这件事。要么在后台系统里塞一个 Excel 导入导出要么在管理端做一套数据录入界面要么产品经理拿着 Google Sheets 的截图告诉你“就按这个做”。早些年我习惯用 js-xlsx 自己拼数据表格后来换成 Luckysheet效果都还行但一碰到多端协同、权限控制、自定义函数这些需求就得自己写一大堆插件逻辑越写越像一个粗糙的 Excel 克隆。Univer 就是在这个背景下杀出来的。它是一个开源的、基于 TypeScript 的在线电子表格引擎你可以简单理解成一个“可以嵌入到任意前端项目里的在线 Excel 组件”支持公式计算、条件格式、图表、筛选、冻结窗格、导入导出 xlsx/csv还支持多人实时协同。和普通表格插件不一样Univer 把自己定义成一套“办公文档基础设施”核心逻辑、渲染引擎、UI 层和插件体系是完全分离的这意味着你不只是拿它当工具用还能基于它二次开发出真正的产品。这篇文章从一个实际接入过的开发者的角度拆解 Univer 的架构思路、接入流程、核心能力和常见坑。适合的人群很明确正在做数据中后台、协同办公、SaaS 产品的前端工程师以及技术选型阶段的架构师。我会尽量把选型时的对比、接入时的踩坑、协同背后的原理都写透不绕弯子。1. 先弄明白 Univer 到底是什么1.1 不是“又一个在线 Excel”而是办公文档引擎很多人第一次听说 Univer会直接把它归类为“开源版 Google Sheets”。这个说法有道理但不准确。Univer 的核心设计目标是让开发者像引入一个组件库那样把整套表格能力装进自己的应用里。你可以只用一个表格控件看数据也可以把工具栏、公式栏、状态栏、右键菜单全权交给它甚至可以通过 Facade API 远程控制单元格的值、样式和工作簿的增删改。这一点和 Electron 里的桌面表格、或者纯前端的富文本编辑器都不一样。Univer 的分层结构非常清晰最底层是univerjs/core负责数据结构、命令系统、协同逻辑中间是引擎层包括渲染引擎和公式引擎再上层是 UI 层用 Canvas 自绘表格区域用普通 DOM 搭工具栏和弹窗最外面是预设包把常用的公式、条件格式、筛选等功能默认集成在一起。我在接入时最大的感受是Univer 的每一次操作本质上都是“发命令”。点击单元格、打字、拖拽、改样式不直接改数据而是生成一条命令交给核心处理。这个设计初看有点绕但好处非常明显——因为所有变动都可记录、可回放、可合并所以做撤销重做、协同推送、审计日志时几乎不用额外设计。1.2 选型对比为什么我从 Luckysheet 换到 Univer表格组件市面上并不少我简单列一下主流方案的实际体验对比方案部署形态嵌入成本协同能力二次开发维护活跃度Excel 在线版微软云服务低但受制于平台强受限商用Google Sheets谷歌云服务低但国内网络不稳定强受限商用OnlyOffice可私有部署中后端较重强中活跃Luckysheet纯前端低弱需自行实现中停滞过一阵Univer纯前端 SDK中低支持 OT 协同协议映射强高我早年用 Luckysheet 做了几个实验项目它胜在轻量可以快速把数据渲染成表格。但有一个核心痛点Luckysheet 把数据和 UI 绑得比较死我拿不到一个干净的数据模型想接 AI、想做操作日志、想多人同时编辑都得猜它内部的数据结构。Univer 则把“数据怎么存”和“界面怎么画”拆开了所有东西都通过 command 和 facade 走公开接口长期维护的软件项目更适合这种克制架构。如果你只是要展示一个只读表格Univer 有点重。但如果你要做的是“用户可以编辑、协作、导出、扩展”的完整表格应用Univer 是目前开源前端方案里最接近产品级的那个。2. 从零集成一个可编辑表格2.1 环境准备与依赖安装Univer 是用 TypeScript 写的发布物以 npm 包为主所以你的项目最好也基于现代前端工程链。我用的是 Node 18 以上的环境配合 Vue 3 Vite。比较老的 webpack 4 项目能跑通但需要留意依赖的 ESM 兼容性如果遇到编译报错建议升级构建工具链。安装依赖是我踩的第一个坑如果你只看官方 README可能只装一个univerjs/preset-sheets实际上根据你要用到的功能还需要补充依赖。我自己确定的最小集合是这样npm install univerjs/core univerjs/preset-sheets univerjs/sheets-ui univerjs/sheets-formula univerjs/engine-formula univerjs/engine-render这里解释一下各自的职责核心包管数据模型和命令系统预设包负责把常用功能一次性组装好sheets-ui 和 engine-render 负责用户界面与 Canvas 绘制公式相关两个包提供公式解析和计算能力。如果你的项目不需要公式可以去掉公式依赖这样打包体积会小不少。我第一次全量引入后 bundle 直接超过了 1MB后来改成按需引入体积才压下来。安装完成后有一个很容易忽略的点需要引入样式文件。Univer 的样式分为基础样式和主题样式宽泛起见可以直接引入预设包带出来的 CSSimport univerjs/preset-sheets/lib/styles.css;如果漏掉这一步表格区域能渲染但工具栏按钮的图标、弹窗布局大概率是错乱的肉眼看起来就是“样式没加载”。2.2 最小可运行示例说再多不如跑起来。Univer 的初始化大致可以分为三步找到容器 DOM、创建 Univer 实例、创建工作簿数据。我写了一个非常精简的示例基于 Vue 3 的 setup 写法template div refcontainerRef classuniver-container/div /template script setup langts import { onMounted, ref } from vue; import { createSheet, Univer } from univerjs/preset-sheets; import univerjs/preset-sheets/lib/styles.css; const containerRef refHTMLDivElement(); onMounted(async () { const univer new Univer(); await univer.init({ container: containerRef.value, }); univer.createSheet(Univer.SheetType, { id: demo-workbook, name: 教学表格, row: 1000, column: 26, cellData: { A1: { v: 项目, s: { bl: 1, fs: 12 } }, B1: { v: 负责人 }, A2: { v: Univer 接入, s: { cl: { rgb: #1e80ff } } }, B2: { v: 我自己 }, C2: { v: { formula: A2 } }, }, }); }); /script style scoped .univer-container { width: 100%; height: 600px; border: 1px solid #ddd; } /style上面这段代码做完后页面上就会出现一个完整的可编辑表格工具栏、底部的 Sheet 标签页、行列头、单元格编辑框都齐了。注意容器必须有明确的高度和宽度否则 Univer 会算出 0 尺寸然后“消失”这是我第一次接入踩过最蠢的坑排查了半天以为是依赖没装全。cellData是一个用A1、B2这类字符串坐标作为 key 的对象v是单元格的值s是样式对象样式里的bl是加粗、fs是字号、cl是颜色。如果单元格需要公式不需要你手动计算结果直接把v写成{ formula: SUM(A1:A10) }即可引擎会自动求值。2.3 工具栏、菜单和主题按需裁剪 UIUniver 默认会渲染出一整套界面但如果你的产品只需要一个素净的数据表格全套工具栏反而累赘。它提供了配置项来控制 UI 元素我常用的几个设置如下await univer.init({ container: containerRef.value, ui: { toolbar: { items: [bold, italic, undo, redo, merge, formula], }, }, });工具栏的 items 配置是白名单模式你需要什么加什么。默认配置虽然全但其实很多按钮在编辑场景里根本没人用把 entry 入口、截图、打印这些按钮藏起来整个界面会清爽很多。右键菜单和上下文菜单也是可以裁剪的在ui.menu里配置。有一点需要注意Univer 的菜单配置项在不同 minor 版本里变动挺频繁我升级一个版本时发现某个菜单 key 换了名字导致右键菜单少了一项。这种 API 变动在官方 GitHub 的 CHANGELOG 里会有说明遇到菜单行为不对先去翻 CHANGELOG 而不是怀疑业务代码。主题方面Univer 支持亮色和暗色主题初始化时传入theme配置即可。如果产品用深色模式可以直接套官方暗色主题不用自己一套套改 Canvas 里的颜色省不少时间。3. 真正值钱的三个能力公式、协同与数据交换3.1 公式引擎从内置函数到自定义函数Univer 的公式引擎是独立设计的它的定位不是“模拟 Excel 的几个函数”而是一套完整的公式计算框架。内置函数覆盖了日常用的求和、平均、查找、文本处理等场景基本可以满足 90% 的需求。我在接入时最在意的是两点公式能否跨 Sheet 引用、能否保存成 xlsx 后公式不丢失。这两点实测都没问题。跨 Sheet 引用的写法是Sheet1!A1Univer 能正确识别并且当被引用的单元格变化时依赖它的单元格会自动重算。这背后的机制是公式引擎会做依赖图分析不是一个公式变了全表重新算一遍而是只重算受影响的那条依赖链。我一开始担心一个千行表格里摆五十个 VLOOKUP 会不会卡死实测下来在 Canvas 渲染 局部计算的前提下普通办公级数据量是完全没压力的。真正的重头戏是自定义函数。比如我需要一个“从字符串里提取手机号”的业务函数Univer 允许你注册一个新的函数名然后实现它的计算逻辑。流程是这样import { FunctionType } from univerjs/engine-formula; const ExtractPhoneFunction { name: EXTRACT_PHONE, functionType: FunctionType.USER, minParams: 1, calculate: (params) { const text String(params[0]); const match text.match(/1[3-9]\d{9}/); return match ? match[0] : ; }, };然后通过插件注册方式把函数挂到公式引擎里。注册后用户在单元格里输入EXTRACT_PHONE(A2)就能直接使用。有一点要特别提醒自定义函数的calculate在 Univer 的 worker 模式下运行它不能访问 DOM 也不能用闭包里的复杂对象必须是无副作用的纯函数。我第一版在里面引用了全局配置对象结果公式一计算就报错排查半天才意识到是运行环境的问题。3.2 多人在线协同的实现路径协同比公式更能体现 Univer 的设计功底。传统的协同方案是“保存时覆盖”多人编辑时后保存的人会把前面的覆盖掉。Univer 走的是操作转换Operational Transformation的路子简单说就是两个人同时编辑同一格服务器不直接存结果而是把两个人的操作相互转换让两个人的文档最终收敛到同一个状态。我画的实操路径是这样的用户输入字符产生一条命令命令被序列化成协同事件协同服务收到事件后根据当前文档版本做 transform再广播给房间内其他人其他客户端收到 transform 后的事件把事件应用到自己本地文档。整个过程在 Univer 里被封装成了univerjs/collaboration一类的能力核心数据模型天然具备“事件同步”的结构。实际实现多人协同需要你自己搭一个 WebSocket 服务端因为纯前端 SDK 只是提供了协议的客户端部分。我当时的归档是WebSocket 服务负责事件转发和版本管理Redis 存会话状态业务后端负责权限验证。接入流程可以参考下面的思路用户打开表格时服务端下发当前文档快照客户端建立 WebSocket 连接把自己的会话 ID 和文档 ID 绑定每一次本地编辑先把命令发给服务端服务端 transform 后把命令广播给房间内其他人客户端收到远端命令后应用到本地。这里最容易被低估的是“命令的幂等性”。如果一条命令被重复应用表格数据就会乱。Univer 的命令系统设计得比较规整命令 ID 加版本号基本能避免重复应用。上线前我建议做一次五六个浏览器同时编辑同一处的高压测试重点看冲突时的光标位置是否正常、撤销时会不会把别人的操作一起撤销掉。3.3 导入导出 Excel 和 CSV保真度与性能数据导入导出几乎是所有表格需求的底线Univer 支持 xlsx 和 csv 的导入导出。如果要启用需要安装univerjs/sheets-import-export并在初始化时注册。实测下来xlsx 的标准功能——单元格样式、合并单元格、列宽行高、基础的公式、数据筛选——导入导出的保真度都不错。但有几个点必须提醒第一Excel 里的图片和图表在 xlsx 文件中属于二进制附件和图表 XMLUniver 只保证单元格数据层面兼容复杂的图表在导入后有可能变成空对象导出时部分对象也会丢失。第二超大文件的导入不能走同步路径。一个 20MB 的 xlsx即使前端解析只需要几秒渲染时也可能把浏览器主线程卡住。我的建议是导入后立刻用 Univer 的“断点渲染”能力或者先展示前 1000 行用户滚动到哪才真正渲染到哪加载体验能好很多。第三如果你把 Univer 部署在低端客户机上对内存要做预期管理。一个充满单元格样式的大表格会消耗很多内存建议给容器外的使用者提供“导出为 CSV”的轻量通道能规避很多不必要的崩溃。4. 性能调优、常见坑与生产部署4.1 渲染性能和大数据量的实测建议我第一次用 Univer 渲染 5 万行数据时滚动流畅度其实不错因为在 Canvas 渲染模式下Univer 只会绘制视口范围内的单元格而不是把所有 5 万行全部画出来。这个“怎么画”的问题被渲染引擎内部解决了作为使用者你不需要手动指定哪些行可见。但“定义数据”依旧可能成为性能瓶颈。如果一次性地把 5 万行乘 20 列的巨大 JSON 扔给单元格数据模型就算画面只画可见部分数据结构本身也会占用大量内存以及初始化时间。我的经验是数据按需加载初次只给前 1 千行触底时通过 Facade API 增量添加尽量避免给整表设置复杂的条件格式规则条件格式是叠加判断规则多了会拖慢重算公式计算可以开启 worker 模式把计算放到 Web Worker 里这样编辑单元格时界面不会卡顿频繁更新单元格时尽量合并成批量命令一条条 setCellValue 会有额外开销。另外 Univer 在处理大量样式时会有克隆成本尤其是给几千个单元格分别设置不同的颜色和边框最好不要用循环逐个 set 样式而是一次性构造区域样式对象。4.2 高频问题速查表接入过程中我整理了一份问题清单很多都是社区里反复被问到的现象大概率原因处理方式页面白屏表格不显示容器高度为 0 / CSS 未引入给容器设置显式高度并引入样式文件工具栏缺图标按需引入时漏了 icons 包检查 UI 相关依赖是否完整安装中文显示为方块字体加载策略问题初始化配置里指定中文字体渲染或引入全局字体公式计算结果为空公式引擎未注册确认已经安装并注册 formula 插件协同编辑时数据互相覆盖服务端 transform 逻辑缺失检查自己实现的协同服务端是否做了版本合并导入 xlsx 后样式全丢未启用 import 相关插件注册 sheets-import-export 插件构建警告 ESM 问题旧版 webpack 兼容性不足升级构建工具或配置 polyfill这里最想强调的是协同问题因为 Univer 官方 SDK 只负责客户端协议映射服务端如果没有正确处理 transform数据一样会冲突。很多人以为引了 univer 就自动多端同步这是个比较大的误解等注意到的时候已经丢了几组测试数据了。4.3 生产部署与容器化Univer 本质上是一个前端包部署方式和你部署其他单页应用没有区别。构建产物里的 JS 包体积偏大务必开启 gzip 或 brotli 压缩在 Nginx 里加一段location / { gzip on; gzip_types application/javascript text/css application/json; }如果你是给内部系统用可能需要内网部署直接在后台应用里打包前端资源即可。如果放在公网建议所有静态资源走 CDN并且注意 WebSocket 跨域问题协同服务端要允许前端域名访问。Univer 在初始化时会加载国际化语言包如果服务部署在网络受限环境最好把语言资源提前打包初版我没注意这个上线后发现英文语言包加载失败导致菜单显示英文。尤其对于中文产品记得在初始化时显式设置locale: zhCN。5. 扩展与二次开发把表格变成产品的一部分5.1 理解插件机制你要改的不是组件而是流程Univer 的插件机制可以说是它的灵魂。官方把这些插件按照功能拆得很碎有负责公式的、有负责条件格式的、有负责筛选排序的你甚至可以写一个插件拦截用户的保存命令把表格数据同步到自己的业务后端。插件本质上是一个注册器它可以在不同生命周期里挂载自己的逻辑。我做过一个最简单的插件主要用来监听单元格变化并发送到后端保存import { Plugin } from univerjs/core; class SyncerPlugin extends Plugin { static NAME demo.syncer; onMounted(): void { this.getContext() .getCommandService() .onCommandExecuted((command) { if (command.type 4) { // set cell value 命令类型 fetch(/api/saveSheet, { method: POST, body: JSON.stringify(command.arguments), }); } }); } }初看这套机制会觉得复杂但用过几次之后你会发现它比“在渲染层 bind 几个 event”靠谱得多。因为命令层是不依赖 UI 的无论用户是通过工具栏、右键菜单、还是脚本调用发起的操作你都能统一拦截并处理。5.2 三个典型扩展场景第一个场景是“权限控制”。协同编辑产品里不同人只能编辑自己负责的列。Univer 本身不限制你的业务规则但你可以封装一个插件拦截编辑命令判断目标单元格是否属于当前用户可写区域不可写就直接拒绝。这个思路比在 UI 层隐藏单元格更安全。第二个场景是“一键导出业务报表”。产品里经常要生成带公司 Logo 和指定格式的表格Univer 负责展示数据导出时可以在插件里动态调整样式然后调用导出功能。因为命令是可控的你可以先批量设置样式再导出得到的 xlsx 比用户手动排版整齐得多。第三个场景是结合 AI 数据分析。这是很多团队正在探索的方向。Univer 提供了 Facade API可以读取整个工作簿的数据结构、调用公式引擎、修改任意单元格。AI 拿到表格数据后通过自然语言生成筛选条件然后动态更新数据源最后把结论直接写进表格。本质上Univer 在这里就是 AI 和用户之间的“可视化容器”比让 AI 直接吐 Markdown 表格要正式得多。这类扩展里我建议重点关注安全。表格是业务数据的非常直接的载体接口鉴权、单元格权限、导出文件脱敏都要做扎实。6. 最后再分享几个实用技巧如果你正打算在项目里引入 Univer我根据自己的实操经验给你几个不一定写在文档里的建议。第一升级版本前先去 GitHub 看 CHANGELOGUniver 迭代速度非常快跨 minor 版本升级很可能会出现配置项改名不要轻易跳太多版本。第二协同功能要尽早设计服务端前端把命令对象准备好之后尽早联调协议不要等表格功能全做完再补协同否则回改成本很高。第三遇到表格区域渲染异常时先怀疑容器高度、样式文件和 bundle 重复引入再怀疑业务代码这三个问题占了接入阶段八成以上的故障。Univer 这款工具最打动我的一点是它没有尝试“再造一个 Excel 然后让你放弃 Excel”而是选择成为开发者手里的积木。公式、渲染、协同这些硬核能力都被拆成了可以组合的模块你可以像搭积木一样把它嵌入到自己的产品里为数据表格赋予真正符合业务逻辑的交互。这种思路在开源表格领域里算是比较难得的。希望这些经验能帮你少走几个弯路做出一款真正能打的表格应用。
返回列表