1. 从“univer”这个标题说起:它到底是什么,能解决什么问题
第一次看到“univer”这个词,很多人会以为是“universe”的缩写,或者某个新出的前端框架。实际上,Univer 是一个开源的、面向电子表格与文档场景的通用协同编辑引擎,核心定位是“把 Excel 和 Word 的能力做成可嵌入的 SDK”。它用 Canvas 做渲染层,用插件架构做功能扩展,跑在 Node.js 生态里,可以理解为“前端表格文档领域的一块乐高底板”。
我最早接触它是因为一个需求:公司内部系统要嵌入一个轻量级的在线表格,支持公式、筛选、单元格样式,还要能多人同时编辑。市面上的方案要么太重(直接嵌一个完整办公套件),要么太轻(只能展示静态数据)。Univer 刚好卡在中间——它提供了一套完整的表格内核,但把 UI 和业务逻辑拆成了插件,你可以只拿核心渲染和计算引擎,自己拼装界面。
这个项目适合三类人:一是前端工程师,想在自己的产品里嵌入表格或文档编辑能力;二是全栈开发者,需要一套可私有化部署的协同编辑方案;三是技术选型负责人,在对比 Luckysheet、Handsontable、AG Grid 这类方案时,想了解 Univer 的差异点。它不要求你精通 Canvas 底层,但需要你对前端工程化、插件机制、Node.js 构建流程有基本认知。
提示:Univer 不是“开箱即用的在线 Excel”,它更像一套 SDK。你要自己写入口、配插件、接后端。如果只想找个现成的在线表格工具,它可能不是最优解。
2. 核心架构拆解:Canvas 渲染、插件机制与 Node.js 工具链
2.1 为什么用 Canvas 而不是 DOM 表格
传统表格方案大多基于 DOM,每个单元格是一个<td>或<div>。数据量小的时候没问题,一旦行数上千、列数上百,DOM 节点数量爆炸,滚动和编辑都会卡。Univer 选择 Canvas 作为渲染层,把所有单元格画在一张画布上,只维护一个 Canvas 元素。这样做的代价是:你没法用 CSS 直接控制单元格样式,所有交互(点击、拖拽、输入)都要自己算坐标。
我实测过,在 5000 行 × 50 列的数据量下,DOM 方案滚动时帧率掉到 20fps 以下,而 Univer 的 Canvas 渲染能稳定在 50fps 以上。这个差距在移动端更明显。Canvas 的另一个好处是导出图片方便——直接canvas.toDataURL()就能拿到截图,不需要额外处理样式。
但 Canvas 也有坑。比如文本换行、富文本编辑、无障碍访问,这些在 DOM 里天然支持的能力,在 Canvas 里都要自己实现。Univer 的做法是:编辑态用一层透明的 DOM 输入框覆盖在 Canvas 上,用户输入时实际操作的是 DOM,输入完成后再把值写回 Canvas 渲染。这个“混合渲染”思路在在线表格领域很常见,但实现细节很考验功力。
2.2 插件架构:为什么不做成单体
Univer 的插件架构是我最欣赏的部分。它的核心包@univerjs/core只包含最基础的数据模型、命令系统和生命周期管理。公式计算、条件格式、筛选、排序、协同编辑,全部是独立插件。你可以按需加载,比如只做只读展示,就不需要引入编辑相关的插件。
这种设计的好处很明显:打包体积可控。我做过一个只读表格的页面,只引入了 core、render-engine、sheet 三个包,gzip 后不到 200KB。如果引入完整功能,体积会到 1MB 以上。对于内部系统来说,200KB 和 1MB 的加载体验差别很大。
插件之间的通信通过命令系统完成。每个操作(比如设置单元格值)都是一个命令,插件可以监听命令、拦截命令、修改命令。这跟 Redux 的 action 机制有点像,但更偏向于“可撤销操作”的场景。Univer 内置了撤销重做栈,每个命令执行后都会记录快照,用户按 Ctrl+Z 时反向执行。
注意:插件加载顺序会影响功能。比如公式插件必须在渲染插件之前注册,否则公式计算结果无法正确显示。我踩过一次坑,把公式插件放在后面加载,结果所有公式单元格都显示为原始文本。
2.3 Node.js 在 Univer 生态里的角色
Univer 本身是前端库,但它的开发、构建、服务端协同都离不开 Node.js。官方推荐用 Node.js 18 LTS 或更高版本,我实测 18.20.4 和 20.x 都能跑,但 16.x 会在安装依赖时报错,因为部分包用了较新的 ES 语法。
构建工具链用的是 Vite + TypeScript。如果你要二次开发,需要先pnpm install,然后pnpm dev启动开发服务器。官方仓库的 monorepo 结构比较庞大,第一次 clone 下来安装依赖可能要几分钟。如果网络环境不好,建议配置国内镜像源,否则node-sass或canvas这类原生模块编译容易失败。
服务端协同方面,Univer 提供了@univerjs/pro-server包,可以跑在 Node.js 里做协同编辑的后端。它用 WebSocket 做实时通信,用 OT 算法解决冲突。如果你只是做单机版,不需要这个包;如果要多人同时编辑,就需要部署一个 Node.js 服务。
3. 从零搭建一个 Univer 表格:完整实操流程
3.1 环境准备与依赖安装
先确认 Node.js 版本。打开终端执行node -v,如果低于 18,去官网下载 18.20.4 LTS 或 20.x 版本。Windows 用户直接下安装包,macOS 用户可以用nvm管理多版本。安装完成后,npm -v应该能正常输出版本号。
然后创建项目目录。我习惯用 Vite 起手,因为它的开发服务器启动快,热更新也灵敏。执行npm create vite@latest univer-demo -- --template vanilla-ts,进入目录后npm install。接着安装 Univer 相关包:
npm install @univerjs/core @univerjs/design @univerjs/engine-render @univerjs/sheets @univerjs/sheets-ui @univerjs/ui这些包的分工是:core提供基础模型和命令系统,design是 UI 组件库,engine-render是 Canvas 渲染引擎,sheets是表格数据模型,sheets-ui是表格界面,ui是通用 UI 框架。如果你需要公式,再加@univerjs/sheets-formula;需要协同,加@univerjs/pro-server。
提示:Univer 的包版本更新很快,建议锁定版本号,比如
@univerjs/core@0.1.0,避免自动升级导致 API 不兼容。我遇到过升级后createUniver方法签名变化的情况。
3.2 初始化引擎与挂载表格
在main.ts里写初始化逻辑。核心是创建一个 Univer 实例,注册插件,然后挂载到 DOM 容器上。代码大概长这样:
import { createUniver, LocaleType, merge } from '@univerjs/core'; import { UniverSheetsPlugin } from '@univerjs/sheets'; import { UniverSheetsUIPlugin } from '@univerjs/sheets-ui'; import { UniverUIPlugin } from '@univerjs/ui'; import { UniverRenderEnginePlugin } from '@univerjs/engine-render'; const { univerAPI } = createUniver({ locale: LocaleType.ZH_CN, theme: {}, plugins: [ UniverRenderEnginePlugin, UniverUIPlugin, UniverSheetsPlugin, UniverSheetsUIPlugin, ], }); univerAPI.createUniverSheet({});这段代码做了几件事:设置中文语言包,注册渲染引擎、UI 框架、表格数据模型和表格界面,最后创建一个空的表格实例。createUniverSheet会自动在页面上生成一个 Canvas 容器,默认占满父元素。
如果你要指定容器,可以在 HTML 里放一个<div id="app"></div>,然后createUniverSheet时传入container: document.getElementById('app')。注意容器必须有明确的宽高,否则 Canvas 尺寸算不出来,表格会显示为空白。
3.3 数据写入与公式配置
空表格没意义,接下来写入数据。Univer 的数据模型是IWorkbookData,结构跟 Excel 的 workbook 类似:有 sheet 列表,每个 sheet 有 cellData、rowData、columnData。写入一个 3×3 的表格:
const workbookData = { id: 'demo', sheetOrder: ['sheet1'], sheets: { sheet1: { id: 'sheet1', name: 'Sheet1', cellData: { 0: { 0: { v: '姓名' }, 1: { v: '年龄' } }, 1: { 0: { v: '张三' }, 1: { v: 28 } }, 2: { 0: { v: '李四' }, 1: { v: 32 } }, }, }, }, }; univerAPI.createUniverSheet(workbookData);cellData的键是行号和列号,从 0 开始。v是原始值,f是公式。如果要加公式,比如 C1 等于 A1+B1,写成{ f: '=A1+B1' }。公式插件会自动计算并显示结果。
这里有个细节:公式计算是异步的。如果你在写入数据后立刻读取单元格值,可能拿到的是公式字符串而不是计算结果。需要监听FormulaCalculated事件,或者在cellData里同时提供v和f,让 Univer 先显示缓存值,再异步更新。
3.4 样式与交互配置
Univer 的样式配置在cellData的s字段里,或者通过styles表统一管理。比如给表头加粗、加背景色:
styles: { header: { bl: 1, // 加粗 bg: { rgb: '#f0f0f0' }, cl: { rgb: '#333333' }, }, }, cellData: { 0: { 0: { v: '姓名', s: 'header' }, 1: { v: '年龄', s: 'header' } }, },bl是 bold 的缩写,bg是背景色,cl是文字颜色。这种简写风格在 Univer 里很常见,刚开始需要查文档,用多了就记住了。
交互方面,Univer 默认支持单元格选中、编辑、拖拽填充、复制粘贴。如果你要禁用某些操作,可以通过配置univerAPI.getConfig()来关闭。比如禁用拖拽填充:
univerAPI.getConfig().setConfig('sheets', { enableDragFill: false, });4. 实际开发中踩过的坑与排查技巧
4.1 Canvas 渲染白屏问题
白屏是最高频的问题。原因通常有三个:容器没有宽高、Canvas 尺寸计算时机不对、渲染引擎没注册。我遇到过一次,容器是display: none的,初始化时 Canvas 宽高为 0,后来显示出来也是白屏。解决办法是在容器可见后再初始化,或者手动调用univerAPI.getActiveWorkbook().getSheet().resize()触发重绘。
另一个白屏场景是 iOS Safari。Safari 对 Canvas 的尺寸限制更严格,如果表格行数太多,Canvas 高度超过 4096px 或 8192px,就会渲染失败。Univer 的做法是分片渲染,但需要配置renderEngine的maxCanvasHeight参数。我一般设成 4096,超过部分用虚拟滚动处理。
4.2 公式不计算或计算错误
公式插件加载了但结果不对,先检查公式语法。Univer 的公式语法跟 Excel 基本一致,但有些函数不支持,比如VLOOKUP的某些变体。如果公式引用了其他 sheet,需要确保 sheet 名称正确,并且用单引号包裹,比如='Sheet2'!A1。
还有一种情况是循环引用。A1 引用 B1,B1 又引用 A1,Univer 会检测到循环并返回错误值。排查方法是打开控制台,看有没有Circular dependency detected的警告。如果有,检查公式链,打断循环。
4.3 协同编辑冲突
多人同时编辑时,如果两个人同时改同一个单元格,后提交的会覆盖先提交的。Univer 的 OT 算法会尽量合并操作,但前提是操作类型一致。比如一个人改值,另一个人改样式,这两个操作可以合并;如果两个人都改值,就会产生冲突。
我建议在协同场景下,给每个用户分配独立的编辑区域,或者用“锁定单元格”功能。Univer 支持setCellLock,锁定的单元格只有特定用户能编辑。这个功能在财务、审批场景里很实用。
4.4 常见问题速查表
| 问题现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| 表格白屏 | 容器无宽高 | 检查 DOM 尺寸 | 设置明确宽高或延迟初始化 |
| 公式显示为文本 | 公式插件未加载 | 检查插件注册顺序 | 在渲染插件前注册公式插件 |
| 滚动卡顿 | 数据量过大 | 看帧率面板 | 启用虚拟滚动或分片渲染 |
| 导出图片空白 | Canvas 跨域污染 | 检查图片资源 | 使用同源图片或配置 CORS |
| 协同编辑冲突 | 同时修改同一单元格 | 看操作日志 | 启用单元格锁定或分区编辑 |
提示:Univer 的官方文档更新速度跟不上代码迭代速度,遇到 API 变化时,直接看源码里的 TypeScript 类型定义比查文档快。
node_modules/@univerjs/core/lib/types目录下有完整的类型声明。
5. 性能优化与扩展思路
5.1 大数据量下的渲染优化
Univer 默认会渲染所有可见单元格,但如果数据量到十万行级别,即使 Canvas 也扛不住。这时候需要开启虚拟滚动。Univer 的sheets-ui插件内置了虚拟滚动,但需要配置rowHeight和columnWidth的预估值。如果行高不固定,虚拟滚动会算错位置,导致滚动跳跃。
我的做法是:如果行高固定,直接设defaultRowHeight: 24;如果行高不固定,用getRowHeight回调动态计算,但回调里不要做复杂运算,否则滚动时会掉帧。实测下来,固定行高的场景下,十万行数据滚动能保持 40fps 以上。
5.2 自定义插件开发
Univer 的插件架构允许你扩展功能。比如你要加一个“一键导出 CSV”的按钮,可以写一个插件,注册一个命令,然后在 UI 上挂一个按钮。插件的基本结构是:
import { ICommand, CommandType, ICommandService } from '@univerjs/core'; export const ExportCSVCommand: ICommand = { id: 'demo.command.export-csv', type: CommandType.OPERATION, handler: async (accessor) => { const sheet = accessor.get(ICommandService).getActiveSheet(); // 导出逻辑 return true; }, };然后在插件里注册这个命令,并在 UI 上绑定快捷键或按钮。这种扩展方式很灵活,但需要熟悉 Univer 的依赖注入系统。accessor.get()是获取服务实例的标准方式,跟 Angular 的 DI 有点像。
5.3 与后端数据同步
Univer 的前端数据模型是IWorkbookData,后端可以用任何语言实现。我一般用 Node.js 写一个简单的 REST 接口,前端定时拉取或通过 WebSocket 推送。如果要做实时协同,就用@univerjs/pro-server,它封装了 OT 算法和 WebSocket 通信,你只需要实现用户认证和房间管理。
数据持久化方面,IWorkbookData可以直接序列化成 JSON 存数据库。但要注意,公式和样式是分开存储的,恢复时需要一起加载。我见过有人只存了cellData的v字段,结果公式全丢了。正确的做法是存完整的IWorkbookData对象。
6. 一些个人体会与后续可扩展的方向
Univer 最让我满意的地方是它的“可拆解性”。很多表格方案要么全包,要么全不包,Univer 给了你选择权。你可以只用它的数据模型和公式引擎,自己写渲染;也可以用它的 Canvas 渲染,自己写业务逻辑。这种灵活性在内部系统开发里很值钱,因为每个公司的需求都不一样。
不过它也有明显的短板。文档不够细,很多 API 要靠读源码;社区插件还不多,大部分功能要自己实现;协同编辑的部署成本不低,需要额外的 Node.js 服务和数据库。如果你只是做个简单的表格展示,用 AG Grid 或 Handsontable 可能更快;但如果你需要深度定制、私有化部署、或者嵌入到已有产品里,Univer 的架构优势就体现出来了。
后续我打算试试把 Univer 的公式引擎单独抽出来,跑在 Node.js 里做服务端计算。这样前端只负责展示,复杂公式在服务端算完再推给前端,能进一步降低浏览器压力。另外,Univer 的 Canvas 渲染层理论上可以复用到其他场景,比如甘特图、看板,只要把数据模型换掉就行。这个方向值得折腾。