ARTICLE DETAIL

资讯详情

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

Univer 开源在线表格集成实践:从架构原理到协同落地

Univer 开源在线表格集成实践:从架构原理到协同落地

如果你最近在 GitHub 上搜过“在线表格、开源 Office、Excel 编辑器”这类关键词,大概率会撞见 Univer 这个名字。它不止是一个能跑的表格组件,而是一整套基于 Web 的文档、表格、幻灯片解决方案,核心能力是协同编辑。简单讲,你可以在自己的网站里嵌一个“在线 Excel”,用户能在浏览器里编辑、计算、多人协作,而且这套东西是开源、可自行部署的。Univer 在线版可以直接在官网体验,换句话说,如果你想快速确认手感,不用先搭环境,打开官网拖一拖数据就好。它真正吸引我这种做业务系统的人的点,是把“嵌入”当头等大事来设计,而不是像某些组件库那样让你去迁就它的发布流程。下面这篇内容就用我实际跑过的流程告诉你,Univer 能接多深、坑在哪、怎么避。

1. Univer 到底是什么,值不值得研究

1.1 从 Luckysheet 到 Univer,这东西的来头

如果你在开源表格这个领域混过一阵子,应该对 Luckysheet 有印象。当时 Luckysheet 在国产开源圈挺火,因为它解决了一个非常实际的问题:在浏览器里做一个像 Excel 的组件。但用过的朋友也知道,Luckysheet 的架构核心是 DOM 渲染单元格,数据量一上来,性能就比较难受。Univer 相当于是团队用 TypeScript 从零重写的一版,把渲染换成了 Canvas,把整体做成了模块化、插件化的架构。

我第一次在本地跑 Univer 的时候,说实话是有惊喜的,一个包含几万行数据的 Sheet 拖起来很跟手,这和我之前折腾纯 DOM 表格组件的体验完全不是一个级别。要特别提醒的是:网上那些 2023 年左右 Luckysheet 的教程,千万别把里面的写法直接套到 Univer 上,两者 API 差得挺远。Univer 官网的在线版本我已经点过很多次,那种流畅度基本等于本地体验,这也是“Univer 在线”这个关键词能火起来的原因,因为大家搜到之后第一反应就是先去在线试玩一下。

1.2 横向对比:和 OnlyOffice、x-spreadsheet 的差别

选型最怕的是“只知道一个方案”就下手。我这次在决定用 Univer 之前,和市面上主流方案做了个对比,大致情况可以看这张表:

方案定位部署方式协同能力适合场景
OnlyOffice完整 Office 套件服务端自部署自带协同企业内部全套办公
x-spreadsheet轻量表格组件前端引入无原生协同简单展示和编辑
Univer嵌入式数据协同引擎前端引入,可选协同服务端基于 CRDT业务系统内嵌在线表格

OnlyOffice 我在项目里用过,它最大的优势是文档格式保真度非常高,打开 docx、xlsx 基本能还原 Office 的排版。但它的缺点也很明显,重,部署起来要管数据库、服务端、缓存一大堆东西,如果你只是想让某个后台页面里有一个能编辑的表格,上 OnlyOffice 属实有点杀鸡用牛刀。x-spreadsheet 很轻,但功能也比较基础,公式、样式、协同这些能力都很难撑住一个正经业务。

Univer 给我的感觉是正好卡在中间,它可以像组件库一样打进前端项目,不强制你上服务端,但真要多人协同也有对应的方案。目前它的文档模块和幻灯片模块还在发育中,最成熟的是表格(Sheet),这也是我建议大家从表格场景切入的原因。

1.3 适合接入的业务场景和不适用的场景

从我接触到的需求来看,Univer 最适合的场景有这么几类。第一类是后台管理系统里的数据编排,比如运营配置活动表、排期表、价目表,用户需要在网页上编辑结构复杂的数据,只给一个 textarea 肯定不行,普通表格组件又没有 Excel 的公式和汇总能力,Univer 就很合适。第二类是企业级 SaaS 产品要做一个“数据工作台”,希望客户能在平台上维护自己的业务数据并支持多人协作,这种时候用 Univer 能省掉从零开发表格引擎的巨大成本。第三类是数据分析平台,用户在导出报表之前需要先在预览表格里做二次调整,比如改两列、加个公式、筛一下数据,Univer 的体验和 Excel 接近,学习成本很低。

