☰
开源在线表格Univer实战:从接入到私有化部署
2026/11/4 10:36:48 网站建设 项目流程

最近好几个人跑来问我同一个问题:有没有一个能私有化部署、又能直接嵌进自己系统里的在线表格?我每次的答复都是同一个:去看看Univer。这个项目在GitHub上已经积累了三万多的star,但很多人搜“univer在线”,并不是想研究它有多底层,而是想问一句:这东西到底能不能直接用?能不能把它变成我自己系统里的在线Excel?

这篇文章就是回答这个问题的。我会从实际接入的角度,把Univer是什么、它的核心架构、最小可用工程怎么做、集成时最容易翻车的地方,以及在线协作和Excel导入导出这些“办公套件标配能力”一条条拆开讲清楚。内容适合前端工程师、低代码平台的同学,也适合产品和技术负责人做技术选型参考。

我先把结论放在前面:Univer不是又一个前端表格组件库,它是一个完整的、可扩展的在线办公套件框架。你拿它做在线表格只是最基础的用法。

1. Univer到底做了什么:不是又一个前端表格组件

1.1 一套框架同时覆盖表格、文档和幻灯片

很多第一次接触Univer的人,会下意识把它和Handsontable、Luckysheet这类“前端表格库”放在一起比较。这个类比有机会但不够准确。Univer的官方定位是办公套件框架,它同时覆盖了三类文档形态:在线电子表格、在线文档、在线幻灯片。在一个Univer实例里,你可以创建一个Sheet工作簿,也可以创建一个Doc文档,未来甚至可以在同一个页面里让文档和表格互相嵌套。

从实际项目角度来说,这个设计带来的直接好处是:你的业务系统如果既要一个在线报价单表格,又要一个合同文档编辑器,Univer可以用同一套架构、同一套数据流、同一套渲染引擎去撑起这两个模块。维护一套底层,而不是给表格选一个组件、给文档再选另一个编辑器,后续迭代的成本差异是非常明显的。

当然,要客观一点:目前Univer最成熟、最稳的是表格模块(Sheet),文档和幻灯片模块还在快速迭代中。我测试下来,做常规在线表格是完全没问题的,做复杂文档排版和协作还处于“能跑但别太贪心”的阶段。所以我的建议是:用Univer的表格模块落地业务,同时保留它未来扩展文档能力的可能性。

1.2 它和Luckysheet、Handsontable的定位差异

为了说清楚Univer的特点,我拉了一个对比表,这几个都是大家在选型时经常遇到的方案:

项目文档形态渲染方式协同编辑二次开发友好度典型定位
Univer表格/文档/幻灯片Canvas渲染官方提供协作协议与示例服务端高,插件体系完善在线办公套件基础设施
Luckysheet表格Canvas渲染需自己接后端中,社区文档较旧在线表格组件
Handsontable表格DOM渲染企业版支持中,商业授权需注意数据录入表格组件
x-spreadsheet表格Canvas渲染弱中轻量表格组件

Univer的团队之前在做Luckysheet的时候积累了大量在线表格经验,Univer相当于把前人踩过的坑和积累的能力重新做了一遍,并且从架构上更彻底地走向了“办公套件框架”这条路。

对于选型来说,差异不只是在功能列表上多几个公式函数,而是:Handsontable这类组件解决的是“页面里嵌入一个可编辑表格”的问题,Univer解决的是“让一套在线办公软件长在自己的系统里”的问题。前者是工具,后者是基础设施。

1.3 “univer在线”这个搜索词背后的真实需求

我看了一下,“univer在线”被搜索得很多。大部分搜这个词的人,是真的想找一个“在线版的Excel”或者“在线协同表格”来用。但这里有个认知差需要拉平:Univer不是一个直接给你部署好、打开就能用的网站,它是一个需要二次开发的框架。

它的典型使用方式是:你自己的系统里有一个“数据填报”“在线审批”“报表查看”页面,然后你把Univer嵌进去,数据从你的接口来,保存动作由你自己的逻辑处理。Univer负责的是提供接近Excel的交互体验——单元格编辑、公式计算、格式调整、行列操作、快捷键,这些复杂的事情不需要你从零写。

