ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

开源在线表格引擎Univer:选型架构与业务集成实战

开源在线表格引擎Univer:选型架构与业务集成实战

最近在给团队评估在线表格方案,GitHub 上频繁刷到univer这个词,搜索结果里的 "univer 在线" 讨论度也明显升温。我花了两周时间把它接入到一个内部数据报表系统里做了完整验证,这篇文章直接说说我对这个项目的理解、选型理由,以及实际集成过程中遇到的那些文档里不常写的问题。

Univer 不是一个简单的"网页端 Excel 控件",它是一套用 TypeScript 从零写的开源 Office 套件引擎,目前包含表格(Sheets)、文档(Doc)、幻灯片(Slide)三大模块。因为核心定位是"可嵌入业务系统的在线办公基础设施",所以它和传统意义的纯前端表格组件有本质区别。外层 UI 可以全部关掉,功能模块也能按需加载,理论上你拿它拼一个完全定制化的在线协作平台都行。如果你正在做 SaaS 产品、内部 OA/ERP、低代码平台这类需要内嵌表格和文档能力的系统,这篇内容应该能帮你少踩不少坑。

1. Univer 瞄准的痛点:在线表格库和 Office 套件的分水岭

1.1 从 ExcelJS 到 Univer:代际差在哪里

先说一个很多团队都会遇到的尴尬:业务系统里需要在线表格,第一反应是找个"能读写 Excel 的 JS 库"。ExcelJS 这类库确实能解析 xlsx 文件,可它们本质是数据解析器,不是交互编辑器。你拿它导入导出文件没问题,但想让用户直接在网页里点单元格、改数据、调格式、跑公式,那就完全不是一回事了。

再往上一个层级是 Luckysheet。这个项目当年确实惊艳了很多前端团队,但它后期维护节奏放缓,核心作者后来转向了 Univer。这个背景很重要,因为 Univer 从架构上吸取了 Luckysheet 踩过的所有坑,对"数据层"和"渲染层"做了彻底的重写,而不是在旧代码上打补丁。

另一个常被对比的是 SpreadJS,商业授权、功能完整、稳定性经过大量验证。但它的闭源属性和授权费用,决定了你没法把它深度改造,更别提在它之上构建自己的插件生态。Univer 走的是 Apache 2.0 开源路线,底层的公式引擎、渲染引擎、协同逻辑全部开放,这一点对于想长期自研产品的团队来说诱惑力极大。

1.2 Univer 能做什么:不只是一个"Web Excel"

刚接触 Univer 的人容易把它理解成"开源的 Google Sheets 克隆版",但它的野心明显要大得多。官方定位是All-in-One 的在线 Office 套件,同一个架构下跑表格、文档、幻灯片三类文档模型。

我实际测下来,Univer Sheets 模块能做的事情包括但不限于:

  • 在线表格的完整编辑交互:选区、填充、拖拽、右键菜单、公式自动补全、单元格格式设置、条件格式、图表;
  • 兼容大量 Excel 常用公式,公式引擎运行在独立 Worker 里,不阻塞 UI;
  • 支持撤销重做、复制粘贴优化的剪贴板管理;
  • 多实例、多 Sheet、多 Workbook 的管理模型,适合在同一个页面跑多个文档;
  • 命令系统贯穿所有操作,这意味着你可以在任意操作前后挂钩子,做审计、校验、自动补全;
  • 协同编辑模块有官方支持,配合自建的协同后端,可以实现多人同时编辑一张表格。

我把它嵌入到我们的数据报表系统后,第一感受是:这不是"嵌入了一个表格",而是"在应用的某块区域里长出了一个可编程的表格引擎"。

1.3 适合用和别硬用的场景清单

抛开技术细节,先给你一个场景筛查清单,避免选型选错方向。

适合上 Univer 的场景:

  • 业务系统里的内嵌数据编辑区,比如 OA 里的报销明细列表、ERP 里的 BOM 维护、CRM 里的销售计划填报;
  • 需要"在线打开就编辑"的轻量表格工具,用户不希望下载文件、改完再上传;
  • 低代码/无代码平台里需要给用户提供"类 Excel 设计器"的组件;
  • 需要在表格能力之上扩展自定义业务逻辑的团队,比如单元格里嵌入公司自定义的数据类型;
  • 想做多人实时协作编辑产品的团队,协同部分已经内置了协议层实现。

别硬上的场景:

  • 只是要让网页展示一张静态报表,杀鸡用牛刀,直接 Canvas 画表格或者用 Grid 组件更划算;
  • 对 Office 原生格式的兼容有"必须 100% 像素级一致"的要求,Univer 还在高速迭代中,xlsx 的复杂样式兼容还没到完美状态;
  • 团队没有前端工程化能力,所有功能都靠 CDN 引入一把梭——Univer 的模块化设计决定了你最好走 npm + 构建工具的路线。