不过也有不适合的场景。如果你的产品核心需求是“打开 Office 文件并且像素级还原本地排版”,那目前最稳的选择还是 OnlyOffice 这类重方案,毕竟 Univer 现在的强项是数据协同和编辑体验,对复杂 Office 格式的兼容还在持续完善。所以我一直强调,选型不是看谁技术好,而是看谁跟你的业务需求匹配。

2. 技术架构与核心设计思路拆解

2.1 Canvas 渲染引擎背后的理由

在线表格最大的性能瓶颈是 DOM 数量。一张 10 万行乘 10 列的表格,如果按单元格渲染 DOM,浏览器会直接卡到没法用。Univer 的做法是用 Canvas 一次性绘制当前可视区域内的内容,滚动时再重绘,等于只处理用户屏幕上看得到的几百个单元格,性能自然能撑住。你可以把这种方式理解为地图 App,它也是只加载当前屏幕这一块的地图,你滑动的时候再补下一块。

实际用下来,这种设计带来的体验提升非常明显。以前用 DOM 表格组件时,滚动一下要等半天,现在是丝滑的。但也有一点要注意,因为是 Canvas 渲染,你不能像操作 DOM 那样随意给单元格塞 HTML,Univer 的自定义渲染需要走它自己的体系。对大多数业务场景来说,官方提供的单元格内富文本、超链接、图片这些能力已经够用,只有真遇到特殊需求时才需要深入它的渲染机制。

2.2 命令系统、依赖注入与插件化设计

Univer 的架构核心是一个 headless 引擎,UI、渲染、公式、协同全部是独立插件。这带来的直接好处是,你完全可以把 Univer 的核心能力跑在 Node 环境做计算,前端只展示数据,也可以裁剪掉默认 UI,在公司自己的设计体系上套壳。

所有用户操作,改值、改样式、插入行列、删除数据,在 Univer 里都会走 Command 系统。命令模式的好处有三个:第一,撤销和重做可以做到比较细的粒度;第二,协同的时候可以同步命令而不是同步最终快照;第三,开发者可以注册自己的业务命令,把现有动作加入撤销链。我在接入时用这种方式把“导入外部数据”做成了一个可撤销操作,用户误点了还能撤回来,体验比直接改数据 API 好很多。

Univer 内部还用了依赖注入来管理服务,比如编辑器、右键菜单、主题、UI 组件都是可以替换的。对业务系统来说,这种“可重装”的设计让 Univer 能适配不同的外壳,你不用改动业务系统去迎合表格,而是可以裁剪表格来适配你自己的壳。

2.3 在线协同的实现逻辑

Univer 的协同能力基于 CRDT,具体实现用了 Yjs。为什么不是传统的 OT?OT 需要中心服务端做操作变换,工程实现复杂;CRDT 更像“大家各改各的,最后靠算法收敛”,天然适合去中心化的增量同步。现在很多现代编辑器、在线白板都开始用 Yjs,这是更符合当下实时协同需求的技术路线。

我的理解是,Univer 会把工作簿映射到 Yjs 的共享数据结构上,用户的每个编辑动作通过协同插件转化为 Yjs 更新,再通过 WebSocket 广播给其他端,其他端收到更新后重放,数据就收敛一致了。你在接入的时候不需要自己写冲突处理,真正要处理的是权限控制,谁能看、谁能改,这部分 Univer 不提供完整方案,得配合自己的后端做。和我早期接触的一套协同方案比,Univer 省掉的冲突处理代码不是一点半点,上手难度低很多。

3. 实操:手把手集成 Univer 并跑通协同

3.1 先跑官方 Demo,确认你要的是哪个版本

在动手写代码之前,我建议你先把官方仓库的 Demo 项目跑一遍。Univer 目前迭代速度很快,不同版本的包名和初始化 API 差异比较大。如果你直接上网搜教程,很可能会看到 0.1.x、0.2.x、1.x 各种写法混在一起。跑官方 Demo 有几个好处:确认你选的版本能正常工作,确认你需要的功能在这个版本里有没有,顺便把依赖版本锁死,避免后面被升版坑到。