换句话说,如果你只是需要一个网页版的Excel给自己用,那直接用成熟的云文档产品更省事。但如果你希望表格长在自己的系统里、数据不出内网、交互完全按业务需求定制,那Univer就是目前开源领域值得押注的方向。

2. 拆开Univer的引擎:Canvas渲染、命令流和插件体系

2.1 Canvas渲染:为什么大数据量下也不卡

第一次打开Univer,你看到的是一个非常像Excel的网格界面。但如果你从浏览器的开发者工具里检查节点,会发现页面里并没有成千上万的单元格DOM元素,这就是Canvas渲染的效果。

传统组件库渲染表格,单元格多到一定程度时DOM节点会爆炸,轻则滚动卡顿,重则直接白屏。Univer的思路是:用Canvas绘制可见区域内的内容,滚动时只更新视口内的画面,而不是把所有单元格都画出来。这个思路和地图引擎的瓦片加载很像——你看到的永远只是当前视野对应的那一块,但整体逻辑上仍然是一张无限大的工作表。

这么做换来的好处很明显:一个几万行、几十列的数据表,打开和滚动都还比较跟手。我实际测试过一个模拟的十万行数据表,Univer在动态调整行高、批量更新单元格值时没有出现明显掉帧。当然前提是你别同时打开几十个表格页签,也别在单元格里塞太多大图。

但Canvas渲染也带来一个代价:你没法像操作DOM那样直接给某个单元格“加一个元素”。因为你看到的网格画面本质上是一张图,任何UI上的自定义修改都要通过Univer的插件或UI插槽去做,而不是顺手插个div。这个学习成本是真实存在的,后面讲扩展时会细说。

2.2 Command驱动的数据流:一切修改都有迹可循

Univer内部的数据流设计得比较干净,所有用户操作,比如输入值、合并单元格、调整行高、撤销重做,都会被包装成一条Command。Command经过业务逻辑校验之后,触发数据层更新,数据更新最终驱动Canvas重新绘制。

这个设计最大的价值在于“可回溯”。你操作了十步,每一步都对应一条命令,撤销就是把这十条命令逆序执行回去。业务系统最常见的审计需求也是这样满足的——谁在什么时间改了哪个单元格,理论上都能从命令流里还原出来。

对于开发者来说,这意味着你不要绕过Command层去直接修改内部状态。实际集成中很容易出现一个错误做法:用户点击你的按钮,你直接通过内部API把某个单元格的值改了,界面上确实变了,但撤销键按不下去,因为命令流被跳过了。正确做法是调用Univer暴露的Command接口,让修改走它自己的数据流链路。

2.3 插件化:一个功能一个模块,按需裁剪

Univer的插件体系是它的灵魂。公式、复制粘贴、条件格式、筛选、排序、快捷键、工具栏,这些不是一个巨大的整体,而是一个个插件模块。你初始化Univer的时候,用到哪些功能就注册哪些插件,不用的不加载,这样既控制了包体积,也降低了相互干扰的可能。

我刚开始接入时习惯性把官方示例里的插件列表直接复制过来,结果发现很多能力都用不上。后来我把公式、剪贴板、条件格式、撤销重做这几个基础插件保留,再把其他不必要的模块拿掉,首屏加载的JS体积和初始化时间都有了肉眼可见的改善。

这里要给一个经验性的建议:先跑通完整demo,确认基础功能都正常,再按业务需要做减法。不要一上来就追求极致精简,否则很容易因为剪掉某个依赖而出现隐藏问题,而且这类问题排查起来非常隐蔽。

2.4 协同编辑为什么绕不开Command

如果你想做多人同时编辑同一个表格,Univer的Command机制会直接决定架构方式。因为多人协同的本质不是“多个浏览器连接同一个后端”,而是“每个操作都能被描述、传输、合入”。

比如A用户在第1行输入了“100”,B用户同时在第1列输入了一个公式。这两个操作没有冲突,只需要分别同步给对方即可。但如果A和B同时修改了同一个单元格,就需要有一套转换机制判断以谁的版本为准,或者如何把两个操作合并成都生效的结果。