2. 为什么它的架构能扛住复杂场景:关键设计拆解

2.1 命令驱动一切操作

我第一次看 Univer 源码时,印象最深的就是它的Command 系统。在 Univer 里,用户每一次操作,无论是输入一个字符、拖拽一个选区,还是执行一次排序,都会被封装成命令。命令有execute和undo两个核心方法,整个应用围绕命令和状态在做流转。

这个设计带来的实际好处非常直接:撤销重做不再是各组件自己管的逻辑,而是统一由命令栈调度;业务系统想拦截某些操作(比如禁止普通员工导出),只需要在命令执行前加一层校验;AI 想要操作表格也一样,生成命令、执行命令,不需要模拟用户行为去操作 DOM。

对我这种做业务集成的人来说,这套设计大大降低了"在表格上叠业务"的心智负担。我不用关心界面上的某个按钮怎么触发逻辑,只要找到对应命令,在业务层直接调用命令接口就行。

2.2 双引擎分工:渲染和公式

Univer 的渲染层是基于 Canvas 的自研引擎,这一点非常关键。DOM 表格在渲染几千行单元格时就能把页面拖垮,Canvas 通过区域绘制和脏区刷新,能撑住更大的数据量。实际使用中,我第一次在一个工作簿里塞了 10 万行数据做滚动测试,帧率表现依然可接受,比我之前用 DOM 方案做的同类尝试强了一个量级。

公式计算被设计成独立模块,并且默认跑在 Web Worker。单独强调这一点,是因为传统表格库公式计算和界面渲染抢主线程,是卡顿的最大来源。Univer 把公式引擎拆出来,通过消息通信和 UI 层交互,复杂公式计算时可以保证页面仍然流畅响应。这套拆分带来的工程复杂度很高,但方向是对的。

2.3 协同能力藏在设计里

在线协同是大部头,Univer 的官方文档里把它作为一个独立模块来讲解,依赖 WebSocket 等协议做数据传输。前端部分已经实现了文档模型的变更集管理,后端可以基于官方提供的参考实现去部署调度服务。

我在调研阶段仔细研究过它的协同数据模型:每次操作不再是"把整张表发给服务器",而是传一条描述操作的记录,服务端通过定序和合并算法把多人操作合并到各自文档副本上。这种设计让冲突的粒度控制到了操作级别,而不是行级别,多人同时编辑同一张表时的冲突概率大幅下降。

需要提醒的是,协同功能不是"装上就有的",它需要你自己部署协同后端服务。如果只是单机内嵌使用,完全可以不启用协同模块,按需加载。

2.4 插件机制是扩展性的关键

Univer 的项目结构从根上就是插件化的,核心模块(数据模型、渲染引擎、公式引擎)只负责基础能力,编辑器 UI、右键菜单、图表、公式输入引导,这些都是以插件形式注册到应用里的。

举个例子:你拿到的@univerjs/presets里其实已经聚合了官方维护好的一套默认插件组合,所以我们接入时感觉"开箱即用"。但实际上每一个 UI 组件都是独立的插件,你可以逐个移除,也可以写自己的插件替换默认实现。

这套机制对二次开发的友好度极高。我看到很多关注 Univer 的开发者都在研究怎么把 AI 能力挂进去——让用户用自然语言直接操作表格。正因为表格操作全是命令和插件,AI 生成操作变成了一种天然的衔接方式。

3. 落地集成:把 Univer 表格嵌入业务系统

3.1 初始化和最小可运行页面

我们先走一遍最基础的上手流程。官方文档推荐优先使用@univerjs/presets,因为它把常用插件打包成了一套配置,减少首次接入的理解成本。

npm install @univerjs/core @univerjs/sheets @univerjs/sheets-ui @univerjs/presets

然后是最小示例:

import { Univer, LocaleType } from '@univerjs/core'; import { UniverPresetSheets } from '@univerjs/presets'; import { DEFAULT_SHEET_SETTINGS } from '@univerjs/presets/lib/preset-sheets'; const container = document.getElementById('app')!; const univer = new Univer({ locale: LocaleType.ZH_CN, }); univer.registerPlugin(UniverPresetSheets, { container, ...DEFAULT_SHEET_SETTINGS, });

跑起来后会看到一个几乎完整的在线表格界面,有工具栏、行头列头、公式栏、工作表标签页。这个体验对非技术同事来说,和打开 Excel 没有太大区别,项目能跑通这一行,剩下的就是业务集成的精细化工作。

3.2 读数据、写数据:接入后端报表

单纯的编辑器没法直接用,我们需要把后端数据填充到表格里,同时把用户编辑后的结果拿回来。