官方 Demo 是一个 Pnpm/TypeScript 项目,你把仓库 clone 下来后执行安装和启动即可。跑起来之后点一点工具栏、试一试公式、开两个浏览器窗口测协同,基本能直观判断这个产品是否符合预期。我自己的习惯是先跑通官方 Demo,再引入自己项目,这样可以排除很多环境问题。

3.2 环境准备与依赖安装

我这次用的是 Vite + React + TypeScript 的组合,如果你用 Vue 或原生 JS 也可以用,Univer 本身框架无关。先创建项目:

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

然后安装 Univer 相关包,我这边安装的核心依赖大概是下面这些,不同版本会有细微差别,以你锁定的版本为准:

npm install @univerjs/core @univerjs/sheets @univerjs/sheets-ui @univerjs/ui @univerjs/engine-render @univerjs/engine-formula @univerjs/engine-numfmt

如果你需要中文语言包,再装:

npm install @univerjs/locale

这里有个经验:Univer 的包名经常带-ui、-engine、-core这样的后缀,如果只装核心包,你会发现界面不渲染,因为 UI 是独立插件。很多时候报错都是因为缺了插件导致,看到“请注册 XX Plugin”之类的报错,先去安装对应依赖注册,再回来查业务逻辑。

3.3 初始化一个带基本表格能力的实例

在App.tsx里放一个容器,然后初始化 Univer。下面这段代码大致是 1.x 版本的写法:

import { useEffect, useRef } from "react"; import { Univer } from "@univerjs/core"; import { UniverRenderEnginePlugin } from "@univerjs/engine-render"; import { UniverUiPlugin } from "@univerjs/ui"; import { UniverSheetsPlugin } from "@univerjs/sheets"; import { UniverSheetsUIPlugin } from "@univerjs/sheets-ui"; import { UniverFormulaEnginePlugin } from "@univerjs/engine-formula"; import { UniverSheetsFormulaPlugin } from "@univerjs/sheets-formula"; import { LocaleType, zhCN, enUS } from "@univerjs/locale"; import "@univerjs/ui/lib/index.css"; import "@univerjs/sheets-ui/lib/index.css"; export default function App() { const containerRef = useRef<HTMLDivElement>(null); const univerRef = useRef<Univer | null>(null); useEffect(() => { if (!containerRef.current) return; const univer = new Univer({ locale: LocaleType.ZH_CN, locales: { [LocaleType.ZH_CN]: zhCN, [LocaleType.EN_US]: enUS, }, }); univer.registerPlugin(UniverRenderEnginePlugin); univer.registerPlugin(UniverUiPlugin, { container: containerRef.current, toolbar: true, footer: true, }); univer.registerPlugin(UniverSheetsPlugin); univer.registerPlugin(UniverSheetsUIPlugin); univer.registerPlugin(UniverFormulaEnginePlugin); univer.registerPlugin(UniverSheetsFormulaPlugin); univer.createSheet({ name: "演示表", data: { rowCount: 100, columnCount: 20, cells: { 0: { 0: { v: "你好", t: 1 }, 1: { v: "Univer", t: 1 }, }, }, }, }); univerRef.current = univer; return () => { univer.dispose(); }; }, []); return ( <div ref={containerRef} style={{ width: "100%", height: "100vh" }} ></div> ); }

如果你跑起来发现界面空白或者工具条不显示,先检查两件事:CSS 样式有没有引入,容器有没有明确的高度。Univer 的渲染引擎需要拿到容器尺寸,否则 Canvas 可能没有正确绘制。另外,不同版本中创建文档的 API 可能叫createSheet,也可能叫createUniverSheet,遇到这种差异直接看当前版本官方 Demo 里的写法即可。

3.4 用 Facade API 读写单元格

Univer 提供了一套 Facade API,目的就是让你用更简单的方式操作表格,省得直接碰底层命令。我接入业务系统的时候,90% 的数据读写场景都用 Facade 解决了。

import { FUniver } from "@univerjs/core"; const api = FUniver.newAPI(univer); const workbook = api.getActiveWorkbook(); const sheet = workbook?.getActiveSheet(); sheet?.getRange(0, 0, 1, 1).setValue("我通过 API 写入"); const value = sheet?.getRange(0, 0, 1, 1).getValue(); console.log(value);