在Univer的模型里,前端产生的每个编辑动作天然就是一条命令,协同服务端要做的事情就是接收、合并、广播这些命令。官方也提供了协同相关的协议设计和示例服务端,给自建协同留了比较完整的技术路径。不过要提醒大家:在日常业务集成里,协同比你想象中复杂得多,至少包括操作转换、版本快照、连接状态管理、断线重连、冲突提示这几个环节。如果你只需要“一个用户改完保存,另一个用户刷新后看到新数据”,那不需要上真正的实时协同,做成编辑锁或者版本覆盖就够了。

3. 从零跑通一个Univer在线表格的完整过程

3.1 准备环境与依赖

Univer整体是TypeScript写的,和框架无关,Vue、React、原生JS都能接。我自己的测试工程用的是Vite + TypeScript,Node版本建议在18以上。

先创建一个干净的工程:

npm create vite@latest univer-demo -- --template vanilla-ts cd univer-demo npm i @univerjs/preset-sheets npm run dev

Univer的版本迭代速度比一般开源项目快不少,尤其是API命名在各个小版本之间可能会有调整。所以这里说两个层面的建议:第一,把依赖版本锁死,别用^范围装最新版,否则第二天依赖就可能break;第二,以官方文档当前推荐写法为准,网上很多教程写的是0.x时代的API,对照最新版会跑不通。

3.2 最小初始化代码

下面这个例子足够让你在页面里渲染出一个可用的在线表格。不同Univer版本的API有差异,以下是我测试时能跑通的方式,读者以自己安装版本的官方demo为准:

import { Univer, UniverInstanceType } from '@univerjs/core'; import { defaultTheme } from '@univerjs/design'; const container = document.getElementById('app'); const univer = new Univer({ theme: defaultTheme, }); // 注册渲染引擎与UI插件 univer.registerPlugin(UniverRenderEnginePlugin); univer.registerPlugin(UniverUIPlugin, { container }); // 注册表格模块与扩展功能 univer.registerPlugin(SheetsPlugin); univer.registerPlugin(SheetsUIPlugin); univer.registerPlugin(FormulaPlugin); univer.registerPlugin(ClipboardPlugin); univer.registerPlugin(UndoRedoPlugin); // 创建一份工作簿 univer.createUnit(UniverInstanceType.UNIVER_SHEET, { name: 'Demo', sheets: [ { name: 'Sheet1', rowCount: 1000, colCount: 50, cellData: { 0: { 0: { v: '商品名称' }, 1: { v: '单价' } }, 1: { 0: { v: '苹果' }, 1: { v: 5.5 } } } } ] });

这段代码做的事可以拆成四步:

  1. 创建Univer核心实例,配置全局主题;
  2. 注册渲染引擎和UI插件,Univer才知道界面画在哪里、怎么画;
  3. 注册表格模块及公式、剪贴板、撤销重做等插件,表格交互才完整;
  4. 通过createUnit创建一份工作簿,参数里定义了工作表名称、行列数量以及初始单元格数据。

注意几个容易踩的点:

  • 容器必须有明确高度。如果你给#app只设置了display:block但没有高度,Univer会渲染成一个高度为0的区域,看起来就是空白。我们上面的body要加基础样式:
html, body, #app { width: 100%; height: 100%; margin: 0; padding: 0; }
  • cellData的索引格式是“行号对应列号”。这里行号0代表第一行,列号0代表第一列。如果你用数组习惯先去数第几行,再数第几列,很容易把行列写反。实际项目里从后端接口拿数据转换成cellData时,一定要在服务端就处理好这个结构,不要到前端再循环转换,数据量大时转换逻辑会拖慢首屏。

3.3 塞数据和监听编辑

工作簿创建出来之后,业务系统通常需要两件事:把已有数据加载进来;用户在表格里改了内容后,你把变更拿走去更新自己的业务系统。

要动态设置单元格值,你可以获取当前活动工作表对象,然后更新单元格数据:

const workbook = univer.getActiveUnit(); const sheet = workbook?.getActiveSheet(); sheet?.setValue(2, 3, '这是新内容'); // 第3行第4列

