最近在调研一批能嵌入自家产品的开源办公组件时,univer这个项目又一次被推到台前。它是 Luckysheet 团队的全新重构版本,GitHub 热度一直不低,关键词“univer”“univer在线”也跟着火了一波。这个东西的实际定位并不只是一个“在线 Excel 替代品”,而是一套可以拆开、按需拼装的开源办公套件引擎。前端团队如果想去掉 iframe 套壳这类凑合方案,想在 Web 应用里做出一块能与 Excel 掰手腕的表格区域,又希望功能能顺着业务长出来,那 Univer 基本就是绕不开的选项。
这篇文章不讲抽象概念,只聊实际的东西:Univer 的核心价值在哪、它的技术设计为什么敢这么设计、怎么把它真正集成进业务系统,以及在选型时要避开的坑。
1. Univer 究竟是什么:从 Luckysheet 到通用办公套件
1.1 项目定位:不止是表格,是模块化办公引擎
很多人第一次看到 Univer 的 GitHub 仓库,第一反应是“这不又是一个在线表格吗”。但如果只把它当成表格,就严重低估了这个项目的定位。Univer 的完整形态是一套“模块化办公套件”:电子表格、富文本文档、幻灯片都被纳入同一个内核,每个能力以插件的形式分化出来。也就是说,你可以在同一个引擎里既有表格又有文档,也可以只取表格部分嵌入你的收银台、数据后台、教学系统,甚至给表格做一个完全属于自己的外观和交互。
从 Luckysheet 到 Univer 的演进尤其值得注意。Luckysheet 当年火,是因为作者把 Excel 风格的交互搬到了纯前端 Canvas 上,不用任何后端就能模拟出相当细腻的操作体验。但它毕竟是在早期架构上快速叠加功能,随着 Excel 级功能变多——公式引擎、条件格式、数据透视表、跨表引用——代码复杂度开始失控。Univer 是推倒重来的结果:数据层、渲染层、命令层、插件层全部独立,每个模块都能单独维护、单独测试。这个改动带来的直接影响是,不再像 Luckysheet 那样只能做一个“大而全的表格”,而是能够以表格为第一优先级,同时向文档和演示文稿延伸。
这里要刻意澄清一个概念:Univer 不等于一个“产品”。它更像是一个 SDK、一套引擎,最终用户看到的是你基于它拼出来的样子。你可以把官方默认 UI 直接跑起来,也可以完全隐藏内置 UI,只调用数据 API,让表格成为一个无头组件,再用你自己的按钮、菜单、弹窗去操作它。这种拆解对产品团队来说特别有吸引力,因为多数编辑型功能很难复用,而 Univer 把“编辑”这件事做成了可以被集成的基础设施。
1.2 一句话能力清单:编辑、公式、可视化、协作
如果只能挑几条来说明 Univer 能干什么,我会按优先级列这样一份清单:
- 电子表格编辑:支持单元格编辑、选区、右键菜单、格式刷、合并单元格、冻结窗格、筛选排序、拖拽填充、复制粘贴(含跨表格、跨标签页)。
- 公式计算:内置常用数学、逻辑、文本、日期、查找引用函数,支持跨工作表引用和自定义函数扩展。
- 数据处理:数据透视表、条件格式、图表,能覆盖常规数据分析场景。
- 导入导出:开源版使用 SheetJS 生态完成 Excel、CSV 导入导出,可嵌入流程。
- 多人协作:架构上预留协作能力,命令可同步,但生产环境需要自己搭建协同服务。
- 移动端与跨端:底层基于 Canvas 渲染,同一套代码可适配桌面与移动端浏览器。
这些能力意味着,你不必为了做“一张能编辑的表格”去同时维护六个前端库。公式、格式、历史记录、快捷键、编辑器状态这些最难啃的部分,Univer 已经替你消化了很大一部分。剩下需要业务自己解决的,主要是“数据从哪来、改完了存到哪去、权限怎么控”这类后端问题。
1.3 它解决了什么真问题
在引入 Univer 之前,很多团队面对的需求是这样的:产品经理想要一个页面,让运营人员可以在线维护配置数据、批量调整报价、导出报表,而不是反复提工单让研发改数据库。直接上 Excel 复制粘贴太原始,套用开源的富文本编辑器又发现表格单元格编辑比想象中复杂一百倍——光标位置、选区、公式、复制粘贴格式、撤消重做,每一件都不是十天半月能写完的。
Univer 解决的是这个“复杂编辑能力”从零到一的问题。它把 Excel 级编辑体验打包成了前端组件,让研发团队不需要研究光标如何定位、选区如何渲染、公式如何求值,而是专注在业务逻辑上。这也是为什么很多低代码平台、数据分析产品、内部管理系统都在看 Univer 的原因:它本质上是在帮你做“办公领域最麻烦的脏活累活”。
2. 技术拆解:Univer 的五个核心设计
2.1 渲染引擎:Canvas 绘制与 DOM 编辑的博弈
Univer 最核心的技术决策是把渲染放在 Canvas 上,而不是像传统网页表格那样用大量 DOM 节点堆出单元格。之所以这么做,核心原因是滚动性能和编辑流畅度。一个包含几万行数据的表格,如果用 DOM 渲染,浏览器需要同时维护数万个节点,任何一次滚动和选区变化都会引起大批量重排,性能断崖下跌。Canvas 则把整块画布当作一张位图,鼠标移动、滚轮滚动时只需要重新绘制区域内的图形,开销稳定得多。
具体到执行层,Univer 维护了一套自己的渲染引擎,负责处理视口计算、图层管理、事件坐标换算。注意“事件坐标换算”这一项,因为整个页面是一块画布,浏览器并不知道某个单元格是一个独立可交互节点,所有点击、悬停、键盘输入都要通过坐标判断落在哪个区域。Univer 内部有一套区域命中检测机制,能够根据网格布局反推出当前坐标对应的单元格,再触发对应的命令。这套机制意味着开发者需要接受一个事实:你没法用传统 DOM 选择器去操作某个单元格,所有交互都要走 Univer 的命令或 API。
从我实际体验看,这就是选择 Univer 的隐含换——你可以获得接近原生的流畅度,但同时也失去了浏览器给的标准文本交互。输入法、剪贴板、富文本这些平时依赖 DOM 原生能力做的事,都需要在 Canvas 层做额外适配。Univer 对此有专门处理,后面章节会提到实际遇到的问题。
2.2 命令系统:把每一次操作变成可追溯的事件
Univer 的内部不再是“函数调用式”的,而是“命令式”的。用户点击一个按钮、按一个快捷键、甚至由协作端收到远程操作,最终都会归一为一条命令。拿“把 A1 单元格改成 100”举例:这不会直接在数据模型上改值,而是创建一个 SetRangeValues 命令,命令携带工作表 ID、改动范围、新值这些参数,随后被派发到命令系统执行,同时记录到撤销栈。这样的设计有非常多的好处。
第一,撤销重做变得好做。因为所有变更都有迹可循,Univer 能维护栈结构,一条命令的逆转就是另一条命令。第二,协作容易做。多人编辑中每个用户产生的操作都是一条命令,服务端或者一个 broker 只需要把这些命令广播给其他在线客户端,其他客户端按照相同顺序播放命令即可最终收敛到一致性状态,这比直接同步整块单元格数据轻量得多。第三,业务扩展干净。你想加一个“把 B 列所有空值填 0”的功能,不需要去改 Univer 内核,只需要造一台业务自定义命令,内部调用若干基础命令,系统会自动处理撤销和合并。
所以研发团队接入后,应该尽量养成“通过命令改数据”的习惯,不要直接去修改内部数据模型。任何绕过命令系统的操作都会破坏撤销栈和协作双方的同步,这在多端产品里会引发诡异的数据不一致问题。
2.3 插件化设计:核心、功能、UI 三个层级解耦
Univer 的插件架构是最影响日常开发的设计。从包里就能看出一二:@univerjs/core是内核,只负责数据模型、命令基础、类型定义;@univerjs/sheets提供电子表格的功能逻辑,不包含界面;@univerjs/sheets-ui才提供工具栏、右键菜单、弹窗这些 UI 部分。你甚至可以只装@univerjs/sheets,使用它的事件和公式计算能力,把所有界面包在自己产品里。
这也意味着功能组合变得非常灵活。如果你的产品只需要表格的“公式计算”能力,不需要用户看到标准 Excel 的编辑界面,你可以跳过 UI 插件,只使用公式引擎接口。反过来,如果你想要完整办公体验,那就把官方 UI 插件接进来,注册一个容器节点,一行代码加载。插件之间通过内核提供的依赖注入容器去订阅事件、注册菜单、添加工具栏按钮,整体耦合度低,有利于团队并行开发。
由插件机制延伸出的一个工程实践是:企业完全可以针对自己的业务封装私有插件。比如表格里要频繁插入“审批金额”这种特殊类型,就可以做一个自定义渲染插件,让单元格以特定颜色和图标显示。这种能力在传统在线 Excel 里通常要改宿主代码,而在 Univer 里只是新增一个插件包。
2.4 公式引擎:自带计算引擎的意义
很多前端表格组件是不带公式计算引擎的,只负责把静态数据渲染到页面上。一旦用户编辑某个单元格,其他关联单元格的值不会自动重新计算。Univer 则内置了公式引擎,支持函数解析、依赖追踪和重算调度。
这里有一个关键细节:公式引擎是一个独立模块@univerjs/engine-formula,它在数据模型中维护公式源码、参数依赖关系和计算结果。当一个区域内某个单元格被修改,公式引擎能快速找出哪些公式单元格依赖于这个单元格,进行最小范围的重算。这种“脏格子标记+局部重算”的设计,比每次编辑后全表公式重刷的性能好很多。
从业务集成的角度,公式引擎的存在也让“业务逻辑数据化”更容易。比如,许多内部系统不再需要写后端逻辑去计算合计、折扣、汇总,只需在表格里写入 SUM 公式,让引擎计算。必要时还可以注册自定义函数,比如一个CUSTOM_CALC_TAX()函数,把税点计算封装进表格内部,这样运营人员调整参数时,整个报表会自动联动。
2.5 实时协作的底层思路
Univer 的协作方案并不神秘,核心是把前面提到的命令系统与一个消息通道结合起来。客户端产生操作时,先生成本地命令并执行,乐观更新界面,同时将同一命令序列化后发到协同服务;协同服务负责消息转发、排序、持久化;其他客户端收到远程命令后,在本地按照同样的顺序执行。为了保证一致性,Univer 在设计中预留了对命令的合并、冲突处理接口,而真正的 OT 或 CRDT 算法则需要结合业务后端去实现。
这个定位很值得思考:官方把协作做成了“架构支持”,而不是“开箱即用的完整服务”。这意味着如果你要做多人同时编辑一个表格,需要自己搞定 WebSocket 服务端、房间管理、操作日志、历史版本、权限控制。这比集成一个纯前端表格组件要重不少,但同时也给企业留下了足够的掌控空间——你完全可以只开放给内部局域网用户,并把所有编辑数据存到自己的数据库里。
3. 把 Univer 集成进你的产品:实操与代码
3.1 五分钟跑起第一个表格
集成 Univer 比想象中直接。官方发布到 npm 的包已经拆分清楚,以当前主流版本为例,最小示例只需要安装核心、表格功能、表格 UI 三个包:
npm install @univerjs/core @univerjs/sheets @univerjs/sheets-ui然后在主入口初始化:
import { Univer, UniverInstanceType } from '@univerjs/core'; import { UniverSheetsPlugin } from '@univerjs/sheets'; import { UniverSheetsUIPlugin } from '@univerjs/sheets-ui'; const univer = new Univer(); // 注册表格功能插件 univer.registerPlugin(UniverSheetsPlugin); // 注册表格 UI 插件,指定挂载节点 univer.registerPlugin(UniverSheetsUIPlugin, { container: document.getElementById('app') as HTMLElement, }); // 创建一个空工作簿 univer.createUnit(UniverInstanceType.UNIVER_SHEET, { name: 'Demo Workbook', sheets: { sheet1: { name: 'Sheet1', cellData: { A1: { v: 'Hello Univer', s: { bd: { b: { s: 1 } } } }, }, }, }, });这段代码把事情拆得很直白:Univer 只是内核,UniverSheetsPlugin承载表格逻辑,UniverSheetsUIPlugin负责把界面渲染到 container 节点。cellData里的结构可能让新人不适应,它的对象键是 A1、B2 这种单元格坐标,值是包含v(值)、s(样式)的对象。只要按照这种数据格式组织,就能在前端直接初始化出带格式的表格。
如果你用的是 Vite、Webpack 这类常规工程化环境,基本不需要什么特殊配置。需要注意的一点是,Univer 的一整套包体积不小,建议开启代码分割并按需引入,不要让无关插件(比如文档、幻灯片)影响首屏加载。
3.2 读写业务数据与监听用户编辑
集成表格组件后,最常见的需求就是把后端数据填进表格、用户改完之后再拿回来。Univer 对这块暴露的 API 比较全面,核心有两块:获取当前活动单元的数据结构,以及监听特定类型的命令。
如果你只是要取整份数据,可以直接拿到当前工作簿的快照结构。以 Sheet 的快照为例,里面包含工作簿名称、每个 Sheet 的名称、行列数、cellData、样式、合并单元格配置。通常会写成这样的思路:
function getAllCellData() { const activeSheet = univer.getActiveUnit(); // 获取当前工作簿的快照 const snapshot = activeSheet.getSnapshot(); return snapshot; }这里我把具体方法名写成通用思路,因为不同版本的方法命名会有差异,建议以官方 API 文档为准。重要的是理解机制:Univer 的“快照”是一个可序列化的大对象,业务层可以把整个快照存到后端,也可以深度遍历 cellData 取出需要的字段。
要监听用户修改,则应该订阅命令执行流。比如用户改动单元格后会触发 range change 或 set range values 相关命令,业务代码监听这类命令,解析命令参数里的行列范围和新值,再决定怎么提醒后端。示例思路:
univer.onCommandExecuted((command) => { if (command.type === 'SHEET_SET_RANGE_VALUES') { // 解析 command.params,获取改动范围 // 这里可以根据 params 更新业务缓存 } });用“命令监听”而不是“数据轮询”,是一个很容易被忽略的工程点。轮询快照会有延迟且浪费性能,而命令监听能拿到精确的改动范围,非常适合做增量保存,也适合关联业务图表实时刷新。
3.3 数据保存与后端联调方案
将 Univer 接入现有系统,一般要确定一个保存策略。根据我的经验,有三个层次的方案,复杂度递增:
- 整表快照保存:每次用户点击保存按钮,或者业务进入空闲若干秒后,将整份工作簿快照序列化后交给后端。这种方案实现最简单,适合配置单或个人报表,缺点是高并发多人编辑时容易互相覆盖。
- 基于命令的增量保存:监听命令事件,把用户每一步操作的命令序列发送到后端,后端按顺序记录。建议把每一条命令封装成 JSON 存储,同时定期生成一个基础快照用于快速加载,这样保存与回放都高效。
- 协同服务式保存:引入专门的协同服务端,维护命令序列、冲突合并和房间状态,客户端之间实时同步。这是多人同时编辑一个表格的终极方案,但后端工作量显著增加。
对于多数内部系统,我建议先从第二种方案开始。它不只解决保存问题,还能给你完整的操作审计日志——谁在什么时候改了哪个单元格,命令序列里全都写着。后期若要做数据权限和回滚,数据基础也扎实得多。
后端拿到命令序列之后,需要关注一件事:单元格地址的稳定性。Univer 的地址坐标是基于 0 开始的行列索引,插入行或者删除列之后,地址会平移。如果你把命令原样存库,将来恢复快照时要保证命令执行顺序一致,否则会错位。保险的做法是快照为主、命令为辅,定期对拍一下,避免长期累积漂移。
3.4 在 React / Vue 宿主应用中的注意点
我见过不少团队把 Univer 当成一个普通组件直接塞进 React 或 Vue 的路由页面里,结果遇到各种挂载冲突。原因在于 Univer 是有自己的生命周期和事件循环的,宿主框架的严格模式(比如 React StrictMode)会触发组件重复挂载卸载,容易造成 Univer 实例重复创建、容器节点被清空等问题。
稳妥做法是给 Univer 单独做一个封装组件,在组件挂载时初始化实例,在组件卸载时调用销毁方法。另外,不要在 Provider 或组件内联逻辑里每次渲染都调用new Univer(),那会让文档模型被反复重置。最好是在模块级或者上下文里持有唯一实例,UI 层只负责把容器传给实例。对自定义单元格渲染和工具栏扩展也一样,统一通过插件注册,避免在渲染函数里直接读写 Univer 状态。
还有一个隐蔽问题涉及打包。如果你同时使用了多个包含 Univer 的 npm 包,可能出现“核心包存在多份副本”的情况,代码里到处跑着两套命令系统。排查方法是在构建配置里把@univerjs/*相关包统一指向同一个 node_modules 根,必要时使用 resolve.alias 去重。
4. 谁适合用 Univer:与市面方案的正面对比
4.1 主要竞品横向对比
网上讨论 Univer 时总要拉出另外几个同类选手对比,我从实际选型视角做一个表格,重点看集成友好度和扩展边界。
| 方案 | 渲染方式 | 开源协议 | 定制难度 | 协作能力 | 适合场景 |
|---|---|---|---|---|---|
| Univer | Canvas | 开源 | 中低(插件化) | 架构支持、需自建服务 | 需要深度嵌入自家产品的完整在线表格 |
| Luckysheet | Canvas | 开源 | 中 | 弱 | 零散应用、简单的表格展示与编辑 |
| OnlyOffice 前端 | 混合渲染 | 开源 | 高 | 完整(自建服务) | 主打完整 Office 文档协作的独立产品 |
| Excel Online 嵌入 | 封闭 | 商业授权 | 低(仅白标) | 完整 | 不差预算、愿意被微软生态锁定 |
| Handsontable | DOM 网格 | 商业授权 | 低 | 弱 | 数据录入型表单、网格展示 |
这个表能看出来,Univer 的生态位很独特:它不像 OnlyOffice 那样是一整套服务,而是更贴近前端 SDK,企业和开发者能直接改内部逻辑。反过来,如果你的需求只是“页面上有一个能快速填数据的网格”,Handsontable 可能更轻量,不必把完整的公式引擎和文档模型拖进来。
从协议角度还要额外说一句,如果你是做商业闭源产品,一定要去官方仓库查当前开源协议的精确条款,确认是否允许闭源集成以及是否需要开源自己新增的插件代码。不同版本之间存在协议调整的可能,这是选型前必须做的法律尽调。
4.2 场景选型建议:什么情况别用 Univer
我虽然看好 Univer,但并不认为它适合所有项目,下面这几个场景需要慎重:
- 只是做一个数据录入表单,不需要 Excel 的网格交互。用成熟表格组件会更快,Univer 的架构在这里属于杀鸡用牛刀。
- 没有专职前端或前端能力薄弱,只想快速部署一套在线办公软件。这时 OnlyOffice 社区版或直接商业授权可能是更稳的路径,因为它是完整产品而非 SDK。
- 对离线编辑、本地文件系统有强诉求。Univer 定位 Web 协作优先,虽然可以用 Electron 套壳,但那不是它的主战场。
- 后端资源极度有限。若要做多人实时协作编辑,没有团队维护 WebSocket 协同服务,那 Univer 的核心优势发挥不出来,不如退回单机编辑。
倒过来看,选 Univer 的典型用户画像其实很清晰:有一支靠谱的研发团队,产品不想在办公编辑能力上长期堆人,同时希望保持 UI、交互和数据流向的完全控制权。
4.3 从 Luckysheet 迁移到 Univer 的顾虑
近几年不少旧项目在用 Luckysheet,团队迁到 Univer 前最大的顾虑是接口体系变化太大。这是事实:Univer 不是一个兼容层,不是“改改包名就能升上去”,你必须理解新的命令系统和插件机制,然后重写数据桥接逻辑。
但迁移本身是可以分阶段的。我建议先从导出功能入手:利用 Univer 的公式引擎,把旧表格需要用到的计算公式和单元格样式导入,验证计算一致性,再逐步接入真实编辑。千万不要指望一次性镜像所有数据格式。旧项目里的“隐藏行”“自定义格式”“宏”这类深度能力,需要逐个在 Univer 侧做映射,前期工作量不可小觑。
5. 我从接入 Univer 项目中总结的经验教训
5.1 性能优化:大数据量下的保护动作
Univer 的 Canvas 渲染让它能扛住大规模数据的展示,但不是说所有优化都不需要做。我接过一个包含五六万行物流明细的报表,不做任何保护直接初始化,首帧渲染时间依然明显,滚动过程偶尔卡顿。排查后发现问题不全在渲染,而是深层数据结构计算和格式化处理消耗了时间。
我建议做两层控制:一是初始化时尽量分批写入数据,不要让 cellData 对象在同一个渲染周期里全部展开到内存;二是关掉非必要的监听和公式自动重算,比如在批量导入阶段先暂停公式引擎的自动计算,等数据落定后再手动执行一次整体重算。对于超大表格,还可以在业务层设置默认滚动区域,只加载可视范围内的数据。Univer 的内核负责渲染,你的业务层负责控制数据流,两者配合才能达到理想体验。
5.2 中文输入法和剪贴板兼容
Canvas 编辑最麻烦的问题是中文输入法。由于页面没有真实 DOM 输入框,Univer 需要维护一个隐藏输入框来接收 IME 组合字符,然后在组合过程中捕获候选词。实测下来,多数常用输入法(搜狗、微软拼音、微信输入法)都能正常工作,但偶尔会出现输入法弹窗定位不准或者候选词丢失的情况。
剪贴板也是一个坑。直接从网页复制富文本再粘贴到 Univer 时,Univer 需要同时处理 HTML 和纯文本两种格式,稍有不慎就会出现样式错乱。我的建议是尽早建立剪贴板解析的测试用例清单,覆盖“Excel 复制到 Univer”“Univer 复制到 Excel”“网页富文本粘贴”三条主线,确定表现符合预期后再放量给用户。
5.3 API 演进快,如何锁版本
Univer 还处在快速迭代期,API 变动比较频繁。这一点不算缺点,但如果你在长达半年的项目周期里依赖它,就一定要锁版本。
生产环境建议精确到 patch 版本号,比如"@univerjs/sheets": "0.1.3",不要使用^0.1.3这种允许自动升级 minor 的写法。升级前先看官方 changelog,尤其关注命令类型、插件注册方式、快照数据结构这三个核心接口的变动。团队内部如果有多个服务同时用到 Univer,尽量维护一套同版本的公共依赖,否则会出现明明代码写的是同一个 API,实际两个包里行为不一致的诡异问题。
5.4 协作能力的落地成本再评估
如果你确实需要多人同时编辑一个表格,请把预算做足。Univer 架构给了协作的底子,但离真正生产可用还差一个协同后端。你需要提供 WebSocket 网关、房间管理、连接保活、消息顺序、离线重连、权限校验、历史版本回滚等等。这些组件加起来是一个不小的中间件工程。
如果项目预算不允许自研,可以考虑由商业服务来支撑,或者退而求其次实现“单用户编辑、多用户只读”的半在线模式。先让一部分协作场景跑起来,再逐步演进到实时编辑,是我踩过多次坑之后推荐的稳妥路线。
6. 最后分享一个我调 Univer 的小技巧
如果你要把 Univer 嵌入到一个内容比较多的管理后台,记得给表容器一个明确的宽高,并且在外层避免使用display: none到display: block的切换。Univer 在隐藏容器里初始化时,动画、滚动、候选框都会出现定位偏差,原因在于它初始化时读取了错误的视口尺寸。解决办法是在容器可见后手动触发一次 resize 刷新,或者干脆在隐藏时不初始化、显示后再动态创建实例。
我自己的经验是,把 Univer 容器的挂载节点独立出来,不要和页面的通用弹窗、抽屉共用一层包裹,这样能避免大量诡异布局问题。接入成熟的复杂编辑组件,说白了就是尊重它的生命周期、数据入口和渲染边界,把宿主环境的脏事隔离干净。这也是我在多个项目里反复验证的结论:工具选对了只是开始,集成工程做得克制、留好边界,长期维护才会省心。