☰
Univer:开源Web端Excel表格组件,轻松嵌入前端项目
2026/11/5 4:59:42 网站建设 项目流程

1. 项目概述与核心价值

先说结论:Univer 是一个开源的 Web 端 Office 套件,主打“把 Excel 级别的电子表格能力搬进浏览器”。它不是一个插件,不是一个在线表格 SaaS 服务,而是一个你可以直接嵌进自己项目的组件库,像引入 ECharts 一样引入进来,然后通过配置和 API 控制整个表格。

我最早接触 Univer 是因为团队要做一套内部的数据分析后台,老板要求“所有导出 Excel 的数据必须能在网页上直接看到、直接改、直接算”。一开始我们试过在页面上渲染 HTML 表格,公式完全没法用;也试过嵌入谷歌表格,数据安全和服务稳定都是问号;后来找到了 Univer,才算是找到了正确的方向。

它的核心价值用一句话概括:用前端技术复刻桌面版 Excel 的核心体验。包括单元格编辑、公式计算、条件格式、筛选排序、数据透视表、图表,甚至多人实时协同编辑。而且它是 MIT 协议,商用不受限,代码完全开放。这意味着你不需要再为“线上表格到底选谁家”纠结,也不需要被迫接受厂商的锁定,一切都可以捏在自己手里。

适合谁看?如果你是前端开发,想给项目加一个轻量的表格能力;如果你是产品经理或项目负责人,正在评估自研表格方案的可行性;或者你只是好奇“开源项目到底能不能做到 Excel 这么强”的普通开发者——这篇内容都能给你一个参考。我会从设计思路、核心 API、实操步骤、踩坑实录几个维度展开,尽量避开官方文档里没说透的细节。

2. 整体架构与设计解析

2.1 为什么选择前端组件方案而不是纯网页表格

我们先聊一个最基础的问题:为什么浏览器里的表格难做?因为 Excel 的交互模型太复杂了。你点击一个单元格、输入一个公式、拖动填充柄,底层涉及的是数据模型、命令系统、渲染引擎、公式引擎的多层协同。普通的<table>标签只能展示静态数据,连“A1 + B1”这种最简单的计算都要你自己写 JS 去处理,更别提什么条件格式、合并单元格、历史撤销了。

Univer 的架构思路是把这些交互粒度和数据模型完全拆开。它的底层是一个命令驱动的状态机:每次操作(输入、删除、调整列宽)都会生成一个 Command,Command 修改不可变的状态树(Workbook / Sheet / Cell),然后状态树的变化再触发指定区域的 UI 刷新。这个设计和 Redux 的哲学几乎一模一样,好处是:

  • 所有操作都可以被记录、回放、撤销、重做;
  • 多人协同可以直接复用同一套命令队列,只需要一个远端转发命令的服务;
  • 渲染层只管绘制“当前状态”,不需要自己维护脏数据。

所以,Univer 与其说是一个“表格控件”,不如说是一个“表格操作系统”。你拿到手的是一个内核,外壳完全可以按自己的业务需求去定制。

2.2 核心技术栈与渲染引擎细节

Univer 的核心渲染不是用 DOM 画格子,而是用Canvas 绘制所有网格区域。为什么不用 DOM?因为一个 100 行 x 20 列的表格就有 2000 个格子,每个格子里如果都有一个输入框、边框、背景色,浏览器 DOM 树的压力会非常大。Canvas 一次性把所有可见区域绘制成一张位图,性能要高出几个量级。

具体来说,Univer 的渲染分为三层:

  1. 数据层(Core + Sheet / Docs / Slides 模块):管理单元格数据、样式、公式、合并单元格状态;
  2. 命令层(Command 服务):负责所有操作的注册、分发、撤销、重做;
  3. 渲染层(CanvasRenderer):遍历可见视口(Viewport)内的行列,计算位置、绘制背景、边框、文本、选区高亮。

公式引擎也是重头戏。Univer 没有自己从零写一个公式解释器,而是集成了fasterFormulaEngine和Formula Engine两套可选方案。默认的公式引擎支持绝大多数 Excel 函数,包括 VLOOKUP、SUMIF、INDEX/MATCH 等。在按下=号时,会走一个“公式编辑器”的独立流程,命令式地解析并追踪单元格之间的依赖关系,实现类似 Excel 的“自动重算”。

再看协同编辑。Univer 的协同方案是“无头多用户房间 + 操作转换”。简单讲就是:每个用户的每个操作都会提交到服务端,服务端往所有在线客户端广播,并处理并发操作的冲突。前端只需要接入配套的 SDK 服务(univer-sdk-server),或者自己基于 WebSocket 实现一套事务协商。后者成本高,但自由度高;前者适合快速验证。