批量写入的场景更常见,比如从后端拉了一组数据要展示到表格里。一次写入一个二维数组会比循环写入快很多:

const data = [ ["编号", "名称", "金额"], ["001", "办公桌", 1200], ["002", "会议椅", 860], ]; sheet?.getRange(0, 0, data.length, data[0].length).setValues(data);

这里要注意getRange的参数是起始行、起始列、结束行、结束列,写错下标会把数据写到意外的位置。接入初期我犯过几次这种低级错误,排查起来还挺费劲。建议每次写入后打印一下目标区域的值,确认数据落在预期位置。

3.5 挂上协同后端,实现多人编辑

协同是 Univer 的重要卖点,我先在本地起一个 WebSocket 服务端,用来转发 Yjs 更新。服务端代码可以基于y-websocket官方包:

// server.cjs const WebSocket = require("ws"); const { setupWSConnection } = require("y-websocket/bin/utils"); const wss = new WebSocket.Server({ port: 1234 }); wss.on("connection", (conn, req) => { setupWSConnection(conn, req, { docName: req.url }); }); console.log("协同服务已启动:ws://localhost:1234");

然后在客户端把 Yjs 实例和 WebSocket Provider 装配进去:

import * as Y from "yjs"; import { WebsocketProvider } from "y-websocket"; const doc = new Y.Doc(); const provider = new WebsocketProvider("ws://localhost:1234", "room-demo", doc); univer.registerPlugin(UniverCollaborationPlugin, { doc, provider, });

doc是 Yjs 的共享文档实例,provider负责把 Yjs 变更通过 WebSocket 发布出去。两个浏览器打开同一个room-demo,数据就会双向同步。要提醒的是,Univer 的协同插件在不同版本里的名称和参数可能会变化,这段代码描述的是装配思路,真正落地时要以你锁定的版本源码和官方 Demo 为准。

协同跑起来之后,我建议你重点测两个场景:一是两个用户同时改同一行不同列,数据是否都能保留;二是两人同时改同一个单元格,文档会不会崩。Univer 的 CRDT 底层会保证数据一致,但界面上的光标和选中态表现会因为版本不同而略有差异,这部分要结合你自己产品体验调整。

3.6 导入导出与常用能力

如果你用了默认的工具栏,文件按钮里一般会直接集成上传导入 XLSX 和导出 XLSX 的功能。但如果你自定义了 UI 或工具栏,就需要通过命令手动触发。导入导出插件的包名在不同版本里也有变化,你可以在 npm 上搜@univerjs开头的包,找到和你版本匹配的导入导出插件。

我自己比较常用的一个做法是,导出时直接用 SheetJS(xlsx库)自己处理数据。因为 Univer 的 Facade API 可以拿到表格数据,我把读取到的二维数组交给 SheetJS 生成 XLSX 文件,这样做的好处是不受 Univer 版本迭代影响,代码稳定。坏处是如果你需要保留单元格样式,还是得走官方导入导出插件。

公式能力也很重要。注册了UniverFormulaEnginePlugin和UniverSheetsFormulaPlugin之后,表格里就能用=SUM(A1:A10)这类公式了。实测下来,Univer 的公式引擎支持函数数量已经很多,而且输入公式时的自动补全、引用提示都比较接近桌面版 Excel,这个体验对业务用户来说很关键。

4. 常见问题与排坑实录

4.1 动态创建和销毁文档时内存泄漏

这是我踩过最狠的坑。在路由切换场景里,我一开始只是让容器 div 消失,没有调用univer.dispose(),切几次页面之后明显感觉整个系统变卡。后来排查才发现,Canvas 的 Worker、事件监听、定时器都还活着,组件虽然被卸载了,但是 Univer 实例还在后台跑。

正确的做法是在组件卸载时调用univer.dispose()。如果你在一个页面里管理多个文档实例,要维护好实例引用,不要随手 new 一个就丢。另外,如果列表页有很多条数据,每点开一条就创建一个 WorkBook,建议不要一次性全部创建,而是等用户真正打开某条文档时再初始化 Univer,否则内存占用会很可观。

4.2 协同同步后公式结果不一致

