☰
在线表格选型:Univer开源SDK的Excel兼容与协同落地记录
2026/11/4 5:44:58 网站建设 项目流程

上个月我们产品要做在线表格功能,产品经理甩给我一个 Excel 文件,说“就照着这个做,打开别变形就行”。我搜了一圈“univer在线”,发现不少人在讨论这个开源工作簿项目,又去 GitHub 翻了 issues 和示例,折腾两周之后基本把核心链路跑通了。如果你也在评估在线表格的选型,或者已经决定用 univer 但不知道怎么落地,这篇记录应该能帮你省掉不少弯路。

先说结论:univer 不是简单封装一个表格组件,它本身是一套“办公套件 SDK”,表格只是它当前最成熟的模块。它底层用 Canvas 做渲染,核心数据模型完全自研,命令体系贯穿所有操作,所以它在 Excel 兼容性、协同编辑、二次开发这几个层面上,和传统的纯前端表格库是两种思路。下面我按从选型到跑通再到踩坑的顺序,把完整过程写出来。

1. 为什么我会盯上 univer:被 Excel 兼容性逼出来的选型

1.1 最初的需求到底有多麻烦

我们的场景很常见:用户在系统里维护数据,需要能像打开本地 Excel 那样直接查看和编辑一张复杂的报表。复杂到什么程度呢?表头有合并单元格,有跨行跨列的标题区域,有自定义数字格式,有下拉校验,有条件格式,有几十个公式单元格,有些 Sheet 里的数据量能到几十万行。产品的要求很朴素:上传一个 xlsx,页面打开别乱,改完再导出回去,Excel 里显示的样式和公式不能被破坏。

这个需求看起来简单,实际做起来非常恶心。大多数前端表格组件在“简单表格”场景下都很能打,一旦涉及到合并单元格、条件格式、公式链、数字格式的还原,就开始各种打折。很多组件能展示数据,但导出后的 xlsx 已经不是原来那个文件了,公式丢了、样式塌了、批注没了。这也是我一开始把目光投向 univer 的最大原因:它想要做到的不是“网页里画一个表格”,而是“在网页里复刻一套 Excel 的编辑能力”。

1.2 我把主流方案都过了一圈

在做决定之前,我把市面上能评估的方案都过了一遍,这里直接给大家一个对比结论:

方案渲染方式Excel 打开还原度协同编辑商用授权社区活跃度
LuckysheetCanvas中上,样式细节会丢有官方方案但配置重MIT更新缓慢,接近停摆
x-spreadsheetCanvas中等,公式和格式支持有限无官方协同MIT基本不更新
HandsontableDOM中上,但与 Excel 文件双向转换较弱需额外服务商业授权文档好但社区讨论少
SheetJS后端/前端解析仅解析,不负责渲染无Apache/商业双轨活跃但方向偏解析
univerCanvas高,目标就是 Excel 兼容官方有协同方案MIT活跃,迭代快

我当时把其中一个复杂表格分别塞进这几款组件试跑,结果很能说明问题。Luckysheet 打开后合并单元格和数字格式还凑合,但在公式较少、条件格式有多个优先级时会渲染错乱;x-spreadsheet 打开稍微规整点的表都费劲;Handsontable 编辑交互不错,但要把现有 Excel 样式完整还原,得自己写大量映射代码。只有 univer 打开文件后的观感最接近 Excel 原版,公式也能正常计算、条件格式的优先级能保留下来。

1.3 univer 真正打动我的三个点

首先是架构。univer 把所有操作做成 Command,外部可以监听、拦截、重放,这意味着做操作日志、协同广播、撤销重做可以共用一套链路,不会像传统组件那样每个操作都要单独封装。

其次是公式引擎。univer 的公式不是在表格层用字符串解析糊弄过去的,它有独立的 engine-formula 模块,支持大多数 Excel 函数的解析和计算,跨 Sheet 引用也做了处理。对于“Excel 打开别乱”的需求来说,这一点是决定性的。

最后是权限和扩展方式。univer 用插件化注册,表格、公式、数字格式、条件格式都是独立插件,我可以按需加载,而不是一次性塞进来一个几百 KB 且无法裁剪的巨石组件。

