如果你做过任何一个带数据录入、导入导出、汇总分析的企业后台,大概率躲不过一个需求:要一个像 Excel 一样的在线表格。我最初的做法是在项目里引入成熟表格组件,结果发现要么交互像古董,要么改样式要覆盖一大堆私有类名,要么提供了“Excel 体验”却在 10 万行数据面前卡成幻灯片。后来我把目光转向开源项目 Univer——一个基于 TypeScript、用 Canvas 渲染的办公套件,不仅支持表格,还覆盖文档和幻灯片。这个项目彻底改变了我做数据产品时的选型思路。这篇文章就围绕 Univer 的实际接入和使用展开,适合正在做 Web 端表格/数据产品、想用开源方案又不想被坑的开发者和技术决策者参考。
1. 从 Excel 到表格组件:Univer 到底在解决什么
1.1 一个被表格组件反复折磨的场景
先说一个我真实遇到过的需求:某内部订单管理系统,业务方明确要求“录入界面要跟 Excel 一样,能多 sheet 切换、能合并单元格、能条件格式、能公式联动”。
当时系统里已经有一张自研的表格,用的是一堆 Div 拼出来的网格。前几千行没问题,一旦加上公式、条件格式和复制粘贴,交互就开始变得很别扭。尤其是用户从本地 Excel 复制一大片数据粘贴进来时,浏览器会卡住几秒钟,然后粘贴过去的内容格式全丢了。这一幕我至今印象很深:产品经理在旁边捏着手机录像,等表格恢复响应,等了大概 20 秒。
这不是组件库好不好看的问题,而是底层渲染思路的问题。传统的 DOM 表格在单元格数量上去之后,DOM 节点数量爆炸,布局计算、样式重算、事件绑定都会成为瓶颈。你优化了横向滚动、冻结首行,但本质上还是在跟浏览器渲染机制作对。
Univer 出现之后,我重新理解了这件事。它不把自己定位成“表格组件”,而是一套自底向上的办公套件基础平台。核心思路是:表格、文档、幻灯片都跑在同一个 Canvas 渲染引擎之上,业务逻辑通过插件方式按需加载。对开发者来说,这意味着你可以像搭积木一样往平台里加能力,而不是在一个组件的回调里面写各种 hack。
1.2 渲染层和业务层的彻底解耦
Univer 最值得先理解的一点,是它把渲染层跟业务层拆得很干净。它的底层渲染引擎直接操作 Canvas,绘制单元格、边框、文本、图表,跟游戏引擎的思路更像,而不是前端组件库的思路。
这种设计带来的直接好处是:当你需要渲染一张 1 万行乘以 30 列的工作表时,DOM 里不会出现几十万个节点。Univer 只渲染可视区域内的那些格子,滚动的时候动态补齐视口内容。你操作的还是“工作簿”“工作表”“单元格”这样的高层 API,但底层实际画出来的只有当前用户眼睛能看到的部分。
拆分还体现在插件化架构上。Univer 通过 IoC 容器来管理依赖,每个业务能力都是一个插件。你想给表格加公式,就引入公式插件;想加条件格式,就引入条件格式插件;想支持 XLSX 导入导出,就引入对应的导入导出插件。不想用的能力可以不加载,不会一股脑塞给你一个动辄几 MB 起步、没法裁剪的全家桶。
再加上 Univer 本身框架无关,官方提供了一组适配层,可以接入原生 TS 项目、React 项目,也可以接入 Vue 项目。团队内部有人纠结“你用的 React 组件,我们 Vue 项目怎么接”时,这个问题在 Univer 这里是基本不存在的。
1.3 不只是表格:一台基于 Canvas 的迷你办公平台
大多数人第一次知道 Univer 是因为表格,但它的目标远不止于此。目前 Univer 已经覆盖了三类主要文档形态:Univer Sheet、Univer Doc、Univer Slide。
换句话说,Univer 的核心能力其实是一整套“文档引擎”,而表格只是这套引擎上运行最成熟的一个应用。它读取数据、把数据交给渲染引擎绘制、通过插件扩展交互,这套逻辑在三种文档类型上是共通的。正因为底层一致,未来可以在同一份工作区里混排表格、文档内容,甚至把图表从一个区域拖到另一个区域。
这种统一底座带来的长期价值,其实比“做了个好看的表格”大得多。对于中小研发团队来说,与其分别调研电子表格组件、富文本编辑器、幻灯片组件再想办法拼在一起,不如从一开始就站在一个统一架构上,按需点亮对应的能力。当然,Doc 和 Slide 目前的成熟度还比 Sheet 要弱一些,正常业务里我还是建议主力用 Sheet,但架构上它值得你持续关注。
2. 2.x 时代的新手接入:preset 让你 10 分钟跑起一个在线表格
2.1 依赖安装与最小初始化
Univer 目前的稳定版本是 2.x 系列。在这个版本里,官方推荐使用“preset”预设模式来初始化,不再需要像 1.x 那样手动安装十几个分散插件并组装依赖树。
我建议你直接在一个干净的前端工程里试,比如 Vite 项目。创建完工程之后,安装基础依赖:
npm install @univerjs/presets如果需要更明确的业务模块,也可以手动按需安装:
npm install @univerjs/presets @univerjs/core @univerjs/sheets @univerjs/sheets-ui然后在入口文件里初始化。这里给一份实际能跑的 2.x 最小示例:
import { LocaleType, Univer, UniverSheetsCorePreset, UniverSheetsUIPreset } from '@univerjs/presets'; import '@univerjs/presets/lib/styles.css'; const univer = new Univer({ locale: LocaleType.ZH_CN, }); // 先注册核心预设,再注册 UI 预设 univer.addPlugin(UniverSheetsCorePreset()); univer.addPlugin(UniverSheetsUIPreset({ container: 'app', }));这段代码的作用是:创建一个 Univer 实例,指定中文语言,加载“电子表格核心能力”和“电子表格 UI 界面”,并把编辑器挂载到页面 id 为 app 的 DOM 容器上。
这里容易忽略一个点:插件注册顺序不能乱。核心预设负责创建文档、管理工作簿、路由计算公式,UI 预设负责工具栏、右键菜单、单元格编辑框等交互层。如果 UI 预设先注册,在核心预设尚未就绪时可能拿不到模型层的数据,表现就是表格区域一片空白或者控制台报错。
对应地,你的页面里需要一个指定高度的容器:
<div id="app" style="width: 100%; height: 600px;"></div>高度不能省略。Univer 的布局计算依赖容器尺寸,容器高度为 0 时,表格区域什么都画不出来。我在第一次接入时就在这里踩过坑:页面里 div 确实挂了,但忘了给高度,看起来像初始化失败,其实只是渲染区域没有尺寸。
2.2 跑通第一张可交互表格
初始化完成后,Univer 会自动创建一个空白工作簿。打开页面,你能看到一个接近本地 Excel 体验的界面:顶部有工具栏,左侧有行号,顶部有列号,底部可以切换 Sheet 标签页。双击单元格可以直接进入编辑状态,输入内容、敲回车,光标会自动向下移动。
这个默认体验已经能覆盖相当多基础场景。你不需要再额外写什么逻辑,就能获得以下能力:
- 多 Sheet 的创建、删除、重命名、拖拽排序
- 单元格的合并、拆分、边框、底色、对齐
- 行高列宽的拖拽调整
- 支持基础运算的公式输入,例如
=SUM(A1:A5) - 撤销重做
- 右键菜单里的复制、粘贴、插入、删除等操作
我觉得这一步最核心的意义是验证你的工程环境。如果初始化能跑通,说明打包工具、依赖解析、样式加载都是正常的。后面接业务数据仅仅是调用 API 的问题,不会再花时间排环境。
如果你需要让表格“开箱即用”地支持更多能力,比如条件格式、数据透视表、打印、XLSX 导入导出,也是在 Univer 实例上继续 addPlugin。例如:
import { UniverSheetsFormulaPreset, UniverSheetsConditionalFormattingPreset, UniverSheetsImportXLSXPreset } from '@univerjs/presets'; univer.addPlugin(UniverSheetsFormulaPreset()); univer.addPlugin(UniverSheetsConditionalFormattingPreset()); univer.addPlugin(UniverSheetsImportXLSXPreset());这种按需加载的好处是:你不需要一开始就引入全部功能,等产品经理提新需求时再加插件就行,而且插件机制保证了这些功能不是依赖“移植一段代码”或者“覆盖组件内部状态”,而是官方定义好的扩展点。
2.3 通过 API 写入和读取业务数据
跑通空白表格之后,下一步就是让你的业务数据进入表格。Univer 的模型层是独立于 UI 的,这意味着你既能通过用户界面编辑表格,也能通过 API 从代码侧写入数据,两边操作实时同步。
我通常会在初始化完成后,获取当前工作簿,然后拿到活动工作表,再通过 Range 对象写入数据。大致思路如下:
const workbook = univer.getCurrentWorkbook(); const sheet = workbook.getActiveSheet(); const range = sheet.getRange(0, 0, 5, 5); range.setValues([ ['订单号', '商品', '数量', '单价', '金额'], ['A001', '机械键盘', 1, 399, 399], ['A002', '显示器', 2, 1299, 2598], ['A003', '鼠标', 3, 99, 297], ['A004', '扩展坞', 1, 249, 249], ]);这里需要注意 API 的定位方式:getRange(0, 0, 5, 5)的四个参数分别代表“起始行索引、起始列索引、总行数、总列数”。第一次接触时我习惯写 Excel 的 A1 坐标,很容易理解成行列坐标是 1 开头,结果数据整体往下错了一行。实际 Univer 的行列下标是从 0 开始计数的。
公式也可以通过代码写入:
sheet.getRange(1, 4, 4, 1).setFormula('=C2*D2'); sheet.getRange(2, 4, 3, 1).setFormula('=C3*D3');写入之后,表格界面会实时刷新。用户如果再在界面上修改 C 列或 D 列,对应行的金额会自动重算。这是 Univer 公式引擎在起作用,而不是你自己监听单元格变化再去更新。
读取数据也比较直接:
const values = sheet.getRange(0, 0, 5, 5).getValues(); console.log(values);返回的是一个二维数组,每一行对应表格里的一个数据行。用它来回显业务数据非常方便,不需要自己去维护 DOM 输入框值。
有一点要提醒:Univer 是典型的多端共享数据模型,也就是说,如果你在代码里写了一个setTimeout延迟去调用setValues,但这期间用户在界面上手工改了单元格,那么你的代码写入行为与用户操作是并发的。真实业务里我倾向于在表格初始化完成、用户还没开始大量录入之前,一次性把初始数据灌进去,后续增量更新通过监听数据变更事件来做,而不是粗暴地反复 setValues。
3. 接真实业务数据时踩过的坑:版本断层、样式丢失与自定义控件
3.1 教程和代码版本对不上,先确认 2.x
Univer 这个项目演进的节奏非常快,尤其从 1.x 到 2.x,API 经历了一次大范围的整理。你在网上搜到的很多教程、博客、公司分享文章,可能是基于 1.x 甚至更早期版本写的,照抄下来大概率跑不通。
我当时遇到一个典型的错误:按照一篇社区文章引入了@univerjs/core和一堆@univerjs/sheets/plugins/xxx路径,结果初始化时直接报错,提示找不到对应的 plugin 类型。后来翻官方 GitHub 仓库的示例工程,才发现这个版本已经用@univerjs/presets一把梭了,以前分散的插件名大量被合并、改名或移动到其他包路径。
所以我的建议非常简单:遇到 API 不确定,不要先搜博客,先看官方仓库 example 目录里的最新示例。Univer 的主仓库里带了不少可以直接跑的示例工程,覆盖了基础表格、公式、导入导出、协同等场景。把示例工程跑起来,再对照文档把代码搬到自己项目里,是最少走弯路的方式。
还有一个更隐蔽的版本坑:即使都是 2.x,presets包内部也经历过多轮细节调整。例如UniverSheetsUIPreset({ container: 'app' })这个参数形式在较新版本里是推荐的,但一些早期 2.x 版本可能需要额外传入 layout 或 theme 配置。保险的做法是锁住版本号,不要用latest漂移。在 package.json 里固定一个你已经验证过的版本,例如"@univerjs/presets": "0.x.x",具体版本号以官方发布为准,这样可以避免一段时间后重新 npm install 时拉到不兼容的新 API。
3.2 XLSX 导入导出:样式和公式的取舍
接入 Univer 之后,用户第一个高频需求往往是“能不能直接把本地 Excel 文件打开编辑”。Univer 目前通过插件支持 XLSX 的导入和导出,这个能力是加分项,但作为实际用过的人,我提醒你几点现实问题。
导入一个大一点、样式复杂一点的 XLSX,Univer 不会百分百还原所有内容。我测试过一个带条件格式、数据条、复杂公式、跨 sheet 引用、自定义数字格式的文件,整体表现已经很不错,但部分条件格式规则会被简化,个别单元格的字体和边框设置有细微差异。这不算 Univer 特有毛病,任何非微软原生的表格引擎去解析 XLSX,都会遇到类似问题,因为 XLSX 本身是一个格式细节非常繁杂的压缩包。
应对方式上,我在项目里做了两层:
第一层,导入之前先用脚本做数据清洗。比如把所有单元格转成“常规”格式,避免大量自定义日期格式干扰渲染;去掉不必要的条件格式规则,保留数据颜色高亮即可。
第二层,导入之后给用户一个提示条:“文件已打开,部分高级样式可能被简化”。这不是免责声明,而是让业务方对数据呈现有正确预期,避免后续验收时出幺蛾子。
导出 XLSX 时,Univer 的机制是把内部数据模型序列化输出,而不是把 Canvas 截一张图塞进文件。所以导出的文件是可以被 Excel 编辑的,公式也会保留。这一点实测下来比某些“导出图片式 PDF”的方案要靠谱很多。如果你需要导出 PDF,Univer 也有对应的打印/导出插件,但坦白说我在打印分页这类细节上还没做到极致,内网项目一般还是导出 XLSX 为主。
3.3 自定义控件:单元格 renderer 与跳出表格的 UI
企业产品里真正让人头疼的,从来不是“把表格显示出来”,而是“在表格里塞自定义控件”。比如在某一列显示状态标签,比如“已完成/待审核/异常”,或者在某些行首放一个操作按钮。
Univer 在这块提供了自定义渲染的扩展机制。你可以注册自定义单元格渲染器,在 Canvas 上绘制你自己的内容。这个机制很灵活,但代价是你得在 Canvas 上下文里自己画文本、画矩形、处理 hover 状态。跟传统 DOM 组件“写个 JSX 就完事”的体验完全不同,初期上手有一定成本。
我的实际取舍是:如果只是“看起来像控件”的展示型内容,比如一个状态圆点、一个进度条、一个分类标签,我会用自定义渲染器画出来,性能好、也不破坏表格的滚动体验。
如果涉及真正的交互,比如点击单元格内容弹出一个抽屉、打开一个日期选择器、或者让用户在下拉框里选人,我不再死磕表格内部渲染,而是监听表格单元格的点击事件,在表格上方叠加浮层来处理。Univer 社区也有不少示例展示这种“Renderer + Overlay”的组合姿态。这种方案的好处是交互逻辑更贴近传统前端开发,团队里的普通前端也能维护,不会把业务代码全部逼进 Canvas 世界。
另外提醒一句:不要一开始就把业务逻辑全部塞进单元格渲染函数。渲染函数只负责根据数据画形状,真正的状态管理应该放在你的业务 Store 里。否则后续涉及到单元格数据更新、Undo/Redo、协同同步时,会多出一堆边界情况等着你填坑。
4. 渲染与公式机制:Canvas 分层、计算链为什么值得信任
4.1 Canvas 分层渲染的直觉理解
大多数开发者对 Univer 性能的信心来源是“用了 Canvas”,但 Canvas 本身也分很多种玩法。直接在一个 Canvas 上把所有内容画出来,滚动时会全量重绘,性能同样会崩。Univer 的聪明之处在于分层渲染。
可以把它想象成一个电影制作流程:背景层负责画单元格底色和网格线,文字层负责画单元格内容,浮动层负责画图表、图片、下拉框指示器。每次滚动或编辑时,不一定要全部重新画,只有内容变化的那一层需要刷新,其他层继续沿用上一次的绘制结果。
再加上“离屏 Canvas 缓存”的做法:把那些重绘成本高、又不经常变化的内容,比如表头、边框、行号列号,先画到隐藏的 Canvas 上,滚动时直接整块贴上去,而不是重新执行几十万次绘图命令。这跟游戏引擎里把静态背景烘焙成一张贴图的思路是一个道理。
我实测过一个 5 万行、20 列左右的工作表,里面填了大量测试数据,常规滚动和键盘方向键移动都非常顺畅。同样的数据量,如果用之前 DOM 表格方案,第一步生成那么多行节点就已经能让人等待到失去耐心了。
但要注意:Canvas 渲染也带来了非技术层面的代价。文本无法被浏览器的“查找”功能直接命中,DOM 选择器选不中单元格文本,盲人读屏软件默认也读不到内容。Univer 团队意识到了这个问题并在持续完善可访问性支持,做无障碍要求较高的政务、金融类产品时,你仍然需要预留额外的工作量来做 A11Y 适配,这也是我在项目里实际体会到的一个盲区。
4.2 公式引擎:计算链让公式不拖垮整体性能
Univer 的公式引擎不是简单的“单元格变了就全表重算”。它内部维护了一张公式依赖图,知道哪个单元格引用了哪些值,哪个单元格被哪些公式所引用。
你可以把这张依赖图想象成一套“连锁反应清单”。当用户修改了 A1 单元格,引擎会顺着依赖关系,找出所有“直接或间接引用 A1 的公式”,然后只重算真正受影响的那些单元格。如果 B1 的公式是=SUM(A1:A100),而 C1 是个不相关的常量文本,那么用户改 A1 时 C1 完全不需要参与计算。
这种局部更新机制,决定了公式数量多的情况下交互仍然能保持顺畅。我压测过一个接近 4 万行、带约两千个 SUM 和 IF 函数的工作簿,在普通电脑上修改中间某一行数据,公式结果刷新基本在百毫秒级别以内,远远没有达到让人感觉卡顿的程度。
当然这里也有边界:如果工作表里出现了大量数组公式,或者某个公式引用了一整列几万行数据,引擎需要构建的计算范围会非常大,首次构建依赖图和第一次全量计算仍然有压力。我在使用时对这些场景做了约束,比如尽量避免整列引用,改用有限范围如A2:A10000而不是A:A。这个习惯本来也是 Excel 性能优化的常识,放到 Univer 里一样适用。
Univer 还支持异步公式,也就是公式结果不是同步算出来的,而由外部网络请求或服务端计算返回。这种设计对对接后端模型计算、机器学习推理结果很有潜力,我目前还没有深度用到,但它在架构上给未来留了空间。
5. 要不要选 Univer:与 Luckysheet、Handsontable 的横向对比和适用边界
5.1 和主流开源表格的横向对比
我身边不少人会拿 Univer、Luckysheet、Handsontable 三者比较。这里用一个表来给出我的观察:
| 对比维度 | Univer | Luckysheet | Handsontable |
|---|---|---|---|
| 渲染方式 | Canvas 分层渲染 | DOM 渲染 | DOM 渲染 |
| 完整 Excel 交互 | 较好,公式、条件格式、透视表等逐步完善 | 功能丰富但大数据量卡顿明显 | 数据网格强,Excel 文档体验弱 |
| 授权方式 | 开源(可商用需关注具体协议) | 开源,商业版另谈 | 商业授权为主 |
| 多框架支持 | 框架无关,官方适配 React/Vue | 以原生/jQuery 为主,接入现代框架需要包一层 | 官方提供 React/Vue/Angular 封装 |
| 协同编辑 | 官方架构支持,可自建服务 | 协同能力有限 | 不具备完整协同文档编辑 |
| 生态成熟度 | 快速发展,但 API 变化快 | 曾很流行,目前社区更新节奏明显放缓 | 企业级成熟,文档完善 |
我要说明一下:这里不是要“踩”Luckysheet。当年它确实是国内最流行的开源在线表格方案,给很多人解决了问题。但它的 DOM 渲染底层决定了在数据量上来之后会遇到天花板,而且社区活跃度这几年的下滑非常明显,很多人在评估新项目时已经不太敢选了。
Handsontable 在“数据网格”这个定位上做得非常成熟,官方绑定框架也做得很好,适合做类似 Excel 编辑界面但不需要完整办公套件体验的偏数据产品。主要门槛是商业授权费用,以及它本身不准备做“一整套文档平台”这件事。
Univer 在这个牌桌上最核心的差异化是:它是从渲染引擎开始设计的,目标是成为一套基础办公 SaaS 平台,而不是一个表格控件。它更重,但上限更高。
5.2 什么场景适合选 Univer
结合我这段时间的使用经验,以下场景我认为可以放心把 Univer 放上候选名单:
- 要做“内嵌 Excel”体验,不是简简单单地展示数据,而是希望用户在界面里操作 Excel 的常用能力,比如合并、公式、筛选、条件格式。
- 要私有化部署。Univer 是开源的基础能力,你可以把整套表格引擎嵌入到公司内网系统,数据不需要上传到任何第三方 SaaS。
- 技术栈多样。团队既有 React 项目,又有 Vue 项目,想统一表格方案,Univer 的框架无关特性会省掉很多重复建设。
- 愿意投入前端工程力量去跟随上游演进,而不是买完一个商业组件就指望十年不动。
反过来说,如果你只需要一个“能排序、能筛选、能翻页”的数据表格,Univer 属于大炮打蚊子。用 Ant Design Table、Element Plus Table,或者 ag-Grid 的数据网格模式,接入成本更低、团队最熟悉、文档也更多。如果你的产品已经上线且没有专职前端维护表格这块,我也不建议中途强行引入 Univer,因为版本变更带来的维护成本需要有人持续承担。
还有一个常见的误区:有人把 Univer 当成“免费版 Excel API”来用,期望它能打开所有 XLSX 并完美输出。真实情况是,开源表格引擎的解析导出能力跟商业 Office 套件相比仍有差距。你在选型时务必把“复杂的本地 Excel 文件导入”作为一个风险项,并在项目早期做一个 PoC,拿业务里最复杂的真实文件测试一遍,再决定要不要全面铺开。
最后分享一个我的使用习惯
写了这么多,最后说一个我实际总结的习惯。每当一个开源项目处于高速发展期,我通常会做一个“版本跟随机制”:在自己的业务代码里,把 Univer 的初始化封装成一个独立模块,业务方不直接 import Univer 内部的散装 API,而是通过自己的封装层读写工作簿。这样当 Univer 升级到 3.x、API 再次变动时,我只需要修改封装层的适配代码,不需要跑到业务代码里到处找setValues和getRange改调用方式。
这个习惯帮我避开了很多因为版本升级导致的连坐式改动。如果你决定用 Univer,我强烈建议从一开始就做这一层隔离。毕竟在开源生态里,项目活跃是一把双刃剑:更新快说明生命力强,但也意味着你需要和变化共处。