如果你需要在网页里放一个"能编辑的表格",会发现选择比想象中少。

不是没有表格组件。AG Grid、TanStack Table 都是好东西,但它们本质是数据网格——擅长把一万行数据渲染得又快又好,按列排序筛选。可一旦用户想双点单元格改个值、拖一下填充柄、写个 =SUM(B2:B100)、把 A1 和 C1 合并起来,它们就不够用了。你要的是一个电子表格编辑器,不是表格。

而电子表格编辑器这个品类,长期被两家商业产品把持:SpreadJS 和 Handsontable。都能用,都有中文资料,都要按年付费、按开发者数量计费。国产的 Luckysheet 曾经是个免费的出路,但它的 GitHub 仓库最后更新停在 2025 年,README 现在写着一句话:

Luckysheet upgraded to Univer

Univer 就是 Luckysheet 团队的重写版本。 同一批人(DreamNum),推倒重来,用 TypeScript + Canvas + 插件化架构重新做了一遍。Apache-2.0 协议,可以商用。

为什么关注它

两个理由。

一是它刚发布了 1.0。 2026 年 9 月 24 日,v1.0.0 正式发布——这个项目从 2022 年 9 月开始迭代,四年时间才走到 1.0。1.0 不是修修补补,而是一次定位重写:从一个"表格组件"变成一个六个编辑器的统一 SDK。表格、文档、演示、白板、结构化数据、PDF,共享同一套插件系统、命令系统,每种内容有自己的 Facade API。

二是它在被真实使用。 npm 上的周下载量:

包

周下载量

@univerjs/core

571,240

@univerjs/sheets

516,098

@univerjs/preset-sheets-core

363,837

@univerjs/presets

290,113

(数据取自 npm downloads API,2026-09 下旬的一周)

周下载五十万次这个量级,说明不是玩具。GitHub 上 18,800 star,Apache-2.0,最后提交日期 2026-09-24——就是发 1.0 那天。

1.0 带来了什么

编辑器

1.0 的变化

Sheets 表格

公式引擎重构,跨工作簿引用、3D 引用(=SUM(Jan:Mar!B2))、形状可绑定公式、透视表增强 + 透视图

Docs 文档

现代模式与传统模式双模,目录、脚注、内容控件、LaTeX、行内公式

Slides 演示

编辑流程重写,母版与版式、图表、表格、可编辑 SmartArt

Boards 白板

全新画布,形状、连接线、思维导图、Mermaid 导入

Bases 结构化数据

网格 / 看板 / 日历 / 甘特四视图共用一份记录,字段关联、仪表盘

PDFs PDF

查看与编辑,页面管理、文本、图片、表格、批注

对你的意义取决于你要做的东西:只做表格,就只装 Sheets 相关包;要做"文档里嵌表格",1.0 支持嵌入式文档——表格可以嵌进文档并保持可编辑,两者各存各的数据模型。

许可证边界:先说清楚哪些要钱

这一点必须放在前面,因为它决定了你的技术选型成本。

开源部分(Apache-2.0)

需要商业许可

包命名