2. 环境准备与首次启动:从 npm 包到在线表格跑通

2.1 先把依赖装干净

univer 拆得很细,第一次接触的人容易在“到底该装哪几个包”上卡住。我这里基于当时我使用的版本,给一个最精简的安装清单,作用是跑通一个能显示、能编辑、能算公式的表格实例:

npm install @univerjs/core @univerjs/sheets @univerjs/sheets-ui @univerjs/sheets-formula @univerjs/engine-formula @univerjs/design @univerjs/ui

这里的包名对应几个关键模块:core 负责数据模型和命令总线,sheets 提供 Sheet 相关数据与操作逻辑,sheets-ui 是表格界面的渲染层,sheets-formula 和 engine-formula 组合起来提供公式引擎,design 和 ui 提供主题和基础 UI 组件。

有一点必须提醒:univer 迭代速度很快,不同版本的包名和初始化 API 可能不一样。你装完依赖后,先去 node_modules 里看一眼对应包的 README 或者官方文档,以你安装的版本为准。我下面给的是思路级示例,不是“复制就能跑”的绝对答案。

2.2 初始化一个最小实例

在当前版本的框架下,初始化流程大致是:创建 Univer 实例,注册插件,最后把实例挂载到 DOM 容器上。代码长这样:

import { Univer, LocaleType } from '@univerjs/core'; import { UniverSheetsPlugin } from '@univerjs/sheets'; import { UniverSheetsUIPlugin } from '@univerjs/sheets-ui'; import { UniverFormulaEnginePlugin } from '@univerjs/engine-formula'; import { UniverSheetsFormulaPlugin } from '@univerjs/sheets-formula'; import { defaultTheme } from '@univerjs/design'; import { UniverSheetsNumfmtPlugin } from '@univerjs/sheets-numfmt'; const univer = new Univer({ theme: defaultTheme, locale: LocaleType.ZH_CN, }); univer.registerPlugin(UniverSheetsPlugin); univer.registerPlugin(UniverSheetsUIPlugin); univer.registerPlugin(UniverFormulaEnginePlugin); univer.registerPlugin(UniverSheetsFormulaPlugin); univer.registerPlugin(UniverSheetsNumfmtPlugin); univer.createUnit( UniverSheetsPlugin, { sheet: { worksheet: { id: 'sheet-001', name: 'Sheet1', rowCount: 1000, colCount: 50, cellData: { 0: { 0: { v: 'Hello', s: { b: 1, bl: 0, it: 0 } }, 1: { v: 100 }, 2: { v: '=A2*2' }, }, 1: { 0: { v: 20 }, 1: { v: 30 }, }, }, }, }, } ); // 挂载到页面容器 univer.mount(document.getElementById('app')!);

这里有几个容易忽略的地方。第一,cellData 的 key 是行号和列号,从 0 开始,不是 Excel 里的 A1 写法;第二,单元格的样式 s 字段是一组紧凑编码,直接手写维护太痛苦,实际项目里我们一般不会手搓这个结构,而是从命令或导入解析中得到;第三,注册插件的顺序会影响 UI 功能和公式功能的绑定,建议严格按 core 相关模块、sheet 模块、UI 模块、公式模块的顺序注册。

2.3 首次启动最容易踩的三个小坑

坑一:样式文件没引入。univer 的 UI 依赖 @univerjs/design 的样式,如果你用的打包工具配置了 CSS 处理,需要在入口文件引入import '@univerjs/design/lib/index.css'这类的样式资源。不引入的话,表格能渲染出来,但弹层、菜单、工具栏都是裸样式,某些浏览器下甚至会错位。

坑二:容器必须给高度。univer 挂载时如果容器没有明确的宽高,Canvas 会初始化出一个 0 高度的画布,页面上一片空白,控制台还不一定报错。我在第一次跑 Demo 时就卡了十分钟,后来给容器设置了height: 100vh; width: 100%;才正常。