2.3 模块化设计带来的扩展价值

Univer 的模块设计非常类似于插件化架构。它核心分了几个模块包:

模块职责范围
@univerjs/core数据模型、命令基础、生命周期
@univerjs/sheets电子表格交互,包括选区、编辑、样式、格式
@univerjs/sheets-ui表格 UI 组件,比如工具栏、右键菜单、公式栏
@univerjs/sheets-formula公式引擎、函数库、计算链
@univerjs/sheets-data-validation数据校验:下拉列表、范围限制等
@univerjs/ui基础 UI 框架,支持主题定制

这种模块化的好处非常明显:如果你只需要一个“只读展示数据”的报表,可以不加载公式模块和 UI 模块,打包体积能少 30% 甚至更多。如果需要看效果,可以用官方提供的白板模式快速集成;需要深度定制,也可以逐模块按需引入。

3. 从零搭建:五分钟跑通一个 Univer 实例

3.1 环境准备与依赖安装

我以 Vite + React 项目为例,其他框架(Vue 3、原生 JS)的引入方式也类似。先创建项目:

npm create vite@latest univer-demo -- --template react-ts cd univer-demo npm install

然后安装 Univer 的相关包。这里要注意,Univer 的包名已经从早期的@univerjs/core@univerjs/sheets升级过,最新的版本(目前稳定在 0.x 系列)中,某些 API 与旧版不兼容。我强烈建议直接用最新版,并锁定版本:

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

还需要安装一个图标库支持,因为工具栏里有大量图标:

npm install @univerjs/icons

3.2 基础初始化的完整代码

在src/main.tsx中先引入基础样式和注册模块:

import React from 'react'; import ReactDOM from 'react-dom/client'; import { LocaleType } from '@univerjs/core'; import { defaultTheme } from '@univerjs/ui'; import { Univer } from '@univerjs/core'; import { UniverSheetPlugin } from '@univerjs/sheets'; import { UniverSheetUIPlugin } from '@univerjs/sheets-ui'; import { UniverFormulaPlugin } from '@univerjs/sheets-formula'; // 引入所有依赖样式 import '@univerjs/ui/lib/index.css'; import '@univerjs/sheets/lib/index.css'; import '@univerjs/sheets-ui/lib/index.css'; import '@univerjs/sheets-formula/lib/index.css';

然后在 React 组件的useEffect里初始化 Univer 实例。这里有一个很重要的点:Univer 的实例是一个全局单例,同一个页面上不能重复创建,否则会报 “Already exists” 错误。因此我们用 ref 缓存,避免热更新时重复初始化。

import { useEffect, useRef } from 'react'; function App() { const containerRef = useRef<HTMLDivElement>(null); const univerRef = useRef<Univer | null>(null); useEffect(() => { if (!containerRef.current) return; if (univerRef.current) return; const univer = new Univer({ locale: LocaleType.ZH_CN, themes: { default: defaultTheme }, locales: {}, }); univer.registerPlugin(UniverSheetPlugin); univer.registerPlugin(UniverSheetUIPlugin); univer.registerPlugin(UniverFormulaPlugin); univerRef.current = univer; }, []); return <div ref={containerRef} style={{ width: '100%', height: '90vh' }} />; } export default App;

等等,如果你只执行上面这段代码,页面上并不会出现表格。还需要一个关键的步骤:把 Univer 实例挂载到一个 Workbook 数据上。Univer 组件不是“可视化编辑器”模式直接编辑空白文档,而是提供了一个工作簿容器,你需要给它传入初始数据或者数据源。

最常见的初始化方式是通过univer.createWorkbook():

useEffect(() => { if (!univerRef.current) return; const workbook = univerRef.current.createWorkbook({ sheets: [ { name: '销售数据', id: 'sheet1', cellData: { 0: { 0: { v: '商品' }, 1: { v: '销量' }, 2: { v: '单价' } }, 1: { 0: { v: '苹果' }, 1: { v: 120 }, 2: { v: '5.5' } }, 2: { 0: { v: '香蕉' }, 1: { v: 200 }, 2: { v: '2.5' } }, }, }, ], }); }, []);

这里的cellData结构是“行号: { 列号: { v: 值 } }”,v是显示的值,除此之外还有s(样式)、t(值类型)、f(公式)等字段。我习惯叫它“单元格描述的 JSON 方言”,不过刚开始接触时容易记混,稍后我会在实操部分专门讲几种常见写法。

