Univer:我为什么不建议你重复造一个在线表格轮子
第一次看到 Univer 这个项目时,我正被老板要求"下个月给后台管理系统加一个能编辑 Excel 的页面"。当时我脑子里闪过三个选项:用 Apache POI 做服务端解析然后前端用老掉牙的 table 拼界面、直接用 Luckysheet 魔改、或者干脆劝老板用 WPS 表单。最后 Univer 成了一个意外收获——它比我预想的要完整得多,也踩得坑要多得多。
Univer 是一个纯前端的开源电子表格/文档引擎,简单说就是一个可以嵌入任意网页的"在线 Excel 组件",支持公式、条件格式、协同编辑、数据透视等重型功能。它能解决的是"想在 Web 上给用户提供类 Excel 体验,但不想从零写行列模型、公式解析、Canvas 选区渲染"这一整类问题。适合用来给企业系统做数据填报、给 BI 报表加透视交互、甚至自建一套轻量级在线 Office。这篇文章我会从架构拆解、集成实操、生产落地三个层面,把我折腾 Univer 的过程和结论完整写出来。
如果你也在纠结用哪个开源表格库,或者已经在用 Univer 但感觉文档看不懂、API 太散,这篇文章能帮你少走不少弯路。
1. Univer 到底是什么:一个被低估的"办公三件套"级 SDK
1.1 它的出身和名字来源
Univer 的前身是一个叫 Graphite 的开源表格项目,后来团队重组,把产品重新命名并开源。很多人在 GitHub 上看到 Univer 时,第一反应是"这又是个花架子表格库",但实际上它从一开始就是按"全家桶"思路设计的。
Univer 的定位不只是"表格",它内部的架构设计其实在同时承载 Sheet(电子表格)、Doc(文档)、Slide(演示文稿)三种文档类型。你可能暂时只用它的表格能力,但它的整体分层——渲染层、命令系统、插件系统——都是为了支撑多文档类型。这一点非常像从 VS Code 看编辑器架构:表面是产品,底层是框架。
1.2 它到底解决了什么问题
传统的前端做表格页面,通常有两条路:
- 一条用 HTML table 或 div 网格做,列一多、数据一多就开始卡,更别提实现"选择区域拖拽、填充序列、图表透视"这些 Excel 级交互。
- 另一条用成熟库(如 Handsontable、AG Grid),但这些库偏"数据网格",解决的是"展示和编辑结构化数据",而不是"模拟 Excel 的完整操作体验"。
Univer 的切入点是把 Excel 桌面端的交互搬到浏览器里。具体来说:
- 支持行列拖拽调整、单元格多选区、双击编辑、下拉填充、条件格式、数据验证。
- 内置完整的公式引擎,支持大量常用函数。
- 自带工具栏 UI 组件(虽然样式有点"原生态"),也可以完全隐藏 UI 只留核心交互。
- 内置 Undo/Redo 和命令日志机制,为协同编辑预留了底层支持。
这直接改变了前端团队做需求的方式。以前客户说"我们要一个像 Excel 的表单",前端得论证排期一个月;现在可以三天内用 Univer 出一个可交互原型,两周内接完数据上线。
1.3 和渲染层"无 UI"的设计哲学
Univer 有个很核心又容易被忽略的设计:它的 UI 层(工具栏、菜单栏、右键菜单)是基于一系列 UI 插件实现的,而核心引擎不依赖任何 UI 框架。这意味着你可以把 Univer 集成进 React、Vue、Angular、Svelte,甚至是一个 jQuery 老项目里。
我实际体会下来,这个设计最大的价值是:你的业务侧代码不用和它的 DOM 结构绑定。你操作的是 Univer 暴露的 API(比如获取选中区域、设置单元格值),而不是去操作某个 div。这个对复杂业务特别友好——你不需要等下 DOM 渲染完再去 document.querySelector 找不到节点。
2. 选型对比:Univer、Luckysheet、Handsontable 到底怎么选
2.1 直接横向对比一把
我先说结论:如果你的核心诉求是"界面接近 Excel、交互自由度大、社区潜力强",选 Univer;如果只是"表格要能编辑、数据量还行、团队已经很熟",Handsontable 可能更稳妥;如果"项目已经黄了,只求一个没人维护的库能跑",Luckysheet 也可以将就(但我不建议新项目选它)。
| 维度 | Univer | Luckysheet | Handsontable |
|---|---|---|---|
| 渲染方案 | Canvas 自绘 | Canvas + DOM 混合 | 纯 DOM |
| 维护活跃度 | 活跃,版本迭代快 | 已基本停滞 | 商业维护,稳定 |
| 核心能力 | 表格 + 文档 + 幻灯片 | 表格为主 | 数据网格为主 |
| 公式引擎 | 内置,较完整 | 内置 | 需引入插件 |
| 协同编辑 | 架构支持,需自行接入 | 需魔改 | 商业版支持 |
| 上手友好度 | 中等(文档较散) | 中等 | 低门槛 |
| 许可证 | Apache 2.0 | Apache 2.0 | 商业 / 非商业免费 |
2.2 为什么 Univer 的 Canvas 渲染让我下决心
Univer 采用 Canvas 渲染,而不是 DOM 渲染。这个区别在普通表格看不出来,但一旦你滚动几千行、频繁选区、批量改样式,差距就非常明显。DOM 方案的表格库在数据多了以后容易卡在"节点太多、布局重算太慢",而 Canvas 自绘方案只要有绘制上下文,绘制多少行只是一次批量的绘图操作。
代价是:Canvas 渲染意味着你不能用传统 CSS 去选中内部元素、修改样式、加气泡。所有选中态、高亮、拖拽框线都是引擎自己画的。所以做定制样式时,你要去了解它的"皮肤机制",而不是简单地写 .xxx { color: red }。这个点我在后面踩坑环节展开。
2.3 什么场景真的不适合 Univer
老实说,有几种场景我不会选 Univer:
- 超大表格性能洁癖:如果你就是要渲染几十万行、列数很多的数据网格,并且主要做只读展示,你其实更适合 Canvas 虚拟滚动做得更极致的表格方案,或者干脆用 AG Grid 这种专门面向大数据量的表格。Univer 的强项是全功能交互,而不是纯粹的高密度展示。
- 需要和 Excel 文件格式做深度互操作:Univer 支持导入导出 xlsx,但它不是像 ExcelJS 那样偏重文件解析。如果你的核心功能是"把一个带 20 个 sheet、几十个图表、复杂样式的 xlsx 文件 100% 还原",Univer 现阶段还没到那个完成度。
- 团队完全不会 TS/前端工程化:集成 Univer 需要 npm 包管理、Vite/Webpack 构建、一点 TS 类型阅读能力。如果团队只会 JS 写页面,会有一定的门槛。
3. Univer 核心架构拆解:从命令系统到协同编辑的底层逻辑
3.1 命令模式与命令队列
Univer 内部对用户操作的抽象是一切操作都是命令(Command),而不是直接改数据。比如你把 A1 单元格改成 100,在 Univer 内部会触发一个SetRangeValuesCommand的指令,这个指令经过命令队列执行、重组、写入状态、撤销历史记录。
这个设计最开始看着很绕,但用过几次就明白它的价值了:命令模式让撤销/重做、协同编辑、操作录制都变得极其简单。撤销不用去"快照整个表格数据再恢复",只需保存一个反向命令(inverse),逆向执行即可。协同编辑也不用手动 diff 数据,每个人本地执行命令后,把命令广播给其他人,对方本地执行同一个命令就能保持数据一致。
我在集成时用了它的协同编辑思路做了个简化版:把命令 JSON 序列化,通过 WebSocket 推到其他客户端,其他客户端调用univerAPIExcuteCommand之类的方法执行命令。真的不需要自己去同步整个 sheet 对象。
3.2 渲染引擎与"工作簿 - 工作表 - 单元格"的抽象
Univer 内部的对象模型可以简单理解成它自己维护了一套"工作簿"树结构:
- 一个 Univer 实例(Univer)对应一个文档。
- 文档里有一个或多个工作簿(Workbook)。
- 工作簿下面有多个工作表(Worksheet,对应 Excel 底部那些 Sheet Tab)。
- 每个工作表有行、列、单元格、样式表、数据验证规则、条件格式规则等。
UI 上的滚动、缩放、选区,都是渲染引擎读取当前"视口"(viewport)应该显示哪些行列,然后绘制到 Canvas 上。它不是把整个表格渲染出来再滚动;它只画你看得见的区域,这就是虚拟化(virtualization)机制。
我结合源码和实际测试的情况是:Univer 在渲染上万行、每天滚动时保持流畅基本没问题,但如果单元格内有大量复杂富文本或者大量条件格式,Canvas 每一帧绘制内容增多,肉眼能感觉到掉帧。建议条件格式不要无脑铺满整列,尽量缩小范围。
3.3 插件系统:为什么它能同时跑 Sheet 和 Doc
Univer 把功能边界切得很细。它的插件系统类似 VS Code 的扩展机制:核心引擎只处理文档数据的 CRUD 和渲染,其他能力都挂载到插件里。比如公式栏、工具栏、右键菜单、导入导出、协同协作、查找替换,全是不同的插件包。
这种架构对开发者的实际好处是:你可以按需引入。如果我只想要一个能看能编辑的表格,我可以只引核心包和基础插件,不引协同包,包体积能小不少。
但同时带来的困扰是:你要搞清楚"哪些功能在哪个包里面",不然你调一个 API 发现报错"function not found",多半是没装对应的插件包,而不是 API 写错了。
4. 从零跑一个 Univer 实例:我实测过的最小可运行集成
4.1 前置准备
我用的是 Vite + Vue 3 工程(老项目用 React 也一样,Univer 提供框架无关的 API)。建议大家直接在 npm 上使用最新稳定版本,注意不要 GirHub 上直接 clone 老版本 demo 去跑,很多 API 改名了。
安装依赖时特别注意:你需要同时安装@univerjs/core、@univerjs/engine-render、@univerjs/sheets、@univerjs/sheets-ui、@univerjs/ui等一堆包,并且版本号要匹配。这是 Univer 上手最难的一步,也是最容易让新人劝退的一步。
我的建议:先去官方文档找"Getting Started"里的 demo 依赖版本组合,一键复制 package.json 相关段落,不要自己凭空猜版本。
4.2 初始化代码(Vue 3 + Vite 实测)
下面是我跑通的一个最简示例,核心拿来就能用:
import { Univer, UniverInstanceType } from '@univerjs/core'; import { UniverRenderEnginePlugin } from '@univerjs/engine-render'; import { UniverSheetPlugin } from '@univerjs/sheets'; import { UniverUIPlugin } from '@univerjs/ui'; import { UniverSheetsUIPlugin } from '@univerjs/sheets-ui'; import { UniverFormulaEnginePlugin } from '@univerjs/engine-formula'; import { UniverSheetsFormulaPlugin } from '@univerjs/sheets-formula'; import '@univerjs/ui/lib/index.css'; import '@univerjs/sheets-ui/lib/index.css'; // 创建 Univer 实例 const univer = new Univer({ locale: 'zhCN', // 默认不显示工具栏,想看到完整 UI 就 folow 官方 demo 的配置 }); // 注册各种插件(注意顺序) univer.registerPlugin(UniverRenderEnginePlugin); univer.registerPlugin(UniverSheetPlugin); univer.registerPlugin(UniverFormulaEnginePlugin); univer.registerPlugin(UniverSheetsFormulaPlugin); univer.registerPlugin(UniverUIPlugin, { container: 'app', }); univer.registerPlugin(UniverSheetsUIPlugin); // 创建一张工作簿 const workbook = univer.createUnit(UniverInstanceType.UNIVER_SHEET, { id: 'demo-book', name: '测试工作簿', sheets: [ { id: 'sheet-1', name: 'Sheet1', rowCount: 1000, colCount: 20, cellData: { 0: { 0: { v: '姓名' }, 1: { v: '销售额' }, }, 1: { 0: { v: '张三' }, 1: { v: 12000 }, }, }, }, ], });看到没,cellData是一个二维对象结构,键是行号、列号,值是一个 Cell 对象,里面有v(值)、s(样式 id)等属性。这个数据格式是 Univer 内部的标准格式,后面做数据绑定、保存、回显都要和这个结构打交道。
4.3 关键 API:操作单元格、选中区域、设置样式
在实际业务里,你不可能只创建表格,肯定要往里面写数据、读数据。我常用这几个 API:
// 获取当前激活的工作表和选中的区域 const activeSheet = univer.getActiveUnit(); // activeSheet 就是 Workbook 对象,通过它拿当前 sheet const worksheet = activeSheet.getActiveSheet(); // 设置 A1 单元格的值 worksheet.getRange(0, 0).setValue('新值'); // 批量设置范围 B1:C3 的值 worksheet.getRange(1, 0, 3, 2).setValues([ [1, 2], [3, 4], [5, 6], ]); // 设置单元格样式(背景色、字体、加粗) worksheet.getRange(0, 0).setStyle({ background: '#ff0000', bold: true, }); // 监听单元格点击事件 // 这里需要借助 UI 插件的命令服务,示例略这里要注意:getRange 的索引是 0-based(从 0 开始),不是 Excel 里的 1-based。习惯了 Excel 对象模型的同学一定会在这里晕几次,我已经亲眼见过同事把 A1 写成 getRange(1, 1) 然后改成了 B2 单元格。
4.4 加载公式、合并单元格、条件格式
公式引擎是 Univer 的重头戏。设置公式单元格也很简单,只要在v字段里以=开头赋值:
// 把 C1 设为公式 =SUM(B1:B5) ws.getRange(0, 0).setValue('=SUM(B1:B5)');Univer 内部会解析这个公式字符串,并计算结果渲染在单元格里。实际体验里,常用函数 SUM、AVERAGE、IF、VLOOKUP 都能正常跑。如果你在做数据验证,可以使用setDataValidation,但需要先创建校验规则对象,相对繁琐,我后面场景部分再细说。
合并单元格在业务表格里几乎是刚需,示例:
// 合并 A1:B2 ws.merge(0, 0, 2, 2); // 参数顺序:row, col, rowSpan, colSpan // 取消合并 ws.unmerge(0, 0, 2, 2);条件格式用 API 创建的套路比较繁琐,我一般建议优先用工具栏手动设置好模板,再通过 Univer 的"导出 JSON"读出现有格式定义,然后放进业务代码里复用。这个文档没有写得很清楚,是我摸索出来的捷径。
5. 从 Demo 到生产环境:我踩过的坑和最终解决方案
5.1 坑一:样式定制不是写 CSS,而是改"主题"
和传统组件库不同,Univer 的皮肤系统不是用 CSS 变量覆盖这么简单。它有一套基于设计令牌的 UI 配置,里面包括颜色、字体、间距等。你可以全局传入主题配置,但不能像改 Element Plus 那样一句--el-button-bg-color: red搞定。
我的做法是:直接用它的默认主题,然后通过univer.setSkin或插件配置覆盖部分颜色,而内部单元格样式尽量用setStyle去控制。如果你公司有强设计规范和品牌色要求,需要预留额外时间去抠这个定制,不要把它当普通 CSS 项目。
5.2 坑二:数据回显和保存,必须统一走单一数据源
Univer 不强制你绑定服务端数据。你以为直接把后端数据塞进cellData就行,但用户的每一次编辑都会只改 Univer 内部的内存数据。如果没有监听数据变更并保存到后端,刷新页面就全丢了。
我的方案是三分法:
- 初始化:从后端加载数据,映射成
cellData结构。 - 变更监听:通过命令服务监听 "set range values" 等核心命令,将变更区域转成业务字段并提交后端(防抖 500ms)。
- 互相更新:如果其他页面修改了同一张表格,后端推送变更消息后,前端用
setValues增量更新对应区域,避免整表重置导致光标跳动。
核心教训是:不要把"保存"做成轮询读取所有单元格再全量提交。几千个单元格全量 JSON 传输,服务端和浏览器都会很吃力。基于命令做增量,几乎是必须的选择。
5.3 坑三:性能优化的优先顺序
如果遇到滚动卡顿、编辑延迟,按照下面顺序排查:
- 第一个检查条件格式和图表数量,它们会拖慢 Canvas 渲染。
- 第二个检查单元格是否大量使用了富文本(
richText)对象,富文本绘制开销远高于纯文本。 - 第三个检查
rowCount、colCount。如果你创建了一个 100 万行的工作簿,就算只显示前 20 行,Univer 的滚动条比例、坐标计算也会有一定开销。尽量按实际数据范围设置行列数,而不是随手写个大数字。 - 第四个检查是不是频繁触发整个工作簿的全量重绘。有时候你调了
setValues一个大范围,Univer 要重算相关区域、重绘视口,这比想象中要重。
我在一个报表项目里,把 5 万行数据、每行 6 个条件格式,改成了只有选中/过滤时动态添加高亮,性能直接翻倍,肉眼可见不卡了。
5.4 坑四:TypeScript 类型报错和版本匹配地狱
Univer 的包非常多,不同版本的 API 签名很不一样。我在 npm 上安装时遇到最多的问题是:某个包是 0.0.x,另一个包是 0.0.y,它们之间类型不匹配,编译报错 "Property 'xxx' does not exist on type"。网上搜到的代码在你当前版本可能已经废弃。
我的建议是:
- 全部包锁定同一个精确版本号,例如官方 demo 里用的
0.1.0,那你就所有 @univerjs/* 都写死0.1.0。 - 遇到类型报错,去 node_modules 对应包的类型定义文件里搜方法名,比在搜索引擎找更快更准。
- 如果只是让项目跑起来,可以先用
// @ts-ignore临时处理个别 IDE 报错,但要在上线前回头补上类型修复。
6. Univer 的可玩性:用它能搭出哪些有想象力的场景
6.1 低代码平台里的"万能数据录入组件"
现在很多企业内部低代码平台,表单都是字段拼接。遇到客户要"像 Excel 一样批量录入"时,普通表单完全摸不着头脑。用 Univer 做一个嵌入式的 Sheet 组件,可以让用户在网格里直接批量填数据、下拉选择、数据校验、自动计算金额列,提交时再把表格区域转成 JSON 数组给后端。
这块体验的提升是质变的。我帮一个供应链系统做过采购单批量录入:客户从 Excel 里复制一整块粘贴进来,直接进入 Univer 表格;修改几行后点击保存,表格数据转成一个数组 POST 到接口。整个流程顺滑得不像内部系统。
6.2 报表系统的"假 Excel"透视交互
BI 报表常常只读,用户想看明细、拖拽列、手动排序都要自己导出到 Excel。用 Univer 可以在网页里直接生成一张透视分析表——虽然 Univer 官方对数据透视表的支持还在完善,但你可以用自带的公式、筛选功能做一个简化版:用户选中区域,点"汇总"按钮,自动生成一个 SUM 公式统计区域。这个交互比导出 Excel 再操作顺手多了。
6.3 协同编辑:不只是"一起打字"
协同编辑是 Univer 架构上的重头戏,但很多人误以为装上协同插件就能用。实际要做多人在线编辑,你还需要:
- WebSocket 服务端来转发命令。
- 一个版本/冲突处理机制(Univer 提供命令合并的基础,但具体业务冲突解决逻辑需要你实现)。
- 在线状态展示(谁在编辑哪个单元格)。
如果你只是想做"加锁编辑",比如某个人编辑某个 sheet 时其他人只读,那就简单得多:用命令监听判断是否有人已进入编辑,然后前端锁定即可。
我建议不要把协同编辑当作第一个 Univer 集成目标。先把单机编辑、数据保存做扎实,再逐步引入协同,否则排查问题时会同时面对"自己的逻辑问题"和"Univer 内部状态问题"两个复杂层。
6.4 导入导出 xlsx:够用但别期待像素级还原
Univer 的 xlsx 导入导出能力是通过@univerjs/sheets-import-xlsx和@univerjs/sheets-export-xlsx插件提供的。我的实测感受是:简单表格(数值、字符串、简单样式、基本公式)能很好的往返;复杂功能(图表、图片浮动元素、非常规样式、数据透视表)很可能丢失或变形。
所以业务上如果需要下载 xlsx,我会建议:
- 用户编辑时数据源永远是 Univer 的工作簿 JSON。
- 导出功能定位为"给用户一个方便看的数据快照",而不是"完美还原所有格式"。
- 如果需要像素级还原,考虑将 Excel 文件解析做成服务端能力(如用 LibreOffice headless 转出),不要让前端硬抗。
7. 我的最终建议:Univer 值得选择,但要用对姿势
如果你已经在开发和维护一个内部系统,且需要一个能编辑、能保存、能漂亮地展示数据的"Excel 体验",Univer 确实是我目前见过最适合拿来快速集成的开源方案。它的架构干净,Canvas 渲染带来的性能优势明显,命令系统的设计对后续扩展(撤销、协同、操作日志)极有价值。
如果你只是临时做一个只读数据表格,或者公司安全要求不能引入任何"不够成熟"的开源依赖,那你可能应该继续用表格组件而不是 Univer。Univer 的成熟度在核心表格编辑领域是够用的,但生态包装、文档完整度、API 稳定性还有明显的开源项目气质——意味着你要接受一定程度的"自己摸坑"。
最后分享一个小经验:集成 Univer 不要从零开始手写所有配置,先在官方示例基础上做最小改动,逐步迭代出自己的封装层。在我自己的项目里,最后我封装了一个ExcelSheet.vue组件,把初始化、数据变更、保存、导入导出全封装起来,业务方调用时只需要传入columns、dataSource和onChange回调,完全不感知 Univer 的复杂 API。这种"包一层"的做法,才是团队级使用这种底层引擎的正确姿势。