坑三:XLSX 文件的导入导出不是 core 自带能力。你可能会以为“表格组件当然能打开 Excel 文件”,但 univer 的定位是 SDK,它把“文件的解析和序列化”和“表格的编辑展示”拆开了。要打开 xlsx,你得自己解析文件再写入数据模型;要导出 xlsx,也得自己从数据模型生成文件。这听起来麻烦,但其实是设计上的优点,后面第四部分我会详细说。

3. 核心概念拆解:Workbook、Sheet、Range 和 Command 机制

3.1 你操作的不是 DOM,是一棵数据树

习惯了操作 DOM 表格或者面向对象式表格组件的人,上手 univer 时会有一个思维转换:你拿到的一切都是描述性的数据对象。univer 里有三层模型:最外层是 Workbook,相当于一个 Excel 文件;Workbook 里面有多个 Worksheet,每个 Worksheet 有行列信息、单元格数据、合并单元格表、行高列宽、条件格式规则等;再往下是单元格级的数据范围,也就是 Range。

这种设计带来的直接好处是:所有操作都可以被描述成对“数据状态”的修改,而不是对某个 DOM 节点样式的修修补补。比如合并单元格,本质上是往 Worksheet 的 mergeData 数组里加一条区域记录,而不是把若干 td 的 border 属性改掉。这样数据结构干净了,协同、撤销、序列化都变得可控。

3.2 Command 机制是理解 univer 的钥匙

univer 里用户的所有编辑动作,最终都会通过 Command 对象表达。比如“把 A1 的值改成 100”,在系统内部对应一条命令,命令里有操作类型、目标单元范围、新增值等参数。这个机制的工程价值很大:

  • 撤销重做可以基于命令日志做,不用为每个操作单独维护一份反操作;
  • 协同编辑可以把命令作为最小同步单位,服务端广播命令,其他端重放命令;
  • 权限控制可以在命令层拦截,比如禁止某些人执行 setRangeValue 命令;
  • 外部系统可以主动派发命令,模拟用户在界面上的操作。

我在做数据联动的时候,就直接在外部调用了 setRangeValue 命令去改指定单元格的值,界面上的所有响应、公式重算、撤销记录都会自动跟上。这在传统表格组件里通常要多写很多代码。

3.3 公式引擎的独立性

univer 把公式引擎单独拆成 engine-formula,这不仅是模块划分上的洁癖,有实际的工程意义。公式引擎负责维护公式的解析、依赖树、计算顺序,而表格 UI 层只负责展示计算结果。数据变化后,公式引擎会自己判断哪些公式依赖了这个单元格,然后只重算受影响的部分,而不是全表扫一遍。

我实测下来,日常报表中常见的 SUM、IF、VLOOKUP、INDEX/MATCH、跨 Sheet 引用这些都能正常算,而且当我们导入一个带公式的 xlsx 时,公式表达式会保留在单元格模型里,不会在导入时被“翻译”成静态值。这一点非常关键,否则导出回 Excel 后公式就全失效了。

不过也要提醒:如果你要用极冷门的函数,或者特殊的分组汇总逻辑,建议先跑一个针对性测试用例确认。univer 的公式兼容性已经做得不错,但距离 Excel 的完整函数集还是有差异,涉及项目周期评估时不要把“完全兼容”当成默认前提。

4. 从 Excel 到 univer:导入导出的完整链路与坑

4.1 导入 xlsx 的推荐路径

由于 univer 不自带 xlsx 解析,我们项目的做法是:前端先用 SheetJS 解析 xlsx,拿到 cell 结构、样式表、合并单元格、列宽行高、公式等原始信息,再把这些信息映射成 univer 数据模型,最后通过命令写入。

流程可以简单概括为:

// 1. 读取文件为 ArrayBuffer const buffer = await file.arrayBuffer(); // 2. 用 SheetJS 解析工作簿 const workbook = XLSX.read(buffer, { cellStyles: true, cellFormula: true }); // 3. 遍历 sheet,提取 cellData、mergeData、colWidths、rowHeights // 4. 构造 univer 的 worksheet 配置 const worksheetConfig = buildWorksheetConfig(sheet); // 5. 注册插件后创建或更新唯一的工作簿