@univerjs/*

@univerjs-pro/*

表格核心

单元格、区域、公式、数字格式、筛选、排序、数据验证、条件格式、超链接、批注、查找替换

—

超出基础的部分

—

实时协作、编辑历史、导入导出、打印、图表、透视表、迷你图、形状

服务端

无头模式(Node.js 里跑 Facade 改数据)

协同服务、Office 文件转换、SSR

AI

—

AI SDK(@univer-cli/*)

用许可证的方式是注册一个插件:

import { UniverLicensePlugin } from '@univerjs-pro/license'

univer.registerPlugin(UniverLicensePlugin, {
  license: 'YOUR_CLIENT_LICENSE',
})

关键结论:如果你要的是"网页里能编辑、能算公式的表格",开源部分就够了,零成本。如果你要"导入 xlsx、导出 xlsx、多人同时编辑、打印"——这些是 Pro 能力,需要拿许可。别看到 Apache-2.0 就以为整个产品免费,这是最容易踩的预期错位。

安装

先决条件

项

要求

Node.js

无头模式 ≥ 18.17.0;开发本仓库需 ≥ 22.18

npm

≥ 7(npm 3~6 不会正确安装 peerDependencies)

pnpm

≥ 8(6~7 需要设 auto-install-peers=true)

浏览器

Chrome/Edge ≥ 88、Firefox ≥ 90、Safari ≥ 14.1、Electron ≥ 12

构建工具

Vite、esbuild、Webpack 5(Webpack 4 要手动映射路径)

浏览器基线在 1.0 从 Chrome 70 提到了 88,构建目标变了,注意你的构建配置。

两种集成模式

Univer 提供两条路,先选对再动手:

Preset 模式

Plugin 模式

装什么

@univerjs/presets + @univerjs/preset-sheets-core

十几个 @univerjs/* 包

代码量

约 15 行

约 60 行

控制粒度

粗(一个预设打包好了常用能力)

细(按需组合、可懒加载、可替换)

适合

快速验证、常规需求

要控制体积、要裁剪功能

先走 Preset 模式。等你知道自己要砍掉哪些功能了,再换 Plugin 模式。

# 网页端(Preset 模式)
npm install @univerjs/presets @univerjs/preset-sheets-core

# 服务端无头(Preset 模式)
npm install @univerjs/presets @univerjs/preset-sheets-node-core

版本必须对齐。 所有 @univerjs/* 和 @univerjs-pro/* 要用同一个版本号(当前 1.0.2)。这是官方反复强调的一条,混版本会出现各种离奇错误。

十五行跑起来

一个完整的、能编辑的电子表格:

<div id="app" style="height: 600px"></div>
import { createUniver, LocaleType, mergeLocales } from '@univerjs/presets'
import { UniverSheetsCorePreset } from '@univerjs/preset-sheets-core'
import UniverPresetSheetsCoreZhCN from '@univerjs/preset-sheets-core/locales/zh-CN'
import '@univerjs/preset-sheets-core/lib/index.css'

const { univerAPI } = createUniver({
  locale: LocaleType.ZH_CN,
  locales: {
    [LocaleType.ZH_CN]: mergeLocales(UniverPresetSheetsCoreZhCN),
  },
  presets: [
    UniverSheetsCorePreset({
      container: 'app',
    }),
  ],
})

univerAPI.createWorkbook({})

就这样。一个带工具栏、公式栏、右键菜单的中文表格编辑器就跑起来了。

三个要点:

  1. container 对应 DOM 的 id,容器必须有明确高度(height: 600px 或 100vh),否则你会看到一片空白——这是最常见的"装了没反应"。

  2. CSS 必须导入。@univerjs/preset-sheets-core/lib/index.css,漏了就是一堆错位的裸 DOM。

  3. 中文语言包路径是 locales/zh-CN,Univer 内置 19 种语言(含 zh-TW、zh-HK)。

核心概念

搞清这四个,剩下的查 API 就行。

概念

说明

Univer 实例

整个运行时的根,createUniver() 创建。挂在页面上就是一个编辑器

Unit

一个文档实例。一个工作簿是一个 Unit,工作表不是(它是工作簿的一部分)

Facade API

操作文档的高层接口,univerAPI 就是它的入口。浏览器和 Node.js 里用法一致

插件 / 命令

所有能力都是插件,所有修改都是命令。所以 undo()/redo() 天然就能用

Facade API 是分层包装的,记住这个链路:

univerAPI (FUniver)
  └─ workbook (FWorkbook)      工作簿:增删工作表、撤销重做、保存快照
       └─ worksheet (FWorksheet) 工作表:行列操作、冻结、合并区查询
            └─ range (FRange)     区域:读写值、公式、样式、格式

一个必须知道的规则:改数据是异步的。 Facade API 里凡是会修改文档的方法,很多返回 Promise 或异步生效。如果改完立刻读,可能拿到旧值。需要立刻读的话加 await 或等一拍:

sheet.getRange('C5').setFormula('=SUM(B2:B4)')
await new Promise((r) => setTimeout(r, 300))   // 等公式引擎算完
console.log(sheet.getRange('C5').getDisplayValue())   // "76.7"

速查表

FWorkbook —— 工作簿

方法

说明

getId() / getName() / setName(name)

标识与名称

getActiveSheet()

当前活动工作表

getSheets() / getNumSheets()

全部工作表 / 数量

getSheetByName(name)

按名字取(找不到返回 null)

getSheetBySheetId(id)

按 ID 取

create(name, rows, columns, opts?)

新建并指定尺寸

insertSheet(name?, opts?)

插入工作表

deleteSheet(sheet)

删除(传 sheetId 或 FWorksheet,不传名字)

duplicateSheet(sheet)

复制工作表

moveSheet(sheet, index)

移动位置

setActiveSheet(sheet)

切换活动表

undo() / redo()

撤销 / 重做

save()

导出快照(IWorkbookData)

setEditable(bool)

整体只读开关

getId() / getUrl()

标识与链接

FWorksheet —— 工作表

方法

说明

getRange(...)

取区域,见下方重载

getSheetName() / getSheetId()

名称与 ID

setName(name)

重命名

getMaxRows() / getMaxColumns()

表格尺寸

insertRows(index, n?) / deleteRows(index, n)

插入 / 删除行

insertColumns(index, n?) / deleteColumns(index, n)

插入 / 删除列

setRowHeight(row, h) / setColumnWidth(col, w)

行高列宽

setFrozenRows(n) / setFrozenColumns(n)

冻结

getMergedRanges()

所有合并区(判断是否合并的正确入口)

getMergeData() / getCellMergeData(row, col)

合并信息

setHiddenGridlines(bool)

隐藏网格线

setTabColor(color)

标签颜色

hideSheet() / showSheet()

隐藏 / 显示

clear() / clearContents() / clearFormats()

三种粒度的清除

getDataRegion()

已使用区域

FRange —— 区域(最常用)

方法

说明

getValue() / getValues()

读值(单个 / 二维数组)

getDisplayValue() / getDisplayValues()

读显示值(格式化后的字符串)

getRawValue() / getRawValues()

读原始值(不套格式)

setValue(v) / setValues(二维数组)

写值

getFormula() / getFormulas()

读公式

setFormula(f) / setFormulas(二维数组)

写公式

setNumberFormat(pattern)

数字格式(来自 sheets-numfmt)

setBackgroundColor(color) / getBackground()

底色

setFontColor / setFontSize / setFontWeight / setFontFamily

字体

setHorizontalAlignment / setVerticalAlignment

对齐

setWrap(bool) / setWrapStrategy(s)

自动换行

merge() / mergeAcross() / mergeVertically()

合并

breakApart()

取消合并

isMerged() / isPartOfMerge()

是否合并(语义不同,见坑表)

clearContent() / clearFormat() / clear()

清除内容 / 格式 / 全部

offset(row, col, rows?, cols?)

偏移出新区块

getA1Notation(withSheet?)

转 A1 表示法

getRow() / getColumn() / getLastRow() / getLastColumn()

边界

getWidth() / getHeight()

行列数

activate()

设为活动区域

getRange() 的四种写法

sheet.getRange(1, 0)              // 第 2 行第 A 列(0 基)
sheet.getRange(1, 0, 3, 4)        // 起始行、起始列、行数、列数
sheet.getRange('A1:C4')           // A1 表示法
sheet.getRange({ startRow: 1, startColumn: 0, endRow: 3, endColumn: 2 })

行列索引是 0 基的。 getRange(1, 0) 是 A2,不是 A1。而从 A1 表示法进来时,'A2' 就是 A2。同一个方法两套坐标系,这是最容易出错的地方。

五个实战场景

一、把后端数据渲染成一张表

const sheet = workbook.getActiveSheet()

const HEADER = ['设备编号', '温度(℃)', '压力(MPa)', '采集时间']
const ROWS = [
  ['DEV-001', 25.6, 1.2, '2026-09-26 10:00'],
  ['DEV-002', 26.3, 1.15, '2026-09-26 10:01'],
  ['DEV-003', 24.8, 1.22, '2026-09-26 10:02'],
]

// 一次写入整块,比逐个单元格快得多
sheet.getRange('A1:D1').setValues([HEADER])
sheet.getRange('A2:D4').setValues(ROWS)

// 表头样式
sheet.getRange('A1:D1')
  .setBackgroundColor('#e8f0fe')
  .setFontWeight('bold')
  .setHorizontalAlignment('center')

sheet.getRange('B2:B4').setNumberFormat('0.00')
sheet.setFrozenRows(1)
sheet.setColumnWidth(0, 110)

两个要点:

用 setValues() 一次写一块,不要循环 setValue()。每次写都是一个命令,一万个单元格就是一万次命令。

用 A1 表示法,别用行列索引。 getRange('A2:D4') 和 getRange(1, 0, 3, 4) 是同一个区域,但后者要你在脑子里做一次"从 0 数还是从 1 数"的换算——我自己就在这上面写错过一次(见坑表第一条)。只要不是动态计算行列,就用 A1 表示法。

二、公式:写进去、让它算、读回来

sheet.getRange('A6').setValue('平均温度')
sheet.getRange('B6').setFormula('=AVERAGE(B2:B4)')

sheet.getRange('A7').setValue('最高温度')
sheet.getRange('B7').setFormula('=MAX(B2:B4)')

sheet.getRange('A8').setValue('最大温差')
sheet.getRange('B8').setFormula('=MAX(B2:B4)-MIN(B2:B4)')

await new Promise((r) => setTimeout(r, 500))

console.log(sheet.getRange('B6').getDisplayValue())  // "25.5666666667"
console.log(sheet.getRange('B7').getDisplayValue())  // "26.3"
console.log(sheet.getRange('B8').getDisplayValue())  // "1.5"

公式由独立的公式引擎计算,是异步的。三个值的实际计算:25.6+26.3+24.8 = 76.7,平均 25.5667,最大温差 26.3-24.8 = 1.5。

想让它好看点,加个数字格式:

sheet.getRange('B6:B8').setNumberFormat('0.00')
// 现在 B6 显示 "25.57",B8 显示 "1.50"

注意公式里的 B2:B4 指的是行号 2 到 4,不是数组下标。 这里的数据正好写在 A2:D4,所以三行全含。范围写错一行不会报错,只会静默算出错误的数——=AVERAGE(B1:B3) 会得到 25.95 而不是 25.57,因为它们看起来都"像个合理的温度值"。

三、数字格式

setNumberFormat() 接受的是 Excel 风格的格式串:

格式串

输入

显示

'0.00'

25.5666666667

25.57

'#,##0.00'

1234.567

1,234.57

'0.00%'

0.2567

25.67%

'yyyy"年"mm"月"dd"日"'

2026-09-26

2026年09月26日

'¥#,##0'

128000

¥128,000

注意 getValue() 拿到的仍是数字 25.5666666667,getDisplayValue() 才是格式化后的字符串。做后续计算用前者,做展示用后者。

四、服务端无头处理

Univer 是同构的——同一套 Facade API 在浏览器和 Node.js 里用法一致,只是不带 UI。适合做批量处理:定时任务里生成报表、把数据库导出成工作簿快照、在 CI 里校验模板。

npm install @univerjs/presets @univerjs/preset-sheets-node-core
import { createUniver, LocaleType, mergeLocales } from '@univerjs/presets'
import { UniverSheetsNodeCorePreset } from '@univerjs/preset-sheets-node-core'
import sheetsZhCN from '@univerjs/preset-sheets-node-core/locales/zh-CN'
import { UniverSheetsNumfmtPlugin } from '@univerjs/sheets-numfmt'

const { univer, univerAPI } = createUniver({
  locale: LocaleType.ZH_CN,
  locales: { [LocaleType.ZH_CN]: mergeLocales(sheetsZhCN) },
  presets: [UniverSheetsNodeCorePreset()],
})

// ⚠️ 必须手动补这一个插件,原因见下方坑表第二条
univer.registerPlugin(UniverSheetsNumfmtPlugin)

const workbook = univerAPI.createWorkbook({ name: '报表' })
const sheet = workbook.getActiveSheet()

sheet.getRange('A6').setValue('平均温度')
sheet.getRange('B6').setFormula('=AVERAGE(B2:B4)')
await new Promise((r) => setTimeout(r, 500))
console.log('平均温度 =', sheet.getRange('B6').getDisplayValue())

const snapshot = workbook.save()      // 得到纯 JSON,可存库、可回传给前端
console.log(Object.keys(snapshot))    // id, sheetOrder, name, appVersion, locale, styles, sheets, resources

save() 返回的就是一个普通对象,可以 JSON.stringify 存进数据库。前端和服务端交换数据就靠这个快照。

但恢复时有个坑,先看下面场景五。

五、快照的往返

const snapshot = workbook.save()

// 服务端存库,前端取回来
const restored = univerAPI.createWorkbook(snapshot)   // ❌ 报错
// Error: [UniverInstanceService]: cannot create a unit with the same unit id: 5-lEak

快照里带着 id,直接恢复会因为单元 ID 重复而抛错。 改掉它就行:

const restored = univerAPI.createWorkbook({
  ...snapshot,
  id: `copy-${Date.now()}`,      // 换一个不重复的 id
})
console.log(restored.getSheets()[0].getRange('A1').getValue())   // 值都在

或者先 workbook.dispose() 把原工作簿释放掉,再用原快照恢复。工作表自己的 id 不用改,只有工作簿这一层的 unit id 会冲突——save() 出来的快照里 sheetOrder 那串 id 保持原样即可。

这对服务端流程尤其重要:每次从库里读出一份快照要恢复时,都得给它一个新的 unit id,否则第二次加载同一个文档就崩了。

这是 Univer 相比"渲染型表格组件"的根本差异:文档是可序列化的数据,不是 DOM 的投影。

我实测踩到的坑

下面这些都是实际跑出来的,不是从文档抄的。

一、行列索引是 0 基的,跟 A1 表示法混用会静默算错

getRange() 有两套坐标系:

sheet.getRange(1, 0)      // 行列索引:0 基 → 第 2 行、第 A 列
sheet.getRange('A1')      // A1 表示法:第 1 行、第 A 列

getRange(row, column, numRows, numColumns) 里的 row、column 是 0 基下标,而 numRows、numColumns 是个数。实测对照:

sheet.getRange(1, 0, 1, 4).getA1Notation()   // "A2:D2"  ← 不是 A1:D1
sheet.getRange(2, 0, 3, 4).getA1Notation()   // "A3:D5"

我自己就在这上面写错过一次数据。 当时混用了两种写法——表头用 getRange(1, 0, 1, 4) 写(落到第 2 行),数据用 getRange(2, 0, 3, 4) 写(落到第 3~5 行),公式却按直觉写了 =AVERAGE(B2:B4)。结果范围里多了一个表头文字单元格、漏了最后一行数据。

危险的是它不报错。 实测对照:

公式

实际计算

显示值

=AVERAGE(B2:B4)(正确,含三行)

(25.6+26.3+24.8)/3

25.57

=AVERAGE(B1:B3)(漏最后一行)

(25.6+26.3)/2

25.95

25.95 看起来是个完全合理的温度。而且 AVERAGE 会静默忽略文本单元格,所以范围里多含一个表头也不会报错。这类错误肉眼看结果发现不了,只能交叉验证。

规避办法两条:

  1. 静态区域一律用 A1 表示法('A2:D4'),只有循环里按索引访问时才用数字坐标;

  2. 用了数字坐标就立刻回头验证一次落点:

const r = sheet.getRange(1, 0, 3, 4)
console.log(r.getA1Notation())   // 先确认落在哪,再往里写

二、无头预设缺了 numfmt 插件(会直接抛错)

按官方文档用 UniverSheetsNodeCorePreset,然后调 setNumberFormat(),你会得到:

Error: [CommandService]: command "sheet.command.numfmt.set.numfmt" is not registered.

根因:@univerjs/preset-sheets-node-core 的源码里,一边 import '@univerjs/sheets-numfmt/facade'(所以 setNumberFormat 方法存在、能调用),一边插件列表里没有 UniverSheetsNumfmtPlugin(所以命令没注册)。方法存在但底层没有实现,调用即崩。

我核对了同期的网页端预设 @univerjs/preset-sheets-core——它两个都有,所以网页端没这个问题,只有无头预设会踩。

修复就是补一行:

import { UniverSheetsNumfmtPlugin } from '@univerjs/sheets-numfmt'

const { univer, univerAPI } = createUniver({
  // ...
  presets: [UniverSheetsNodeCorePreset()],
})

univer.registerPlugin(UniverSheetsNumfmtPlugin)   // ← 补上

补完之后实测正常:1234.567 用 '#,##0.00' 显示为 1,234.57,0.2567 用 '0.00%' 显示为 25.67%,日期格式输出 2026年09月26日。

三、isMerged() 不是"这个区域被合并了吗"

它要求范围完全相等。合并了 A1:C1 之后:

sheet.getRange('A1:C1').merge()

sheet.getRange('A1:C1').isMerged()   // true
sheet.getRange('A1').isMerged()      // false ← 别以为是 bug
sheet.getRange('A1').isPartOfMerge() // true  ← 判断"在不在合并区里"要用这个
worksheet.getMergedRanges()          // 列出全部合并区,最可靠

源码注释原话是 Return true only for an exact merged range match. Use isPartOfMerge() to check overlap. 我第一次测的时候查了 A1 拿到 false,以为合并异步失败了,查了源码才明白是语义问题。

实践建议:要判断某格是否在合并区内,用 isPartOfMerge();要列出所有合并区,用 getMergedRanges()。

四、deleteSheet() 不认名字

workbook.deleteSheet('第三车间')        // false ← 名字无效
workbook.deleteSheet(sheet.getSheetId()) // true
workbook.deleteSheet(sheetObject)        // true

文档注释写的是 deleteSheet(sheet: FWorksheet | string),那个 string 指的是 sheetId,不是 sheetName。注意 getSheetByName('名字') 是接受名字的——同一个类里两个方法对 string 的含义不一样。

五、改完数据立刻读可能拿到旧值

前面反复出现过。所有修改类 Facade API 都可能异步生效,涉及公式、合并、格式时尤其明显。稳妥写法:

sheet.getRange('A1:C1').merge()
await new Promise((r) => setTimeout(r, 100))
// 现在再查状态

或者用生命周期钩子,等引擎进入稳定阶段再操作:

univerAPI.addEvent(univerAPI.Event.LifeCycleChanged, ({ stage }) => {
  if (stage === univerAPI.Enum.LifecycleStages.Rendered) {
    // 渲染完成
  }
  if (stage === univerAPI.Enum.LifecycleStages.Steady) {
    // 稳定,可以放心读写
  }
})

六、快照的 id 不能复用

const snapshot = workbook.save()
univerAPI.createWorkbook(snapshot)
// Error: [UniverInstanceService]: cannot create a unit with the same unit id: 5-lEak

快照里带着原工作簿的 id,同一个 Univer 实例里再建一个同 id 的单元会直接抛错。实测三种处理:

做法

结果

直接 createWorkbook(snapshot)

❌ 报 unit id 重复

改掉 snapshot.id 再恢复

✅ 成功,数据与数字格式都在

先 workbook.dispose() 再恢复原快照

✅ 成功

工作表层面的 id(快照 sheetOrder 里那串)不需要改,实测保持原样也能正常恢复。

对服务端流程的影响:从库里读出一份快照要恢复时,必须给它一个新的 unit id。否则同一个进程里第二次加载同一份文档就会崩——这个坑在"读库 → 生成 → 返回"的批处理里很容易踩到。

七、TypeScript 报 CSS 导入错误

import '@univerjs/preset-sheets-core/lib/index.css'
// error TS2882: Cannot find module or type declarations for side-effect import

TS 不认识 CSS 模块。在 tsconfig.json 里加一行:

{
  "compilerOptions": {
    "types": ["vite/client"]
  }
}

Vite 项目加了就过。Webpack 项目换成对应的类型声明。

八、包体积要有预期

一个最小的表格编辑器(Preset 模式 + 中文语言包),Vite 生产构建产物实测:

项

未压缩

主 JS chunk

6.78 MB

CSS

116 KB

全部文件(80 个,含各语言包分包)

约 11.1 MB

未压缩 6.78 MB 的主包,gzip 后通常降到 1.5~2 MB 量级,但这个体量意味着首屏加载要当回事。两个应对:走 Plugin 模式只装需要的插件;或者用预设的 workerURL 把公式计算挪进 Web Worker,同时做代码分割。

九、0.x 升 1.0 的破坏性变更

如果你已经有一个 0.25 的项目,1.0 有几个必须改的地方:

旧 API

新 API

createUniverSheet()

createWorkbook()

getActiveUniverSheet()

getActiveWorkbook()

getUniverSheet()

getWorkbook()

旧事件钩子

addEvent()

importXLSXToSnapshotAsync()

importSheetToSnapshotAsync()

Uniscript 自定义公式

已移除,改为把可信函数打进 Worker 注册

另外 presets 不再提供 UMD 构建——如果你是用 <script> 标签直接引的,必须换成模块化构建。

0.25 系列的维护政策:从 1.0 发布起只提供一年安全更新。也就是说 2027 年 9 月起,0.25 连安全补丁都没有了。

上手路径

阶段

做什么

目标

半天

Preset 模式跑通最小示例,改改预设配置、换个语言包

确认能嵌进你的页面

一两天

把真实数据写进去,加公式、数字格式、表头样式

达到"能看"

一周

试 Facade API:工作表增删、冻结、合并区、快照往返;评估包体积能否接受

达到"能用"

按需

需要导入导出 xlsx / 协作 / 打印 → 走 Plugin 模式 + 评估 Pro 许可

决定是否投入

判断要不要上车的三个问题:

  1. 你要的是表格编辑器(用户改单元格、写公式)还是数据展示网格(万行滚动、列筛选)?后者用 AG Grid 这类更合适,更轻。

  2. 你需要 xlsx 导入导出吗?需要的话这是 Pro 能力,要先谈许可。

  3. 你接受 6~7 MB 的主包吗?如果这是个首屏加载要求极严格的小程序/H5,得先做体积优化方案。

三个都是"是"或都能接受,Univer 是当下开源选项里最完整的一个。

参考

资源

地址

官网

https://univer.ai

文档

https://docs.univer.ai

在线试用(六种编辑器)

https://office.univer.ai

能力矩阵

https://univer.ai/capabilities

GitHub

https://github.com/dream-num/univer

1.0 发布说明

https://docs.univer.ai/blog/univer-1-0

API 参考

https://docs.univer.ai/reference

开源工作区示例

https://github.com/dream-num/univer-workspace

无头模式文档

https://docs.univer.ai/guides/sheets/getting-started/node

AI SDK

https://docs.univer.ai/ai

Server SDK

https://docs.univer.ai/server

版本与数据核对时间:2026-09-26。当前最新版本 1.0.2(2026-09-24 发布,与 v1.0.0 同日)。