如果你维护过中后台系统,大概率会遇到这样一个需求:老板说需要一款“像 Excel 一样的在线表格”,产品原型随手甩给你一个链接。此时的选型其实不多:商业控件授权费高,自己写一个网格组件又得处理公式、筛选、冻结、合并单元格……一套组合拳下来,项目周期至少翻一倍。Univer 就是在这种背景下进入我视野的——它不止是表格,而是一套可嵌入 Web 应用的办公套件 SDK,能帮你在浏览器里快速落地在线表格、文档甚至演示文稿,并且核心代码完全开源。
Univer 在圈子里热度一直不低。很多人叫它“开源在线 Excel”,但这只说对了一半:它支持公式、图表、条件格式、协同编辑这些表格能力,同时也把文档和幻灯片纳入了同一套架构。对前端开发者来说,它意味着不用再维护一个“四不像”的自研表格组件;对产品经理来说,它意味着可以通过按钮级别的配置快速交付一个接近 Office 体验的在线办公模块。这篇文章我会从项目定位、架构设计、实际集成到踩坑记录,完整拆一遍 Univer 的使用心得,希望能帮你在选型时少走弯路。
1. Univer 是什么:从项目定位到适用场景
1.1 从 Luckysheet 到 Univer 的项目背景
Univer 由 dream-num 团队主导,在这个项目之前,团队还做过 Luckysheet。如果你以前研究过 Luckysheet,会发现它是一款很优秀的纯前端表格库,但它的定位始终是“表格组件”,在数据模型、插件体系、多文档支持上存在不少历史包袱。Univer 更像是基于先前经验重写的一代产品:底层用 TypeScript 重新设计,把渲染、交互、数据、插件拆成了独立模块,再往上叠加表格、文档、幻灯片等具体业务场景。
我一开始接触 Univer 的原因是客户要求“报表页面里可以点开一个在线 Excel,并且能够复用我们已有的登录权限体系”。当时的方案风险很高:如果要基于自研 Grid 去补公式引擎和协同编辑,保守估计要投入两个人力干三个月。换成 Univer 之后,大部分表格能力直接开箱即用,团队只需要把精力集中在业务侧的数据对接和权限控制上。它解决了“嵌入”这件事最繁琐的部分,这也是我推荐大家关注它的主要原因。
需要明确的是,Univer 并不是要把自己做成一个“公开的在线 Office 网站”,它的核心形态是一个 SDK。你可以把它塞进任意前端项目,让它成为你产品里的一个页面、一个弹窗,或者某个复杂工作流里的一个区块。这种“嵌入式”定位对 SaaS 厂商和内部系统特别友好,因为不需要把一个完整的办公平台搬过来,只需要把需要的能力集成进去。
1.2 核心能力与适合谁来用
就当前版本来看,Univer 已经覆盖了这些高频能力:
- 电子表格:单元格编辑、合并拆分、行列操作、筛选排序、条件格式、冻结窗格、图表、数据透视表、公式函数等。
- 多文档类型:除了 Sheet,还支持 Document 和 Slide,底层模型统一,可以互相跳转和引用。
- 协同编辑:支持通过协作命令广播实现多人同时编辑,配合服务端可以实现在线文档一样的效果。
- 数据交换:支持常见的 Excel 文件导入导出、CSV 处理,也可以通过 API 直接读取和写入单元格数据。
- 插件机制:提供依赖注入容器,开发者可以钩入工具栏、右键菜单、快捷键,甚至自定义渲染图层。
这些能力决定了它的适用人群。第一类是企业应用前端工程师,需要快速把在线表格集成到现有系统中;第二类是有定制化要求的 ToB 产品团队,希望通过插件扩展出私有字段、审批按钮或业务快捷键;第三类是在线协同工具团队,可以在 Univer 基础上搭建自己的在线文档协作产品。至于个人开发者,拿它做个数据展示工具、表格编辑器原型也完全可行。
但有一点要泼冷水:如果你需要的是完整兼容 Excel VBA 宏、复杂图表模板、桌面级打印,或者要求所有 Excel 细节都百分百还原,那 Univer 现阶段还不适合。它追求的是“覆盖绝大多数日常编辑场景”,而不是做一滴水不漏的 Office 克隆。选型的时候把这个预期摆正,后面会少很多抱怨。
2. 技术架构与核心设计拆解
2.1 为什么用 Canvas 而不是 DOM 渲染
表格场景最怕的就是渲染性能崩掉。普通网页表格如果渲染一万行真实 DOM,浏览器光是布局和重绘就能卡到没法看。Univer 的核心渲染引擎选择了 Canvas 绘制,单元格、选区、网格线、图表都绘制在同一块画布上,滚动、缩放、选区高亮都通过局部重绘完成,配合按需绘制,可以支撑远大于 DOM 方案的数据量。
这种设计在架构上和游戏引擎有些类似:有一个独立的 Render 引擎,维护场景树、图层、变换矩阵;业务层只负责生成绘制指令,不直接操作 DOM。带来的好处是性能可控,但代价也很直接——开发者在控制台里没法像查 DOM 一样定位问题,必须学会看 Canvas 图层和引擎调用栈。你在集成自定义插件时如果要做绘制层增强,需要按引擎的坐标系和变换逻辑来写,不能再用“添加一个 div 然后定位到单元格上面”的思路。
另外要注意 Canvas 渲染对文字和字体的处理。Canvas 原生绘制文本在不同操作系统、不同缩放比例下会有一点字体渲染差异;Univer 做了一些字体度量方面的适配,但如果你在项目里用了特殊字体、偏旁复杂的字体,或者需要精确打印预览,一定记得在目标环境里回归测试。特别是 Windows 和 macOS 上的显示效果可能会有细微差别,这是 Canvas 方案绕不开的细节。
2.2 数据模型、命令流与公式引擎
Univer 的数据模型从结构上可以理解为“工作簿 -> 工作表 -> 单元格”三层结构。每个工作簿包含多个工作表,每个工作表保存行列数量、单元格内容、样式、合并区域等信息。整个数据状态被封装在核心服务中,外部不能随便改动一个单元格就算数,而是通过命令系统来完成操作。
命令系统是 Univer 非常关键的设计。你在界面上做的每一次编辑,比如输入值、合并单元格、添加工作表,都会被封装成一个 Command 对象。这个对象不只是“记录操作”,它还承担了撤销/重做、协同广播、历史记录等多重职责。因为所有操作都走同一条链路,多人协同时的冲突处理就变得相对可控:不是同步最终状态,而是同步操作命令,服务端负责合并和排序。
公式引擎方面,Univer 内置了比较完整的公式解析和计算能力。它支持跨表引用、范围求和、条件判断、数组公式、错误值传播等常用逻辑。底层通过计算链和依赖关系来管理哪些单元格需要重新计算,而不是每次任何单元格变化都把整个表格算一遍。这对于表格性能很重要,尤其在上千条数据的业务表中,能不能精确地在局部重算,直接决定了用户输入的跟手程度。
由于数据模型和命令流是独立的,公式引擎不直接操作渲染层,这也让它具备了一定的端云复用可能性。比如服务端需要计算某个复杂公式,理论上可以把公式引擎跑在 Node.js 环境里,实现前后端计算一致性。这类能力在实际项目中很值钱,很多在线表格产品恰恰是卡在“前端算的结果和后端算的结果不一样”这种问题上。
2.3 插件机制与依赖注入
Univer 的插件架构参考了现代编辑器常见做法,通过依赖注入容器统一管理服务。简单说,你可以把 Univer 想象成一个小型操作系统,核心只提供最基本的运行时,其他功能都以插件形式注册进来。需要表格?加 Sheet 插件;需要公式?加 Formula 插件;需要 UI 工具栏按钮?加 UI 插件并扩展配置。
在实际开发中,插件机制能帮我们做到两件事。第一是可裁剪,不需要的功能不打包,减少最终 bundle 体积;第二是可扩展,业务定制逻辑尽量放在插件层,不去改动 Univer 核心包,这样将来升级 Univer 版本时,只需要让插件跟随新 API 做适配,避免把业务代码和框架代码纠缠在一起。
依赖注入的写法需要一点点学习成本。首次接触时,可能不理解“为什么不能直接全局 import 一个单例”。其实是为了避免多个文档同时存在时服务状态互相污染。Univer 允许在同一页面创建多个 Univer 实例,每个实例都有自己的上下文和插件集合,依赖注入让这些实例之间的数据隔离变得自然。如果绕过容器去写死全局状态,多实例场景会立刻翻车。
3. 快速集成 Univer 的实操路径
3.1 环境准备与工程初始化
我建议采用 Vite + React + TypeScript 的组合来快速实践。这个组合的好处是开发启动快、类型提示完整,而且 Univer 本身的 TypeScript 定义完善,配合起来很顺手。先创建一个空项目,然后安装 Univer 相关依赖。你可以在终端里执行下面的命令:
npm create vite@latest univer-demo -- --template react-ts cd univer-demo npm install装完基础工程后,再安装 Univer 核心包。不同版本的包名会有差异,建议以官方文档为准。这里我以当时接触的版本为例,核心依赖一般包括:
npm install @univerjs/core @univerjs/sheets @univerjs/ui npm install @univerjs/sheets-ui @univerjs/sheets-formula @univerjs/sheets-numfmt安装之后不要着急写业务代码,先检查 package.json 中这些包的主版本号是否一致。Univer 的包之间耦合很紧,版本不一致经常会出现运行时诡异报错,比如“Service not found”或“Plugin is not registered”。统一版本是集成时最容易踩的坑。
3.2 在 React 组件里嵌入 Univer
Univer 的初始化核心其实是两件事:创建 Univer 实例、在实例上注册插件,然后把实例挂载到一个有尺寸的 DOM 容器上。下面是一段简化版 React 组件代码,演示如何挂载表格:
import { useEffect, useRef } from 'react'; import { Univer } from '@univerjs/core'; import { UniverSheetsPlugin } from '@univerjs/sheets'; import { UniverUIPlugin } from '@univerjs/ui'; export default function UniverDemo() { const containerRef = useRef<HTMLDivElement>(null); useEffect(() => { if (!containerRef.current) return; const univer = new Univer(); univer.addPlugin(UniverSheetsPlugin); univer.addPlugin(UniverUIPlugin, { container: containerRef.current, locale: 'zhCN', }); return () => { univer.dispose(); }; }, []); return <div ref={containerRef} style={{ width: '100%', height: '600px' }} />; }有几个细节需要特别说明。容器元素必须显式设置宽高,如果高度是自动撑开或者为 0,Univer 的 Canvas 渲染层会把内容绘制到看不到的区域,表现就是白屏。这个问题非常容易出现在 Flex 布局里,建议先写死 height 验证,确认渲染正常后再改成自适应逻辑。
useEffect 里创建实例,清理函数里调用dispose(),这个习惯很重要。开发环境热更新时,组件卸载后如果没有销毁实例,Univer 会在页面里留下多余的监听器和 Canvas 画布,再次初始化时可能出现多个实例叠加,表现为事件错乱或内存膨胀。养成随手 dispose 的习惯,能省掉很多排查时间。
3.3 配置工作簿初始结构
默认的 Univer 实例创建出来后,是一个包含空白工作表的表格。但是在实际业务中,通常希望初始状态包含指定行数列数、工作表名称,甚至预设一部分单元格数据。这可以通过创建工作簿配置来实现。
Univer 的初始化配置通常是在 UI 插件或数据初始化时传入。比如创建多个工作表,设置每个工作表的行数和列数,以及初始单元格值:
const config = { workbook: { name: '业务报表', sheetOrder: ['Sheet1', 'Sheet2'], sheets: { Sheet1: { rowCount: 200, columnCount: 50, cellData: { 0: { 0: { v: '项目名称' }, 1: { v: '负责人' }, 2: { v: '进度' }, }, }, }, Sheet2: { rowCount: 100, columnCount: 30, }, }, }, };这样配置的好处是,用户打开页面时看到的已经不是一张完全空白的表格,而是带表头和基本结构的业务模板。值得注意的是,单元格数据里的v字段表示显示值和原始值,如果后面要写公式,通常还需要额外指定公式字段,具体字段名要参照你所用版本的类型定义。这个阶段一定要打开 TS 类型提示,按着类型定义来写,比自己脑补准确得多。
4. 协同编辑与数据交互实战
4.1 协同编辑逻辑与接入思路
Univer 把协同编辑做成了可接入的模块,而不是内置一个固定的实时服务。原因也好理解:每个团队的账号体系、网络架构、消息推送方式都不一样,框架层面把协议和服务端绑定死,反而限制落地。所以 Univer 的思路是:本地操作通过 Command 生成协作操作,应用层把操作发送到你的后端,后端合并后再广播给其他客户端。
在实际接入协同编辑时,你需要考虑的问题大致有三个。第一,命令的同步格式:Univer 生成的协同操作如何序列化、如何标记版本,都需要按照协作协议来封装。第二,服务端代理:可以先用 WebSocket 做一个透传式服务端,把客户端的操作转发给其他人,但要真正处理冲突、操作排序和版本向量,还是需要一个专门的状态服务。第三,覆盖冲突策略:两人同时改同一个单元格,以谁为准、如何合并,这些规则最好在业务层面提前定好。
我见过不少人以为“接了协同编辑就能直接做在线文档”,最后卡在服务端合并逻辑上。这里想提醒一句:Univer 把离线端的工作做得很到位,但服务端协同逻辑仍然需要投入一段时间去设计。如果产品只要求“多人能同时打开看数据”,不一定非要上实时协同,可以先做基于锁的编辑策略,降低成本。
4.2 通过 API 读写单元格自定义数据
除了用户手动手表,Univer 更常用的场景是让业务系统通过 API 自动填充数据。比如在后台上传 Excel 之后解析并写入表格,或者点击某个业务按钮后把后端数据填充到固定区域。这时候需要用到 Univer 的编程接口。
拿读取活动工作表里某个单元格的值为例,你可以这样做:
const workbook = univer.getActiveWorkbook(); const sheet = workbook?.getActiveSheet(); const cell = sheet?.getCell(2, 3); const rawValue = cell?.getValue();需要注意,API 返回的单元格对象包含的内容可能很丰富,包括显示值、原始值、公式、样式索引等。不要笼统地把对象打印出来就直接当普通 JSON 用,要先明确你要读的是用户输入值、计算后的显示值,还是存储用的原始值。不同场景应该选择不同字段,否则可能出现“界面上明明是 100,接口却拿到一个公式对象”的困惑。
写入数据的原理也类似。你可以通过工作簿 API 找到指定工作表,然后修改单元格对象并提交对应命令。实际操作时,我建议把批量写入封装成服务函数,统一处理行列偏移和坐标转换,避免业务代码里到处散落着魔数坐标。比如“第 1 行第 2 列”在用户界面和数据结构里的起点不同,封装一层“业务行号转内部坐标”的工具函数,会稳妥很多。
5. 常见问题与排查技巧实录
5.1 白屏和加载失败
我接入 Univer 时遇到的第一个问题就是白屏,后来排查发现是容器高度为 0。Univer 的渲染引擎确实会对容器尺寸做检测,但它不会像普通图片组件那样“自适应撑高”,而是把 Canvas 画布放在容器里,容器没有高度,画布绘制区域自然为 0。定位这个问题的方式很简单:打开 DevTools,查看容器元素是否真的有尺寸。如果容器的宽度高度都被压缩了,先修布局。
另一个导致白屏的常见原因是重复初始化。React 严格模式下,开发环境 useEffect 会执行两次,如果你没有正确清理,页面会创建两个 Univer 实例。在热更新时尤其容易出现“实例已经存在但业务状态被重置”的情况,表现就是页面出现空白残影或无法交互。遇到这种问题,优先检查是否有dispose()被调用,以及调用时机是否正确。
版本不匹配也可能导致初始化直接抛异常。Univer 的插件包之间依赖关系很强,如果从@univerjs/ui拿到的上下文和@univerjs/sheets期望的不一致,启动阶段会报错。排查方法是在项目里锁定所有 Univer 相关包的准确版本,不要用latest安装,用 lock 文件固定版本并定期统一升级。
5.2 公式和计算结果不对
使用公式时遇到最多的问题集中在三个方面:函数名中英文差异、单元格区域引用格式、异步重算时机。Univer 既能识别英文函数名,也能识别中文函数名,但如果你在代码 API 里写入公式,函数名格式必须与解析器严格的语法匹配。写错参数分隔符或者多了一个空格,公式可能不会报错,但结果会和你预期对不上。
再说一个重算时机的问题。有些操作会触发异步公式计算,界面上的值可能不是立即刷新。如果你的业务逻辑依赖“写入数据后马上读取计算结果”,最好监听公式计算完成的事件,或者在写入后主动刷新一次单元格数据,而不是直接setTimeout去读。别问我为什么知道,用固定延迟读取真的会在用户网络差时踩坑。
还有一个容易忽略的知识点:如果公式引用了整列,比如SUM(A:A),大数据量时计算性能会受影响。Univer 虽然做了局部计算优化,但整列引用会显著拉大计算范围。合理做法是将公式范围限定到实际数据区域,比如SUM(A2:A500),既提升计算速度,也避免空单元格被大量统计。
5.3 自定义插件开发时需要避开的坑
Univer 的插件机制很强大,但 API 稳定性现在还处于快速迭代期,不同版本之间可能有破坏性变更。我写插件时踩过最大的坑是“想当然地认为注册的 Controller 会被框架自动装配”。实际上,插件的启动生命周期有明确顺序,依赖的服务必须在它自己的插件里声明,不能在另一个插件里凭空拿到。
另一个技巧是尽量用公开的 API 和类型接口去扩充功能,避免直接 hack 内部状态。比如想在右键菜单中加入一个“导入业务数据”的按钮,应该通过菜单扩展点去注册,而不是手动往 DOM 里插入一个元素。前者虽然写起来要查文档,但升级版本后大概率还能继续用;后者可能两个小版本之后就被 Canvas 重绘逻辑覆盖掉。
如果你要在 Canvas 图层上做自定义绘制,不要忘记滚动偏移和坐标缩放。Univer 单元格坐标系和 Canvas 像素坐标系之间是有变换关系的,直接按照“第 5 行第 6 列”的坐标去画,结果很可能落在完全错误的位置。正确做法是先通过引擎提供的坐标转换 API 把单元格坐标转成当前视口坐标,再绘制辅助标识。
最后分享一个我自己的实践结论:不管团队使用 Univer 的哪个功能模块,都一定要从第一天就把版本锁死,并把选型验证阶段的源码、Demo、文档链接保存成团队知识库。不要凭网上零散文章拼凑集成步骤,因为 Univer 迭代节奏不慢,照着半年旧教程很容易装出一个无法运行的组合。
我在实际接入过程中最深的一点体会是:Univer 已经帮你把“在线表格”这座大山移掉了大半,但剩下的那部分——业务模型设计、协同服务、定制化交互、版本升级治理——依然是需要自己认真对待的深水区。它不是一个“装上就完事”的插件,而是一个需要你静下心来理解其架构的伙伴。把它用扎实了,你会得到一套远超传统表格组件的办公能力底座;反过来,如果只是浮于表面抄一段初始化代码,遇到问题时会不知道从哪查起。希望这篇拆解能让你少走一些弯路,也能更清楚地判断 Univer 适不适合你的下一个项目。