这里最容易出问题的是样式映射。xlsx 里的样式模型是非常底层的:颜色有主题色和索引色的区分,边框有对角线、双线等类型,数字格式串的写法跟 univer 内部格式码也不是一一对应。直接照搬肯定会出现“颜色变了、边框多了条线”的情况。

我的建议是:不要把“100% 还原”作为第一个版本的验收标准,先把 90% 的常见场景覆盖住,包括合并单元格、加粗、背景色、边框、数字格式、对齐方式、公式、下拉校验。这些覆盖好之后,用户体感上已经接近“打开没乱”了。剩下的刁钻样式,再用一个字体档去映射补充。

4.2 样式丢失问题的根源

很多人在 issue 里抱怨“用 univer 打开 xlsx 后样式丢了”,其实相当一部分并不是 univer 渲染不出来,而是导入链路的 Sample 代码压根没把样式信息传进去。univer 的渲染层是拿数据模型里的 s 字段画 Canvas 的,如果你的映射代码忽略了样式信息,那画出来的自然是白底黑字。

调试这类问题时,不要先去怀疑渲染引擎,先在导入端打一个断点,看你解析出的单元格对象里有没有 s 字段。s 没有,那后面全都不用看了。这一点也是我踩了两次坑才长记性的。

4.3 导出时别让公式变成静态值

导出 xlsx 时,反过来要把 univer 数据模型里的公式表达式写回 Excel。做法是用 SheetJS 的公式字段,把单元格的公式字符串填进 cell.f 字段,而不是填 cell.v 的计算结果。如果你在导出时只遍历了单元格的显示值,那用户拿到的文件里所有公式全没了,只有死数字。这个问题在验收时极容易被发现,但在开发期又很容易被偷懒绕过去,属于必须在需求文档里写明的高优先级约束。

另外,导出时要注意条件格式和数据校验的写回,这两块在 xlsx 规范里对应的是 sheets 节点的条件格式列表和数据校验列表。我们第一版跳过这两项,结果客户把导出的文件发给同事,对方一打开就发现下拉框没了,非常影响观感。

5. “univer在线”场景落地:在线协同与服务端接入思路

5.1 为什么大家都搜“univer在线”

很多人搜“univer在线”,本质上搜的不是某个网站在线版,而是“怎么拿这个项目去做我的在线表格”。因为 univer 是一个 SDK,官方 demo 跑起来是单机版,要变成真正的在线服务,你得自己补齐服务端、接口和同步机制。下面这部分我就按照从轻到重的方式,梳理三种落地形态。

5.2 轻量形态:命令广播

最简单的一种协同形态,是服务端做一个 WebSocket 中转:用户 A 在页面上做任何操作,univer 的命令总线把命令对象拿到,前端将它发给服务端,服务端广播给房间里的其他用户,其他用户拿到命令后在本地的 univer 实例上执行同一条命令。

这个方案的好处是工程量小,只处理命令的传输和顺序,不需要自己写复杂的数据合并逻辑。缺点也很明显:如果两个人同时修改同一个单元格,后到达的命令会直接覆盖先到达的命令,没有冲突处理。适合内部系统、编辑人数少、互相不干扰的场景。

5.3 完整形态:操作日志 + 状态重建

如果要更像真正的“在线表格”,就需要把服务端的数据存储和客户端操作打通。基本思路是:服务端维护一份操作日志(也就是命令序列),任何端加入时,服务端下发当前完整快照或者全量命令日志,客户端重放后得到最新状态。

这个方案和 univer 的 Command 机制配合得很顺。命令是有序的,重放命令就能重建状态,撤销也能通过丢弃末尾命令来实现。我们在测试环境用这个模式做了一个双人同时编辑的 Demo,一个用户改 A 区域的样式,另一个用户改 B 区域的值,双方刷新后都能看到对方的改动,整个链路跑通后,协同编辑的底层就具备雏形了。

