如果你和我一样,被问过“能不能在我们系统里加一个在线表格”这种需求,应该能理解我见到 univer 之后为什么一直盯着这个项目。Web 端做个“长得像 Excel”的表格界面并不难,难的是要做出真正能用的:公式要自己算、样式要兼容 xlsx、撤销重做得顺手、最好还能多人同时编辑。大多数团队走到一半就倒在了这些细节上。univer 就是冲着“把这些事情一次解决”去的开源项目——一个可以嵌入到任意 Web 应用的类 Excel 办公套件,尤其是一站式的电子表格方案。
我把它接进过内部数据填报平台,也拿它跑过几十 MB 的导入导出,中间踩了不少坑。这篇就把我这个阶段的完整实践写出来:univer 适合干什么、怎么快速跑起来、核心有哪些机制必须理解、应用到实际系统里要注意什么。尽量贴实际操作,不堆概念。
1. univer 是什么:不止是一个“网页版 Excel”
1.1 为什么我会注意到这个项目
先说需求场景。我们当时要做一个面向运营人员的数据录入后台,需求列表里有一条:支持类似 Excel 的批量编辑,要能复制粘贴、有公式、会自动汇总。一开始我直接上了 HTML table 套 contenteditable,写到第 200 行数据就发现滚动卡顿,复制粘贴跨选区时浏览器自带的文本语义完全不受控。后来又尝试在页面里嵌一个 iframe 的在线文档,但客户又要求私有化部署,数据不能过第三方服务器,这条路也封死。
univer 进入视线,是因为它满足了三个硬条件:
- 开源,Apache 2.0 协议,可以私有化部署,不用把用户数据送到别人的服务里。
- 前端集成,不是独立应用。它可以作为组件嵌进 Vue 或者 React 系统里,成为系统内部的一个页面能力。
- 工程化完整。公式、样式、撤销重做、筛选、排序、条件格式、xlsx 导入导出等高频能力都覆盖到了。
它不是“又一个扣 Excel 样子的网页表格”,而是一个把电子表格的运算核心、渲染内核、交互 UI 拆分得非常清晰的组件化套件。
1.2 univer 的整体架构:一堆可拆分的包
第一眼看到 univer 项目结构,容易懵:npm 包太多了。@univerjs/core、@univerjs/sheets、@univerjs/sheets-ui、@univerjs/engine-formula、@univerjs/engine-render、@univerjs/design……我最初也以为官方在炫技,但实际用下来,这种拆分是刻意的。
它把“数据模型”“渲染引擎”“计算引擎”“UI 框架”“交互逻辑”拆成了独立模块。你可以只要一个空白表格壳子,也可以把公式引擎塞进去,再单独决定要不要带预设的工具栏。这种模块化带来的好处是:
- 体积可控。你如果只需要预览和编辑,不吃公式,那完全可以不引入公式引擎相关包。
- 扩展有边界。新增一个自定义单元格渲染,只需要往渲染层挂自定义绘制逻辑,不用去改核心数据模型。
- 协同或其他高级能力作为插件挂进来,核心层面保持稳定。
反过来,这也是项目让人“劝退”的点:刚接触时不了解每个包是干什么的,照着网上旧教程装了一堆包,结果版本对不上,界面渲染不出来。后面我会单独讲怎么规避这个问题。
1.3 它在开源表格赛道里的位置
市面上的前端电子表格方案其实不少,但大多数要么停在“演示级”,要么定位是“解析库”而不是“编辑器”。univer 站的位置很特殊:它更像一个可以自托管的 Google Sheets 内核,具备完整的编辑器形态,同时提供的是代码层面的 API 集成能力,而不仅仅是最终产品。
从产品形态上看,univer 虽然借用了桌面电子表格的操作范式,但它不要求你按“一个独立站点”去使用。它在你的单页应用里其实是一个“表格区域”,可以只占半屏,也可以全屏,可以和页面其他报表组件联动。这一点对我做集成非常关键:客户要的不是多一个 Excel 软件,而是把表格能力长在业务系统里。
2. 第一次把 univer 跑起来:最简操作路径
2.1 环境准备和依赖安装
我使用的是 Vite 项目。为什么选 Vite?像 univer 这类现代前端库,源码本身就是 ESM 风格,Vite 开发服务器下热更新更快,能直接消费 npm 包里的lib目录构建产物,基本不用额外配置。相比 Webpack 要手动配 tree-shaking,Vite 的开箱体验要省事很多。
安装核心依赖:
npm install @univerjs/core @univerjs/preset-sheets @univerjs/preset-formula这是目前 0.3.x 版本的推荐方式。preset-sheets把表格主体、UI、导入导出的常用插件都打包整理了一遍,preset-formula负责公式计算引擎。如果之后不需要公式,第二个包可以不装,首屏加载体积会小不少。
温馨提醒:univer 的 API 在 0.1 到 0.3 之间调整相当大,网上很多 2023 年的代码已经不能直接用了。你安装完包之后,先去看
node_modules/@univerjs/preset-sheets/dist/index.d.ts里的类型导出,或者直接打开官方文档的导入示例核对,再用它写代码。
2.2 初始化代码:三步得到一张可编辑表
核心代码其实很短,核心是“new Univer,装插件,创建单元”这三个动作:
import { Univer, UniverInstanceType } from '@univerjs/core'; import { UniverSheetsPreset } from '@univerjs/preset-sheets'; import { UniverFormulaPreset } from '@univerjs/preset-formula'; import '@univerjs/preset-sheets/lib/index.css'; const univer = new Univer(); new UniverSheetsPreset().install(univer); new UniverFormulaPreset().install(univer); const sheetInstance = univer.createUnit(UniverInstanceType.UNIVER_SHEET, { data: [ ['项目', '本月收入', '本月支出'], ['在线表格', 10000, 3000], ['API 接口', 5000, 1200], ], rowCount: 100, columnCount: 20, });createUnit创建了一个表格文档单元。第二参数可以直接给一个二维数组作为初始数据,也可以传一个能描述行数、列数、样式、合并单元格的完整 JSON。这个 API 设计让我觉得挺顺手:进页面时从后端拿数据,直接把数组灌进去,不需要写循环 setValue。
页面里需要一个容器节点,设置明确宽度和高度,univer 通过精确布局把表格画在容器内。如果把容器的高度写成auto,会出现表格内部滚动区域计算成 0 的问题,界面只显示工具栏,看不见表格。这是我踩过的第一个雷。
2.3 打开 xlsx 的导入导出能力
刚接触 univer 时,我以为默认就能导入 xlsx,结果页面里怎么找都看不到入口。后来查文档才明白,导入导出插件需要额外安装并配置对应的预设依赖:
npm install @univerjs/preset-import-xlsx @univerjs/preset-export-xlsx然后在初始化时安装:
import { UniverImportXlsxPreset } from '@univerjs/preset-import-xlsx'; import { UniverExportXlsxPreset } from '@univerjs/preset-export-xlsx'; const importPreset = new UniverImportXlsxPreset(); const exportPreset = new UniverExportXlsxPreset(); importPreset.install(univer); exportPreset.install(univer);安装后,工具栏的“打开文件”和“下载 xlsx”按钮就会出现了。这个“默认不装”的设计一开始让我觉得绕了一圈,但后来理解了:导入 xlsx 需要内置一个解析器,解析器的体积不小,如果系统内不涉及文件导入,完全没必要让用户为这个能力付出首屏加载代价。
我还做过一个更省事的方式:服务端直接解析 xlsx,生成 univer 需要的 JSON 数据结构,前端通过createUnit渲染。这样前端不需要 import-xlsx 预设,体积进一步变小。缺点是服务端要做一版自己的 xlsx 读取逻辑。如果项目里已经广泛使用 Java/Python 处理 Excel 文件,这个方案是个很自然的取舍,可以直接沿用团队已有的解析工具。
3. 必须理解的几个核心机制:否则改起来心里没底
3.1 命令系统:撤销重做为什么能成立
我第一次尝试“代码改单元格内容”时,习惯性地想去找类似sheet.setCellValue()这样的方法。真正深入后才发现,univer 里所有修改表格数据的动作,本质上都是往一个核心“命令系统”里投递的指令。命令被执行、被记录、被进入撤销栈;UI 上的输入、快捷键、工具栏按钮,最终都走同一条命令链路。
这样设计的好处是显而易见的:只要统一了入口,撤销重做、协同冲突处理、审计日志就都在同一层处理了。改数据不再散落在各个 UI 事件里。我写业务扩展时,会通过 api 层提供的操作封装去发起命令,而不是直接改内部对象。
不过这里有个实际提醒:命令名在不同版本变动频繁,写代码时不要凭记忆,要随时用类型提示去确认。我记得初版用sheet.mutation.setRangeValues,某次升级后就变成了x-sheet.command.set-range-values。如果你手头没有现成代码库,打开@univerjs/sheets的类型声明搜索COMMAND_ID或CommandId,能推进得顺利很多。
3.2 公式引擎:它是独立于 UI 线程的计算模块
univer 的公式不是用 JS 的eval或者临时写一个解析器硬算的,而是一个相对独立的公式引擎。我理解它是一个有依赖追踪能力的计算中心:读取公式字符串、解析语法、构建公式依赖图、逐个单元格计算、缓存结果。单元格中写=SUM(A1:A3),UI 先渲染公式文本,计算引擎在后台得出结果后,再把结果显示回表格。
这个机制带来的影响有两个:
- 公式是异步计算的。如果你用 API 写入公式后立刻去读单元格结果,大概率拿到的是暂未计算的占位值。要监听结果变化,或者等计算任务跑完之后再读数。
- 公式计算的性能和 UI 渲染性能互相隔离,复杂大文件不会把页面交互完全卡死。实测下来,几千个带公式单元格的表格,输入时仍能保持比较顺滑的手感。
如果有同学想在这个层面做深度扩展,比如接入自定义函数,最重要的一点是:不要在公式字符串里套太多前端业务逻辑,优先把它实现成纯函数,输入参数和输出逻辑都保持确定,这才能让依赖追踪正确工作。
3.3 渲染引擎:为什么滚动性能不错
univer 的单元格区域并不是一堆 HTML table 元素,而是用统一的渲染引擎在 canvas 上绘制。这样做对战了大表格渲染性能非常重要:整张表拆分成可视区域内的网格,滚动时只重新绘制当前可视区,而不是创建几千上万个 DOM 节点。
我第一次体会到这个差异是在一个 5 万行数据的表格里,拖动滚动条时操作跟手,完全没有“白块”和卡顿。相较之下,之前 contenteditable 方案到五千行就开始有明显迟滞感。
渲染引擎还有一个高级能力:自定义单元格绘制。比如在某一列里画一个进度条、一个勾选框、一个标签徽章,都可以通过单元格自渲染逻辑来实现。这给了我做业务定制很大空间——不只是展示文本,还能把状态可视化直接嵌在表格里。
3.4 协同编辑:基于 CRDT 而不是“整体保存刷新”
多人同时编辑同一张表是很多系统负责人最先问的功能。univer 的协同是基于 CRDT 数据结构的,在多人并发的场景下,它处理的是“字级别/单元格级别的冲突合并”,而不会出现后保存的人整体覆盖先保存的人这种灾难。
理解 CRDT 的方式其实很简单:它像文档里的批注一样,每个人所有修改都以一种不互相覆盖的方式合并。每个人的光标、输入内容、删除操作,最终都能收敛到同一个结果。我用了几次多人同时改同一个单元格的测试,发现两个用户的输入虽然在界面上会短暂交错,但最终不会丢失有效内容。
部署协同需要一个传输服务。univer 提供接入 Yjs 生态的适配,意味着可以使用 y-websocket、y-leveldb 等 Yjs 社区工具来搭建后端,也可以集成自研的传输通道。实际部署时,我建议把协同后端和业务数据库分开部署,协同产生的增量日志存一段时间的多版本状态,业务数据则通过最终落库接口同步。
4. 把 univer 嵌入真实业务系统的工程化经验
4.1 控制打包体积:按需加载而不是全量上
univer 功能多,自然体积也大。如果不管三七二十一把所有插件都安装,首屏 JS 可能直接多出 1MB 以上,这对体验敏感的页面来说挺伤。我的做法是:
- 把 univer 相关代码放进一个独立的异步模块,配置动态导入,页面进入表格区域时才加载。
- 只安装实际用到的 preset。不需要导入 xlsx 就不装 import 插件;不需要协同就不装 yjs 相关包。
- 公式引擎如果确认在用,尽量保持单例复用,避免多个表格页面重复初始化新实例。
实测下来,动态加载后首屏影响可以压到可接受范围内,表格模块几乎不影响主页面加载。
4.2 与系统内数据源的读写联动
业务系统里表格通常不是孤立数据。我从一开始就设计了一套简单的桥接层:进入页面时,根据业务单据 ID 向后端请求表格 JSON,再通过createUnit或已有实例的加载接口渲染数据。编辑过程中,监听单元格变更事件,把变更收集起来,离开页面或点击保存时统一调接口提交。
有个关键点:不要在每次单元格变更时都向后端发请求。用户在一个单元格输入时,会连续触发多次变更事件,你发出去只会增加请求压力,还会把历史中间态存到数据库里。更好的方式是“防抖 + 批量提交”,比如 2 秒内合并所有变更,或者只保存最终数据样式。
读取数据时也要注意:取单元格内容不等于取公式,还要拿到“计算值”。如果页面只是展示,直接读getRangeValues的显示值即可;如果要回写数据库,需要明确设计哪部分是用户输入值、哪部分是计算派生值,避免把公式计算出来的结果当作原始数据存储。
4.3 主题定制与中文环境适配
univer 默认主题支持浅色和深色。我做的第一个定制工作是给表格区域添加了和系统一致的品牌色。主题定制主要通过设计变量来完成,不需要去改每个组件的样式。对我这种不是专业视觉设计的人来说,能在一个统一的地方改色板,比逐个菜单调整按钮样式要友好太多。
中文环境下更多要注意的是字体。canvas 渲染表格文字时,如果字体还没加载完,就会出现中文缺字形或者统一用默认字体渲染的情况。稳妥的做法是初始化 univer 之前,先用document.fonts.ready等待页面字体加载完成,再创建实例。我专门写了一个小函数,等字体状态 ready 后才执行渲染,规避了首次进入页面中文偶尔闪一下的体验瑕疵。
4.4 和 React/Vue 组件的共存方式
univer 自己管理了一套 DOM 渲染区域,和 React/Vue 的虚拟 DOM 是并行的。正确方式是:在组件挂载完成后创建 univer 实例,组件卸载时销毁实例,保证每个页面/容器只存在一个实例。
我之前看有人把new Univer写在render/setup里,组件一刷新就重复创建实例,最后页面卡死且表格闪烁。工程上不要在响应式触发的高频渲染区域里直接初始化 univer。如果你和我一样用 Vue,可以把 univer 封装成一个独立的 class 模块,组件只负责管理容器 DOM 和调用模块方法,模块自己维护生命周期。
5. 我整理过的排查清单:univer 环境里的怪问题与原因
5.1 插件明明装了,页面却没出现表格区域
这个问题最常见的原因是容器高度为 0 或 CSS 没引入。我用position: fixed容器时也踩过一次:父级 display 属性在计算时变化,导致容器没有实际尺寸。先检查getBoundingClientRect()的高度,再看是否遗漏了@univerjs/preset-sheets/lib/index.css的引入。
5.2 批量写入慢的不像话
如果写数据用了一个超大循环,每格调一次 set API,性能会非常差。因为每一次操作都会经过命令系统、变更通知、重新渲染。正确做法是把数据组织成二维数组,用批量写范围的接口一次性提交。这个优化思路和普通 DOM 操作优化其实相通:减少“操作次数”,而不是减少“数据量”。
5.3 xlsx 导入导出的格式细节
univer 对标准 xlsx 的支持日常使用够用,但和 Excel 原生的精细格式相比还有一些边界。我碰到过日期序列号差一天、复杂条件格式样式丢失、迷你图回到 Excel 后不显示等情况。规避办法是:重要文档的格式走回归测试,把 xlsx 导入后再导出,检查关键样式是否保真。不要把“格式完全一致”当成默认承诺。
5.4 升级版本后 API 频繁改名,我怎么能不掉坑
univer 还处在一个快速演进期,各个子包的版本号可能不同步,直接npm update会让某个包和自己预期出现断裂。我的经验是:锁版本号,不要用latest或^自动升级。升级迁移只依赖官方 release notes 和迁移文档,不靠猜。我之前从 0.2 升 0.3 时,约花了半天时间把所有 preset 初始化方式改了一遍,好在改动都集中在入口文件里,业务代码基本没动。
5.5 和服务端返回的数据结构打架
有时候后端给的数据是扁平的 key-value 对象,而前端表格需要二维数组。最好在前端做一个转换层,尽量不要让后端模型直接驱动表格模型。这样修改表格结构时不会影响历史接口,后端也不会因为表格行数调整被迫改变业务数据结构。
6. 选择表格能力时的几条避坑对比
6.1 和 Luckysheet、x-spreadsheet 对比
Luckysheet 是很多前端表格改造方案里常见的选项,但它对现代工程化的支持相对弱一些,npm 包的更新节奏也慢。x-spreadsheet 功能更简单,适合做个轻量 demo,真要上生产需要自己补非常多的细节。相比它们,univer 最大的优势是有活跃维护、正式的插件架构和清晰的桌面端经验继承。
6.2 和 SheetJS、exceljs 区分开
SheetJS、exceljs 是 xlsx 解析/导出库,它们的核心价值是“数据格式转换”,不负责在浏览器里渲染可编辑界面。如果你只是想读取文件、填充模板、导出报表,用这类库更轻量。univer 定位是一个完整的编辑器,如果你需要给用户一个“能操作的空间”,再用它。两者不存在二选一,实践中甚至会共存:服务端用 exceljs 生成模板,前端用 univer 做在线编辑。
6.3 与 OnlyOffice、Google Sheets 的边界
OnlyOffice 和 Collabora 是重服务端方案,可以自部署,但部署和维护成本高,适合作为独立办公系统运行。Google Sheets 和飞书类在线表格,体验成熟但无法私有化,数据也不掌握在自己手里。univer 的路线介于两者之间:编辑器在前端,数据由你自己管理,后端可以很轻。它不适合当“完整办公平台”来抵,更适合嵌进现有产品中若有若无地提供表格能力。
6.4 什么场景别硬用 univer
如果你的需求仅仅是把线上数据导出成 xlsx 报表,或者读取一个固定模板,用 exceljs 加一个导出按钮就够了,完全不需要引入整套编辑器和渲染层。如果团队里没有前端维护能力,需要的是“交付即用”的完整系统,那在线表格 SaaS 服务会更合适。univer 适合的是:你的团队具备前端开发能力,且表格能力要做成自己产品的一部分,同时有数据、样式完全可控的诉求。
7. 我们接下来在 univer 上要做的几件事
第一件是自定义业务字段控件。我们是业务系统,表格里很多单元格不是纯文本输入,而是关联了业务主键。我想在单元格里渲染一个自定义下拉选择控件,选中后写回数据的不仅是展示文本,还有隐藏的业务 ID。univer 的自定义单元格绘制能力刚好能承载这类需求,不需要把整个表格重新发明一遍。
第二件是把协同编辑接入真实业务单据。内部多人同时审核数据单的情况不少,目前方案是编辑完再提交,协作感知弱。拿到 yjs 适配后,我会先做一个只读模式下的多人光标展示,让用户能看到别人在看哪些区域,再逐步开放真正的并发编辑。
第三件是做一个模板动态配置。让运营人员自己配置表格模板,比如字段列、默认值、校验规则,然后用 univer 渲染成一个带业务校验的填写表。这个能力如果做成了,以后各种数据采集需求都不需要重新开发页面,直接配置一张表就能上线。
univer 最让我心动的一点,是它把“网页表格”从演示玩具带到了“产品级组件”的位置。它的生态还在快速成长,API 变动也频繁,但正因为底子打得好,你付出一次学习成本,换回来的是一个可持续深耕的在线表格底座。如果你也在纠结怎么在自己的项目里植入真正的 Excel 能力,也许可以找个周末拿 Vite 起个项目,照着上面最简示例跑一遍,踩过几个 API 变化的坑之后,你会对它有一个自己的判断。