3.3 初始化后的关键配置

初始化时还需要注意几件事:

第一,默认皮肤和主题。defaultTheme可以直接用,如果想改表格边框色、工具栏背景色,最快的方法是在 CSS 里覆盖变量名,比如:

:root { --primary-color: #1677ff; --primary-color-hover: #4096ff; --background: #ffffff; }

第二,公式插件和 UI 插件的注册顺序。注册顺序应该先注册基础插件(UniverSheetPlugin),再注册 UI 插件,最后注册公式插件。如果先注册 UI 再注册基础插件,工具栏的某些操作(比如新建、删除工作表)可能因为找不到数据模型而报错。虽然不是 100% 必现,但顺序对了能避免一批问题。

第三,locale 语言设置。在 Univer 的初始化参数中,locale决定默认界面语言,locales字段要传入语言包。目前中文语言包需要单独安装和注册,否则界面显示英文。如果需要中文本地化,记得:

npm install @univerjs/locale

然后在初始化时引入中文语言包:

import { zhCN } from '@univerjs/locale'; const univer = new Univer({ locale: LocaleType.ZH_CN, locales: { [LocaleType.ZH_CN]: zhCN }, // ... });

4. 实操过程:数据的读写、公式、样式与事件

4.1 如何用 JSON 描述一个表格

我上面给的cellData是 Univer 的底层数据结构,但实际业务场景中,你往往需要动态生成这个 JSON。最常见的方式是从后端接口拿到一个二维数组,再转换成cellData。我写了一个简单的转换函数:

interface CellDataRow { [columnIndex: number]: { v: string | number | null; t?: number; f?: string }; } function arrayToCellData(data: (string | number | null)[][]): Record<number, CellDataRow> { const cellData: Record<number, CellDataRow> = {}; data.forEach((row, rowIndex) => { row.forEach((value, colIndex) => { if (value === null || value === undefined || value === '') return; cellData[rowIndex][colIndex] = { v: value }; }); }); return cellData; }

这里有几个细节:cellData这个对象在 Univer 中并不是普通的对象,它内部会被转换成CellMatrix之类的数据结构。如果你的数据量很大(比如十万行),建议不要一次性在代码里生成一个大 JSON 对象塞进去,而是使用 Univer 提供的高级 APIsetRangeValues,它可以分片写入。

4.2 通过 API 读取和修改单元格值

初始化之后,你可以通过workbook.getActiveSheet()拿到一个工作表实例。这里最常用的几个操作:

const sheet = workbook.getActiveSheet(); // 读取某个单元格 const cell = sheet.getCell(3, 1); // 第4行,第2列(从0开始) console.log(cell?.v); // 设置某个单元格 sheet.setValue(3, 1, '新值'); // 获取范围内所有数据 const range = sheet.getRange(0, 0, 10, 5); // 0,0 左上角,10行5列 console.log(range.getValues()); // 设置公式 sheet.setFormula(3, 2, '=SUM(C1:C5)');

注意坐标全是 0 索引的,和 Excel 显示的行号列号差 1。写代码时容易混,我踩过坑:某次从数据源里取sheet.getCell(0, 0)以为是 A1,结果取的是 A0(不存在),特指行号越界报错。建议封装一个工具函数:A1ToRowCol('A1')来做转换。

4.3 应用样式与条件格式

Univer 的样式不算特别丰富,但常规的加粗、背景色、边框、字体颜色、对齐都有。给单元格加样式是在cellData的s字段或者通过 APIsetRangeStyle设置:

import { BorderStyleTypes } from '@univerjs/core'; const style = { bl: { s: BorderStyleTypes.THIN, cl: '#FF0000' }, // 左边框 bg: '#FFFF00', // 背景色 ht: 2, // 水平对齐 vt: 1, // 垂直对齐 }; sheet.getRange(0, 0, 5, 3).setStyle(style);

条件格式在 Univer 中比较有意思。它不是简单地传一个规则对象,而是要使用ConditionalFormattingRule的注册机制。比如要做一个“销量大于 150 的单元格背景变绿色”的规则:

import { ConditionalFormattingRule } from '@univerjs/sheets-data-validation'; const rule = new ConditionalFormattingRule(sheet, { ranges: [{ startRow: 1, startColumn: 1, endRow: 100, endColumn: 1 }], rule: { type: 'cellIs', operator: 'greaterThan', formula: '150', style: { bg: '#00FF00' }, }, });

不过这个 API 在不同版本的 Univer 中有调整,如果你用的是 0.1.x 之前的老版本,写法完全不同。遇到版本变动时,优先查对应版本的 README。我建议先通过 UI 面板手动设置一个条件格式,再在 DevTools 里查看它生成的 Rule 对象,这就是快速摸清 API 的最佳方法。

4.4 数据验证(下拉选择)

数据验证是业务后台很常用的功能,比如“库存状态”列只能选“有货/无货/停产”。Univer 提供了表单式验证:

const dataValidation = { type: 'list', formula: ['有货', '无货', '停产'], showDropDown: true, }; sheet.addDataValidation(2, 3, 100, 3, dataValidation);

这样在 C3:C100 区域内,点击单元格就会出下拉箭头。注意,Univer 的数据验证与 Excel 的严格的“拒绝输入”不同,它默认只是警示,不会强制阻止非法输入。如果需要强制执行,需要设置strict: true一类的参数,不过当时我用的版本还没暴露这个选项,只能通过>const subscribe = sheet.onCellChange((cellInfo) => { console.log('单元格变化:', cellInfo.row, cellInfo.column, cellInfo.cell?.v); });

这里要特别注意:事件回调不能直接修改单元格(会导致循环或卡死),必须把修改动作放在命令监听之后或通过setValue异步触发。

关于协同,Univer 官方提供了非常完整的协同 SDK 方案。通过 WebSocket 与 Univer Server 通信后,多人编辑同一份工作表的数据结构时,所有人的状态会实时同步。自研协同的坑比较深,涉及版本号、时间戳、操作合并、光标位置广播、在线状态管理。如果你只是个普通前端,建议直接使用官方协作组件,不要轻易自己实现。

5. 常见问题与排查技巧实录

5.1 表格不显示,页面上一个空白的坑

这是新手最常见的坑。80% 的情况是因为没有注册对应的 UI 插件,或者没有给容器设置高度。Univer 的容器默认高度是 0,所以在style里必须给一个明确的高度。还有 20% 的情况是初始化了多个 Univer 实例,页面报错Univer instance already exists。解决方案就是我在前面说的:全局用 ref 缓存单例。

5.2 公式不计算,单元格显示 #NAME?

这一般是公式引擎没有注册成功。检查一下你是否安装了@univerjs/sheets-formula并调用了univer.registerPlugin(UniverFormulaPlugin)。还有另一种可能,是你写入公式时没有加=前缀。Univer 内部存储公式时,f字段的值应该为=SUM(A1:A5),如果你的数据接口传错了,也会出现此类问题。建议在设置公式后,调用sheet.getCell(...)看一眼f字段的原始值,能非常快地定位问题。

5.3 大批量数据渲染卡顿

Univer 的 Canvas 渲染确实比 DOM 表格快很多,但也不是完全无敌。如果一次性通过setRangeValues写入一万行 x 二十列的数据,渲染层依然会卡。原因主要在“样式对象的序列化”和“单元格矩阵的索引更新”。我的经验是:

  1. 数据写入分批处理,每批 500 行,用requestAnimationFrame控制节奏;
  2. 如果只是展示数据,不要给每个单元格设置独立样式。尽量减少s字段的使用,统一用整行的默认样式;
  3. 打开工具栏的“性能模式”或关闭滚动时的平滑动画(如果有这个选项)。

5.4 协同编辑时的冲突和光标丢失

如果使用自研协同,最常见的问题就是多个客户端同时修改同一个单元格时,出现“后者覆盖前者”。Univer 的 Local State 是基于命令的,解决冲突的方法是给每条命令设置为原子操作,再做转换(OT 或 CRDT)。建议不要自己写转换逻辑,而是直接使用官方支持的工具链,或者将同一单元格的编辑权限做锁定(比如服务端校验单元格版本号,不匹配返回失败)。

5.5 中文输入法下输入慢、丢字

这是一个比较小众但真实的问题。Univer 的历史版本中,对中文输入法(如搜狗、微软拼音)的组合输入处理不够完善,快速输入时可能出现字符丢失。2024 年之后的新版本已经大幅优化了这块,但如果你是老版本,建议升级。如果升级后还有问题,检查是否在onCellChange事件中做了重绘操作,重绘会打断输入法的组合状态。

6. 实用技巧与二次开发建议

6.1 把 Univer 封装成 React 组件

实际项目中,不要直接在每个页面都写初始化代码。我建议封装一个<UniverSheet data={jsonData} config={...} />组件。数据处理、初始化、事件绑定、插件注册都收敛在组件内部,外部只传数据源和回调函数。这样可以避免 Univer 实例的重复创建,也方便做路由切换时销毁和重建。

销毁实例时不要只设置univerRef.current = null,最好调用univer.dispose()释放事件监听和 Canvas 上下文。我发现不 dispose 的话,某些浏览器下页面离开后 Canvas 内存不会释放,多次切换路由会导致内存占用持续增长。

6.2 定制工具栏和右键菜单

Univer 的工具栏默认提供了一整套“桌面办公”的按钮,很多在 Web 场景下没用。你可以通过配置项或 CSS 隐藏不需要的按钮。如果想要完全自定义,比如加一个“导出 CSV”按钮,可以在UniverSheetUIPlugin的初始化配置中注册自定义命令:

univer.registerCommand('custom.exportCsv', { handler: () => { // 读取当前工作表数据,生成 CSV 并下载 }, });

然后在工具栏模板里插入你的按钮。这套定制逻辑和 Univer 的整体命令系统是统一的:所有 UI 动作都映射到一个 Command,所以自定义功能本质上就是“注册一个新的命令”。

6.3 与后端数据源同步的策略

如果你只是用 Univer 做前端展示,数据同步很简单。但如果你要做一个“在线编辑后台”,需要考虑“本地编辑 - 服务器存储 - 实时同步”的闭环。我的建议是:

  1. 进入页面时用只读模式加载数据,不开启编辑;
  2. 用户点击编辑按钮后,从服务器拉取一份编辑副本;
  3. 通过 debounce 监听单元格变化,每 500ms 把改动汇总成 Patch 提交给服务器;
  4. 服务器存版本号和细粒度的操作日志,用于回溯。

6.4 打包体积和性能优化

Univer 本身不算小,所有模块引入后 gzip 大概在 600KB 左右。对于中后台应用是可接受的,但对于面向 C 端的页面需要优化。主要的做法是动态导入(React.lazy 或 Vite 的按需加载),把 Univer 单独做成一个异步模块,首屏不加载。还可以在vite.config.ts中把它的相关包设置成 manualChunks,避免和业务代码混在一个包里,提升浏览器缓存命中率。

下面是我在 Vite 中的分包配置参考:

build: { rollupOptions: { output: { manualChunks: { univer: ['@univerjs/core', '@univerjs/sheets', '@univerjs/sheets-ui', '@univerjs/sheets-formula', '@univerjs/ui'], }, }, }, },

6.5 样式覆盖的正确姿势

Univer 的 UI 使用 CSS Modules,类名是经过 hash 的,直接覆盖类名非常困难。官方推荐方式分为两种:一是通过主题变量,二是通过ix前缀的自定义 class 覆盖。实际操作中,我对工具栏的 Logo、顶部高度做调整时,基本都是用:global(.univer-toolbar) { height: 48px; }这种全局样式。注意要给选择器加上!important,防止被内部样式覆盖。

7. 我对 Univer 的持久体验与项目适配建议

如果你要问 Univer 能不能替代 Excel,我会说:在 Web 环境下,它是目前开源社区最接近 Excel 的体验选择。它的数据模型、命令系统、Canvas 渲染和模块化设计都非常成熟,已经不再是一个“玩具框架”。在我自己的项目里,用 Univer 完成了数据填报、结果查看、轻量化报表生成三个场景,除了协同编辑投入成本较高外,其他功能几乎不需要太多额外开发。

但它也不是没有短板。

第一,版本迭代太快,破坏性更新频繁。我在从早期 alpha 版本更新到 0.2 系列时,几乎每个大版本都要改一遍 API。如果你的团队要基于它做长期项目,一定要把 Univer 所有依赖锁死版本,并且在升级前查阅 CHANGELOG。

第二,文档覆盖度还不够。很多 API 只有 TS 定义,没有完整示例,遇到问题时只能去 GitHub issues 找人提问。这个原因导致团队上手时要预留 1~2 周的学习成本。好在官方社区比较活跃,提问响应速度还不错。

第三,复杂图表能力弱于 ECharts。Univer 内置的图表只覆盖柱状图、折线图、饼图这些最常见的形态,复杂组合图建议还是把它当数据操作层,把数据抛给 ECharts 做可视化。

最后给一个小建议:如果你只是想“快速在网页里展示一张表格”,不一定要用 Univer;但如果你需要真正的交互式电子表格体验,并且希望保留二次开发空间,Univer 是我目前最推荐的方案。它值得你花一个下午去跑一个 Demo,再花一周去熟悉 API。投入产出比非常可观,而且随着版本成熟,未来很可能会成为开源办公组件里绕不开的一个名字。

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

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

立即咨询