需要强调的是,真正成熟的产品要做 OT 或者 CRDT 算法来解决并发冲突,univer 社区也在往这个方向演进。但如果你只是想在企业内部系统里做到“多个人能同时看、改、保存”,命令日志加冲突后覆盖的策略已经能应付大部分场景了。

5.4 服务端技术选型上的建议

协同场景里,服务端状态存储我建议先用 MongoDB 或者 Redis 这类成熟方案保存命令日志和工作簿状态,不要一上来就引入复杂的协同算法库。先让业务跑通,再在用户真正遇到编辑冲突的时候,按冲突率决定要不要引入更重的协议。工程上最忌讳的是第一天就把复杂度拉满。

另外,在线场景一定做权限控制。univer 的命令总线可以拦截命令并校验权限,我在后端对命令做了白名单校验,禁止非法的 setRangeValue 或 addMerge 请求,避免有人跳过前端直接调接口篡改表格数据。

6. 实战踩坑记录与性能优化建议

6.1 大数据量下的渲染表现

univer 的 Canvas 渲染在数据展示层面比 DOM 表格强很多,但也不是无上限。当我们的表格达到几十万行、并且每行都有复杂样式和公式时,初始化和全表公式重算还是会有明显卡顿。

优化方向上,我做了三件事:第一,只加载可视区附近的数据,远端数据用分页/虚拟化接口配合,表格里看不完的数据不在前端一次性塞满;第二,公式计算尽量异步执行,初始化期间不要阻塞 UI,等计算完成再刷新显示;第三,禁用不必要的插件和功能,例如不需要协同就完全不注册相关模块,不打印命令日志,减少运行时开销。

6.2 条件格式和多优先级规则的正确写法

条件格式是隐藏深坑。univer 对条件格式的规则支持比很多组件都好,但项目里如果存在多条优先级不同的规则,必须确认规则的顺序与你 Excel 原文件一致。因为条件格式的优先级决定了多个规则同时命中一个单元格时,到底哪条样式生效。

我遇到过一次场景:原 Excel 里“空值显示为灰色”的规则排在“大于 100 显示为红色”之前,导致大于 100 的空值显示为灰色而不是红色。问题不在 univer 的渲染能力,而在导入映射时我没保留规则的原始优先级顺序。修复方法很简单,解析 xlsx 时把条件格式列表按源文件顺序写入数据模型,不排序、不去重。

6.3 单元格类型和值类型别混用

univer 的单元格值类型区分得很严格,数字、字符串、布尔值、日期是不同类型。从外部接口写入数据时,如果不注意类型,把字符串"100"写进了本应是数字的单元格,导出的文件在 Excel 里就会被标记成“以文本形式存储的数字”,并且无法参与 SUM 等计算。

这个问题的排查方式也很简单:导入/导出前先做一次类型清洗,根据表头语义把字符串数字转成 number,把时间字符串转成日期对象,再进数据模型。我们在接口适配层写了一个 schema 校验,每个字段标记期望类型,写入前强制转换,从此再没出现“求和为 0”的诡异反馈。

6.4 打包体积控制

univer 模块多,全量引入打包体积很可观。实际项目里我用的是按插件注册的方式,只加载需要的能力。举一个具体做法:如果产品里只需要表格编辑,不展示文档和幻灯片,那就只装 sheets 相关包,完全不引入 doc 和 slide 模块。再配合构建工具的 Tree Shaking,把未注册的插件代码从产物里摇掉。

如果你要做首屏加载优化,另一个思路是动态导入 univer 初始化模块,等用户真正点进报表页时再加载,主站的 bundle 不会被表格组件拖慢。

最后再分享一个实操经验:第一次接入时,不要急着把权限、协同、导出全部做满,先跑通“导入 xlsx → 页面展示 → 简单编辑 → 导出 xlsx”这条最核心的链路,让产品经理拿真实文件验收一遍。等你确认样式还原度能满足业务预期,再往里面加协同和高级功能。因为 univer 这套体系一旦跑顺,后面的很多能力都是顺着 Command 机制自然长出来的,反而是最开始的导入导出映射代码需要反复打磨。希望这份记录能让你少踩几个我踩过的坑。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询