Univer 提供了明确的工作簿访问模型:

const workbook = univer.getActiveWorkbook(); const sheet = workbook?.getActiveSheet(); // 向指定范围写入数据 sheet?.getRange(0, 0, 100, 6).setValues([ ['2024-01', '销售部', 120000, '华东区', '已完成'], ['2024-02', '市场部', 85000, '华南区', '进行中'], ]); // 读取整个工作表的当前值 const values = sheet?.getRange(0, 0, sheet.getRowCount(), sheet.getColumnCount()).getValues(); console.log(values);

我在接后端接口时,通常把接口返回的二维数组 map 成单元格矩阵,一次性写入。如果数据超过几万条,建议分段写入并配合setValue批量操作,避免单次写整个大范围造成渲染卡顿。

需要注意一个细节:getActiveWorkbook()在页面刚加载、还没有文档实例时可能返回空值。正确做法是在初始化完成后通过生命周期钩子或插件的onReady里获取实例,不要一初始化就立刻调。

3.3 只读模式与界面裁剪

很多业务场景不需要用户动公式栏和工具栏,我只想给用户一块"能看数据、能选中阅读"的表格区域。

Univer 的UniverPresetSheets配置里可以通过displayOptions控制 UI 的显隐:

univer.registerPlugin(UniverPresetSheets, { container, ...DEFAULT_SHEET_SETTINGS, displayOptions: { toolbar: false, // 隐藏顶部工具栏 formulaBar: false, // 隐藏公式栏 contextMenu: false, // 隐藏右键菜单 sheetTabs: true, // 保留工作表标签 }, });

同时配合单元格编辑权限控制,在数据层面拦截修改:

sheet?.getRange(0, 0, 100, 6).setProtection({ locked: true, hide: false, });

比较稳妥的做法是"UI 只读 + 数据层保护"双管齐下。界面隐藏只是防正常用户误操作,数据保护才能真正避免程序化写入。

3.4 集成 React/Vue 时的容器管理

这段是我实际踩坑最多的部分。Univer 的初始化是命令式的,它不关心外层框架是 React 还是 Vue,你只给它一个 DOM 容器。但在组件化开发中,一定要避免因组件卸载导致容器被销毁、但 Univer 实例没有释放的问题。

React 里的基本写法:

import { useEffect, useRef } from 'react'; export default function SheetEditor() { const containerRef = useRef<HTMLDivElement>(null); useEffect(() => { const container = containerRef.current; if (!container) return; const univer = new Univer({ locale: LocaleType.ZH_CN }); univer.registerPlugin(UniverPresetSheets, { container, ...DEFAULT_SHEET_SETTINGS, }); return () => { univer.dispose(); }; }, []); return <div ref={containerRef} style={{ width: '100%', height: 600 }} />; }

组件卸载时必须调用dispose(),否则工作簿实例、事件监听、Worker 线程都会残留,造成内存泄漏。我在第一版集成时忽略了这一步,页面切换十几次后浏览器标签页直接卡死,后来排查才发现是旧实例堆叠。

4. 二次开发:插件命令与"AI 表格"的尝试

4.1 自己写一个轻量插件

Univer 的生命周期设计让插件开发门槛比想象中低。核心生命周期有onStarting和onReady,你可以理解为"插件即将注册"和"应用已完成初始化"两个时机。

import { Plugin, Univer } from '@univerjs/core'; export class BusinessPlugin extends Plugin { constructor(private univer: Univer) { super('business-plugin', univer); } onStarting(): void { // 注册命令、监听数据变更 } onReady(): void { // 应用就绪,执行初始化逻辑 } } univer.registerPlugin(BusinessPlugin);

我在业务插件里做过的最典型操作是:监听单元格变更,把修改过的单元格自动打上背景色标记,方便数据审核人一眼看到变化。命令系统里可以直接订阅命令执行结果,而不必去 diff 整个表格数据。

4.2 用命令系统扩展菜单与快捷键

Univer 所有的菜单项和快捷键都映射到命令,所以向自定义菜单项绑定业务逻辑,本质上就是注册一个新命令。

univer.registerCommand({ id: 'custom.generateReport', type: CommandType.MUTATION, handler: (context, params) => { // 处理业务逻辑 const sheet = context.workbook.getActiveSheet(); return true; }, });

把命令注册后,通过插件在对应菜单插槽里挂载按钮,用户点击时执行这个命令。这套模式下,业务逻辑和 UI 解耦,未来即使换了外层皮肤,命令逻辑也不用重写。

4.3 我在 Univer 里做的 AI 联动实验

这是我觉得最兴奋的部分。既然表格操作全部命令化,我尝试把大模型对表格操作的自然语言解析结果直接拼成命令执行。

我们做的实验逻辑很简单:用户在侧边栏输入"把销售额大于 10 万的行标红",后端大模型返回一组合适的操作指令,前端把这些指令映射为 Univer 命令批量执行。本质上就是把"自然语言"翻译成"命令序列",而不是直接操作 DOM 元素。

这个方案能跑通的前提,正是 Univer 命令系统的完整覆盖度。它几乎把所有用户操作都抽象成了命令,AI 生成命令、执行命令就成了一个纯逻辑链条。如果换成传统 DOM 操作的表格库,这一步复杂度会高到几乎不可行。

4.4 获取选区、单元格与 Range 对象

AI 联动里频繁会用到选区 API,这里列几个我常用的:

const workbook = univer.getActiveWorkbook(); const sheet = workbook.getActiveSheet(); // 获取当前选区 const selection = sheet.getLastSelection(); const range = selection?.getRange(); // 读取选中区域的值 const values = range?.getValues(); // 跨工作表操作 workbook.getSheets().forEach((s) => { console.log(s.getName(), s.getRowCount(), s.getColumnCount()); });

命令处理函数的上下文里也能拿到workbook和sheet,在业务插件里写逻辑时优先用上下文参数,而不是从全局实例再获取,这样更利于测试和复用。

5. 踩坑清单、性能边界与实战体会

5.1 大数据量下的实测表现

我在一台 2020 年的 Intel MacBook Pro 上测试过:单 Sheet 写入 20 万行、8 列数据,初始化渲染耗时约 2 秒左右,滚动流畅度基本维持在 30-60 帧之间;定位到某个单元格时,Canvas 渲染引擎不需要重绘整个视图,只刷新当前视口内的内容,所以滚动体验远比传统 DOM 实现稳定。

但是真的不要拿 Univer 当数据库表格。如果你有"大数据量 + 复杂筛选 + 单元格绑定业务对象"的需求,合理的架构是:表格只渲染当前屏幕可见的数据,后端保存全量数据,通过虚拟滚动和分片查询与前面对接。Univer 本身支持数据分片加载,但怎么做仍然取决于你的后端设计。

5.2 协同模式需要注意的部署问题

如果做多人协同,前端只是其中一半,另一半是协同服务端。这部分需要你自己根据官方文档或社区实现去搭建。我建议在小流量场景先跑通单机版本,同时规划好 WebSocket 服务的部署容灾。协同场景下对网络稳定性要求很高,一旦消息乱序或丢失,前端会抛出冲突错误,对用户来说就是"内容对不上了"。

保守建议:没有专门的实时通信开发经验,先用"保存到服务端 + 通知刷新 + 乐观锁"的半协同模式,也叫伪协同,上线风险会低很多。

5.3 样式冲突与 iframe 嵌入的注意事项

Univer 自带整套 UI,引入后对宿主页面的 CSS 有影响。我在项目里遇到过几次明显的样式冲突:全局重置了button样式,导致 Univer 工具栏按钮边框消失;宿主页面设了box-sizing: border-box,需要仔细检查套件内部布局是否受影响。

最稳妥的方式是把 Univer 放进一个CSS 隔离的容器/iframe。嵌入式仪表盘建议用 iframe,避免剪贴板权限、快捷键拦截和 z-index 三层问题同时爆发。如果你不想用 iframe,就要做好样式全量隔离:容器内所有元素继承box-sizing,显式设置z-index,在宿主样式表里对.univer-*相关类做白名单。

5.4 版本迭代速度和升级建议

Univer 的版本迭代非常快,我在写文章时它还在高速增加新功能和 API 调整。这意味着两件事:第一,社区资料和博客文章经常过期,看文档要以官方 GitHub 仓库为主;第二,项目引入后要把版本锁死,并且建立升级测试流程,不能"顺手升一版"。

我这边目前的策略是:

  • 使用 exact 版本锁定,升级时走专门的兼容性分支;
  • 关注官方 release notes,每个大版本先跑一遍内置 demo 回归;
  • 对核心 API 的调用统一封装成业务侧的工具模块,一旦升级导致断点,只需要在封装层修补。

5.5 我的整体判断

Univer 是目前开源阵营里最有可能把"在线表格能力"做成熟做完整的一个项目,它背后有 Luckysheet 社区积累和全职团队持续投入,架构上起点高,生态也在快速成型。我对它的期望不只是"项目里多了一个表格组件",而是未来它可以演化成一整套自托管的在线文档协作底座,和业务系统的集成深度还会继续扩展。

如果你现在要选型,我的建议是先用三天时间跑一个最小 Demo,把"读取后端数据→展示→二次编辑→保存回数据库"这条完整链路打通,再评估它是否适合你的场景。结合我上面的实战细节,这个验证过程应该不会太难。

返回列表