协同场景下,我有一个用户设置了=B2*2,另一个用户同时改了 B2 的值,结果前一个用户的公式结果没有自动刷新。这个问题的核心是公式重算没有被协同端触发。

排查思路分几步。先确认UniverFormulaEnginePlugin和UniverSheetsFormulaPlugin是否已经注册,顺序尽量靠前。再打开开发者工具,在网络面板里看 WebSocket 的帧,确认协同消息确实到达了对端。如果消息到了但公式不重算,基本可以锁定是公式引擎监听问题。这里我只能说,多人在线公式重算的实际体验还依赖具体版本的实现,建议上线前多做几个并发场景的测试,别想当然觉得注册了插件就万事大吉。

4.3 样式和选区错位

有段时间我发现表格里鼠标点选的位置和单元格实际高亮的位置差了一截,键盘移动光标时选区也会错位。排查下来发现是容器初始尺寸的问题。如果容器在弹窗里,弹窗有过渡动画,Univer 在动画结束前就完成了尺寸测量,Canvas 的坐标系就和点击坐标对不上了。

解决思路是监听容器尺寸变化,在尺寸变化后调用渲染引擎的resize()方法。如果在 React 里用了display: none再切到display: block,建议延迟创建 Univer,或者在切换完成后手动触发一次 resize。这个坑特别隐蔽,因为界面不会报错,就是选区位置不对,遇到类似情况先怀疑容器尺寸一定没错。

4.4 版本更新后 API 变化太大怎么办

Univer 的版本迭代速度是真的快,从 0.1.x 到 1.x 我经历过接口大改的情形。网上有些中文教程写得很早,照抄大概率跑不通。我的经验很简单:

  1. 项目里锁定精确版本,不要用latest这样的范围依赖。
  2. 以 GitHub 仓库当前 tag 对应的 Demo 为准。
  3. 不确定 API 时,直接看 node_modules 里对应包的 d.ts 类型定义。
  4. 生产环境要用的话,最好 fork 一套稳定版本,自己做定制和降级。

这套思路不只在 Univer 上有用,遇到其他迭代快的开源项目也适用。真正靠谱的文档不是某篇博客,而是当前版本的源码和类型声明。

5. 实际应用与影响分析

5.1 对业务系统的价值

如果业务系统需要表格能力,Univer 能帮你省下至少几个月的开发量。公式、样式、编辑、选区、撤销、协同,这些原本需要团队攻坚的功能,它基本都提供了。你真正要投入精力做的是业务相关的部分:权限模型、数据模板、操作审计、后端存储。我接入后的感受是,团队可以把注意力从“怎么写一个 Excel”转移到“怎么让我们的业务数据更好用”上。

从产品角度说,一个平台里能直接编辑和协同的表格,对客户是非常直观的“专业感”加分项。运营同学不再需要把表格下载到本地,改完再传回去,直接在网页里操作,数据实时落库,这个体验升级是很明显的。

5.2 对开发者社区的生态意义

Univer 延续了 Luckysheet 的开源精神,而且它的插件化架构让第三方扩展成为可能。以后社区可以围绕它做自定义公式、图表渲染、私有协同协议这些扩展。虽然生态目前还在早期,但方向是正确的。

对开发者个人来说,研读 Univer 源码本身也是一次很划算的学习机会。依赖注入、命令模式、CRDT、Canvas 渲染,这些平时零散的知识点在一个项目里同时出现,扒一遍源码比看十篇架构文章都管用。这也是我建议有一定技术基础的朋友去研究它的原因,哪怕暂时不接入生产,光是理解它的模块划分方式就有收获。

最后说点我个人的体会。Univer 目前最稳的落地方式,是把它当成一个功能强大的表格组件嵌进自己的业务系统,而不是指望它能开箱取代整个 Office Web 套件。文档和幻灯片模块还在成长中,如果你只看表格场景,它现在已经是完全可以投入生产的选择。我们团队在接入时没有贪多,先把 Sheet 加协同跑通,再逐步叠加公式和导入导出,这个节奏比较稳。如果你正准备选型,建议先照着官方仓库的 Demo 跑一遍,再按这篇文章的路径做一次路由切换和内存回收,感受一下它的维护成本,再决定要不要深入。

返回列表