监听用户操作,推荐走数据变更事件。下面代码示意了整体思路:

const activeSheet = univer.getActiveUnit()?.getActiveSheet(); activeSheet?.onCellChange((changes) => { // changes里包含了被修改单元格的行列号和最新值 // 这里可以推给后端、触发联动计算、或者打日志 console.log(changes); });

这里要重点提示一下:onCellChange这类事件监听器的具体名称和回调结构在不同版本里可能有调整,不要死记API名字,要记思路——把Univer当成一个“黑盒编辑器”,你只通过事件拿结果。如果事件解构不出来,就去查你当前安装版本的.d.ts类型定义。

我的习惯是:不在这个监听回调里做太重的事情。比如你监听到了一个值变化,不要立刻去请求后端保存一整张表的数据,因为用户连续输入十几个单元格,你可能会触发十几次保存请求。正确做法是做一个防抖函数,等用户停下后统一保存变更。

3.4 在React或Vue工程里的挂载细节

如果你用的是React或Vue,要注意的是生命周期管理,而不是渲染语法。Univer创建之后会自己管理内部的渲染循环,和React的虚拟DOM没有关系。在React里把Univer初始化放在useEffect里,并且在组件卸载时调用univer.dispose()释放实例。

有一个非常容易犯的错误:在React严格模式(StrictMode)下,useEffect会被执行两次,如果第二次执行时你没有清除第一个Univer实例,页面里会有两个编辑器同时响应、互相干扰。解决方式是初始化前先判断实例是否已存在,或者使用一个useRef保存实例并在清理函数里销毁。

const univerRef = useRef<Univer | null>(null); useEffect(() => { if (univerRef.current) return; const univer = createUniver(containerRef.current); univerRef.current = univer; return () => { univer.dispose(); univerRef.current = null; }; }, []);

Vue里的处理思路完全一样,在onMounted里创建、在onUnmounted里销毁。核心就一条:Univer的生命周期必须由你自己掌控,别依赖框架的自动清理解析。

4. 接入Univer之后,我替你们踩过的几个坑

4.1 隐藏容器和动态尺寸引起的渲染问题

这个问题在我接入过程中出现得最频繁。业务页面常常有一个“点击按钮展开表格”的交互,比如抽屉、弹窗、折叠面板。如果Univer在容器隐藏状态下初始化,它获取到的容器尺寸是0,Canvas画出来就是空白;等你展开容器,表格不会自动重绘,依然空白。

我的处理方案是:在容器可见后再初始化Univer实例。如果容器需要从隐藏切换到显示且无法避免提前初始化,那就在切换完成后手动触发一次Univer的重绘或者尺寸更新。

另一种关联问题是动态调宽。Univer所在容器从500px拉宽到1000px,默认情况下表格区域不会自动填满新的宽度。我在实际项目中见过不少人被这个问题卡住,最后是通过监听窗口和容器尺寸变化,然后调用Univer的布局更新方法解决的。这里的重点不是找最神奇的API,而是理解Canvas渲染器天然不会像DOM流式布局那样自动适应,所有尺寸变化都要主动通知它。

4.2 中文输入法的编辑光标问题

做中文系统肯定绕不开输入法。我测试较早版本的Univer时,碰到过一个具体问题:在单元格里输入中文,拼音候选框弹出时,编辑状态的光标位置和候选框的定位会发生错位,甚至输入法选词后,单元格里出现的内容和预期不一致。

这类问题通常和Canvas渲染的文本选区管理有关。编辑器需要知道有没有正在进行的输入法组合事件,并在组合期间调整光标绘制逻辑。现在新版Univer对中文输入法的处理已经靠谱很多,但如果你用的版本比较旧,并且在中文输入场景下发现光标跳动、候选词上屏错乱,优先做两件事:升级到最新版本,以及缩小问题现场写一个最小复现demo去开源仓库提issue。

我在集成时也有一个备选思路:对纯数据录入类场景,可以不用Univer的原生单元格编辑,而是双击时在Canvas上面覆盖一个原生<input>,等用户确认后再把值写回单元格。这个方案兼容性最好,代价是你需要自己处理编辑框的位置联动,交互细节要花时间打磨。

4.3 撤销重做失效:因为你绕过了Command层

这个坑我前面提过一次,但值得单独展开。业务开发里最常见的需求是“点一个按钮批量填充一些单元格”,比如根据规则把一整列不符合条件的值标红。如果你图省事,直接遍历列然后调用底层接口改样式和值,界面看起来一切正常,但用户紧接着按Ctrl+Z时会发现毫无反应。

问题根因是你的修改没有形成一个可撤销的命令记录。Univer的撤销重做是基于命令栈实现的,你绕过命令栈直接改了内部状态,操作自然不在栈里。

正确做法是找到Univer提供的对应命令接口,或者把业务里的批量操作也封装成自定义命令。你可能觉得麻烦,但反馈到用户体验上,这就是“编辑器的撤销好不好用”的差别。我后来的原则是:凡是用户可感知的操作,一律走命令层;只有后台静默初始化、程序自动校正这类非用户操作,才考虑直接改数据。

4.4 包体积控制和二次打包

Univer功能完整,带来的直接问题就是包不小。如果用一个演示工程直接全量引入,构建产物能达到数MB级别,这在企业内网应用里可能不太明显,但在面向C端用户的系统里会直接影响首屏加载。

优化手段第一步是插件按需注册,这一步能砍掉很大一块体积。第二步是合理配置代码分包,把Univer相关代码提取成一个独立的chunk,让它在浏览器里可以被缓存。第三步是确认你最终使用的Univer包没有把整套设计资源和语言包一起打进主包。

坦白说,除非你的表格场景特别简单,否则Univer的包体积不太可能优化到和单功能上传组件一样小。选型时要接受这个现实:你买的是完整办公套件的能力,付出的是体积成本。

4.5 不是所有Excel能力都开箱即用

最后提醒一个更容易误判的问题:Univer提供了Excel的交互体验,但不代表Excel全功能都100%覆盖。数据透视表、复杂图表、宏、多级分类汇总这些场景,要么依赖独立插件,要么还在演进中。

我自己的做法是:在选型阶段就把业务里必须用到的Excel功能列成清单,逐项在Univer上测试。如果你发现某个强需求没有实现,先别急着框架层“打补丁”,因为这种补丁通常成本很高。可以考虑调整产品交互——比如不支持的功能引导用户下载Excel模板填写后回传导入,比硬补一个不稳定的实现要稳妥得多。

5. 把Univer用出“在线办公套件”的感觉:协作、导入导出与扩展

5.1 Excel文件导入导出

“univer在线”的很多搜索者,第一诉求其实是与本地Excel文件互通。Univer在这方面的思路是提供ExcelIO相关的导入导出能力,而不仅仅是页面里画一个表格。

在集成时,我能稳定跑通的方式是:把上传的xlsx文件交给Univer的导入处理逻辑,解析后渲染成工作簿;编辑完成后,把工作簿数据重新序列化成xlsx文件供用户下载。

这里有三个经验点:

第一,导入前的校验很重要。Univer对格式规范的文件表现很好,但用户在业务系统里上传的文件往往千奇百怪,比如嵌套了宏、外链图片、特殊编码。导入前要在服务端先做简单的文件类型和大小校验。

第二,导出Excel后格式是否和本地Excel一致,这需要在项目里预留足够的回归测试时间。我的做法是维护一份“格式回归用例表”,包含合并单元格、跨页打印、条件格式、冻结窗格等常见场景,每次升级Univer版本就跑一遍。

第三,导入导出的服务端处理比前端复杂。如果你们团队最终决定把xlsx解析放在服务端,注意依赖安全和性能。如果完全在前端处理,也要注意大文件的浏览器内存压力。

5.2 协同服务怎么设计

官方对协同提供了协议层面的方案,但我必须坦诚地说:自建协同服务是一个不小的工程。你至少需要一个WebSocket服务端、一个命令广播通道、一个操作转换逻辑,以及一套数据持久化策略。

从务实角度出发,我建议业务系统按阶段推进:

  • 第一阶段:单用户编辑,用编辑锁或版本号控制并发;
  • 第二阶段:多人实时编辑,用Univer提供的协同协议和示例服务端;
  • 第三阶段:涉及大规模冲突处理、离线编辑、权限细化时,再考虑深度定制协同算法。

有一个容易被忽略的点:协同不只是“表格内容同步”,还包括用户头像、选区状态、当前查看滚动位置的同步。这些体验功能如果都要做,工作量会再上一个台阶。所以如果你的业务只需要协作,先确认用户真的需要在同一秒看到对方的光标,还是一个保存后刷新的版本控制就够了。

5.3 只读模式、水印和权限控制

你用Univer做ERP数据展示页时,需求往往是“表格只读,但可以滚动、筛选、复制”。Univer里开启只读模式的方式相对简单,关键是在初始化或创建Unit时把可编辑性关掉,或者通过配置项禁止单元格编辑。

不过要记住一个安全边界:前端的只读只是交互层面的限制,不等于数据安全。用户完全可以打开浏览器控制台绕过前端校验,直接构造请求。任何真正敏感的单元格,权限都得住服务端做校验。前端只读模式解决的是“普通用户不会误改数据”的问题,解决不了“恶意用户篡改数据”的问题。

水印层面,Univer作为Canvas渲染,你可以通过Canvas绘制层或业务后端图水印去叠加。如果只做一个全局水印,前端叠一个带用户标识的浮动层通常是最快的方案,但要注意遮挡表格操作。如果要求单元格级别的防截图切片水印,那要按Canvas瓦片去处理,复杂度高一些。

5.4 主题定制和UI扩展

Univer默认的主题是深浅色两套设计体系,通过设计变量可以统一调整。实际项目里接企业品牌色、调工具栏样式,都比改传统DOM表格组件要复杂,但思路是清晰的:找到对应的主题配置对象,覆盖变量后重新初始化。

业务扩展最常用的两个点是:工具栏按钮和右键菜单。

  • 工具栏:你可以注入自定义按钮,点击后触发自己的逻辑,比如“保存到服务端”“提交审批”“切换到只读模式”。
  • 右键菜单:可以在单元格右键菜单里增加“查看历史记录”“添加批注”等功能,前提是这些功能本身有对应的业务数据接口。

做这块扩展时,我的建议是:不要试图在Univer内部塞太多业务逻辑,而是把它当成编辑器,对外抛出事件即可。业务逻辑写在你的应用层,这样后续Univer升级时,你的扩展代码不会轻易崩掉。

6. 站在选型者的角度说几句实在话

如果让我给一个挑大梁的技术负责人总结,Univer适合的场景有这几个:你的系统里需要一个“很Excel”的在线表格能力;你需要私有化部署,数据全部落内网;你有前端团队愿意花时间去理解插件体系和升级节奏;你希望表格模块未来能和文档模块、幻灯片模块共用一套底层。

反过来,不适合上Univer的场景也很明确:如果只是需要一个简单的数据录入网格,Handsontable或原生表格就够,没必要背着整个办公套件框架跑;如果你的团队没有前端基建能力,期望开箱即用、文档点击就交付,那Univer的学习曲线可能会让你难受,因为它的演进速度真的很快。

我个人的体会是,做技术选型不要泛泛比较“谁更牛”,要看你的诉求和团队的持续投入能力。Univer这三年在开源社区的发展速度非常快,从在线表格能力、渲染性能到协同方案都在逐步闭环。选择一个正在高速演进的基础设施,意味着你会跟着它一起升级,这既是红利也是成本。

最后分享一个小技巧:接Univer这类持续变化的开源项目,一定把你的接入版本、改动文件、自定义插件和官方升级日志对应起来。我自己的做法是,在项目里维护一份UNIVER_CHANGELOG,每次升级都记录改了哪些模块、动了哪些初始化参数。这个习惯帮我熬过了好几个从0.x往1.x迁移的坎。如果你也打算把Univer嵌进自己的业务系统,强烈建议从第一天就建立这份记录。

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

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

立即咨询