插件与 Facade 扩展
按需组装功能,通过公开 API 扩展内容操作与业务界面。
Univer 通过插件组合产品与功能,通过 Facade 向应用开放内容操作。你可以选择需要的内容引擎、功能和界面,也可以把业务逻辑封装成插件,再提供带类型提示的 API,供自己的界面或 Agent 工具调用。
按需组装内容能力
Preset 是已经组合好的插件集合,适合快速接入;Plugin 模式让应用显式选择产品、功能及配置。两者使用同一套插件机制。
| 层次 | 职责 | 典型组成 |
|---|---|---|
| 产品与内容模型 | 管理 Unit、内容结构与基础操作 | Sheet、Doc、Slide、Relational Table、Canvases、PDF 的产品插件。 |
| 功能插件 | 增加特定领域能力 | 公式、筛选、图表、评论等。 |
| UI 与渲染插件 | 提供可视化呈现和交互入口 | Canvas 渲染、菜单、工具栏、快捷键与组件。 |
| Facade 扩展 | 为已安装能力提供公开调用接口 | 工作簿、范围、文档和对象操作,以及语义事件。 |
产品逻辑与 UI 分离,使适合无头运行的内容能力能够在 Node.js、Worker 或浏览器中复用。Headless 宿主按需注册内容插件;菜单、DOM 和交互相关 API 仍需要对应的 UI 环境。
依赖、生命周期与资源归属
插件通过 @DependentOn(...) 声明直接依赖,运行时解析依赖并协调构造顺序。缺失的依赖插件会以默认配置注册;需要自定义配置时,宿主应先注册并配置依赖。它不会代替包安装,也不会自动引入样式、语言资源、Facade 入口或 Worker。
插件内部使用依赖注入(DI)连接 Service、Controller 和模型。模块依赖公开的服务标识符,平台相关实现可在支持的配置或注入边界替换;业务逻辑不必自行创建并维护所有依赖。
| 生命周期 | 适合执行的工作 |
|---|---|
onStarting() | 注册依赖、命令和控制器;此时不要读取尚未创建的 Unit 或 DOM。 |
onReady() | 在对应内容就绪后初始化依赖模型的逻辑。 |
onRendered() | 初始化需要渲染器、Canvas 或 DOM 的扩展。 |
onSteady() | 延后非首屏必需的工作。 |
命令注册、事件订阅、快捷键和渲染模块等资源应交给拥有者管理。对返回 disposable 的注册调用使用 disposeWithMe(),让资源随插件或控制器一起释放。mergeMenu() 当前返回 void,不能当作 disposable 使用。
不同入口,共用内容变更机制
工具栏、快捷键和业务 API 可以调用同一业务命令;Agent 宿主也能在安装对应能力后复用该入口。命令系统把业务意图、持久化变更和临时交互分开。
| 类型 | 职责 | 示例 |
|---|---|---|
COMMAND | 校验目标、组织业务步骤,编排 Mutation 或 Operation。 | 写入销售报告、删除一行。 |
MUTATION | 使用明确、可序列化的参数修改内容模型,供重放及对应的协同处理使用。 | 修改单元格内容、插入行。 |
OPERATION | 修改临时交互状态。 | 调整选区、滚动位置、面板状态。 |
例如,FRange.setValues() 会调用 SetRangeValuesCommand,由命令生成 SetRangeValuesMutation 并安排相应的撤销数据。内容变更进入同一条执行链,不同入口可以复用已有的校验、拦截及变更处理。图中的撤销、协同和历史记录仍需对应机制;注册一个 Mutation 本身不会自动获得这些能力。
把插件能力扩展为 Facade API
Facade 提供面向内容对象的公开接口,隐藏内部服务查找和命令参数编排。扩展可以组合现有 Facade 方法,也可以调用插件自己的业务命令。
下面为 FUniver 增加 generateSalesReport();业务命令将在下一节实现。三部分分别负责运行时方法、运行时挂载和 TypeScript 类型提示。
sales-report.facade.ts
import { FUniver } from '@univerjs/core/facade';import type { ISalesReportParams } from './sales-report.plugin';import { GenerateSalesReportCommand } from './sales-report.plugin';export interface IFUniverSalesReport { generateSalesReport(params: ISalesReportParams): Promise<boolean>;}export class FUniverSalesReport extends FUniver implements IFUniverSalesReport { override generateSalesReport(params: ISalesReportParams): Promise<boolean> { return this._commandService.executeCommand(GenerateSalesReportCommand.id, params); }}FUniver.extend(FUniverSalesReport);declare module '@univerjs/core/facade' { interface FUniver extends IFUniverSalesReport {}}FUniver.extend() 在运行时安装方法;模块增强只补充类型。接入方必须执行扩展文件的 side-effect import,仅导出类或注册插件不会执行另一个 Facade 入口。发布为包时可暴露独立的 /facade 子路径,并通过 package.json 的 sideEffects 保留这一注册副作用。
只对明确支持 extend() 的 Facade 类使用这种方式;导出了某个 Facade 类,不代表它一定支持 mixin。扩展方法应使用明确的业务命名,避免与已有方法或其他扩展重名。
案例:让应用与 Agent 生成同一份销售报告
这个最小业务插件把英文销售数据、汇总公式和表头样式写入指定 Sheet 的 A1:D5,会覆盖该区域。它显式接收工作簿和工作表 ID,不依赖当前选区或焦点。
命令先检查目标及区域大小,再调用已有的 SetRangeValuesCommand,复用它的内容变更和撤销/重做逻辑;插件在 onStarting() 注册命令,并保存释放句柄。固定数据用于演示,实际应用可改为经过校验的业务输入。
示例输出。为便于阅读,截图补充了底色、数字显示格式与行列尺寸。
sales-report.plugin.ts
import type { ICellData, ICommand, Workbook } from '@univerjs/core';import { CommandType, DependentOn, ICommandService, Inject, Injector, IUniverInstanceService, Plugin, UniverInstanceType,} from '@univerjs/core';import { SetRangeValuesCommand, UniverSheetsPlugin } from '@univerjs/sheets';export interface ISalesReportParams { unitId: string; subUnitId: string;}export const GenerateSalesReportCommand: ICommand<ISalesReportParams> = { id: 'sales-report.command.generate', type: CommandType.COMMAND, handler: (accessor, params) => { if (!params) return false; const sheet = accessor.get(IUniverInstanceService) .getUnit<Workbook>(params.unitId, UniverInstanceType.UNIVER_SHEET) ?.getSheetBySheetId(params.subUnitId); if (!sheet || sheet.getRowCount() < 5 || sheet.getColumnCount() < 4) return false; const value: ICellData[][] = [ ['Region', 'Q1', 'Q2', 'H1 Total'].map((v) => ({ v, t: 1, s: { bg: { rgb: '#17324D' }, cl: { rgb: '#FFFFFF' }, bl: 1 }, })), [{ v: 'East', t: 1 }, { v: 128000, t: 2 }, { v: 142000, t: 2 }, { f: '=SUM(B2:C2)' }], [{ v: 'West', t: 1 }, { v: 96000, t: 2 }, { v: 118000, t: 2 }, { f: '=SUM(B3:C3)' }], [{ v: 'North', t: 1 }, { v: 110000, t: 2 }, { v: 125000, t: 2 }, { f: '=SUM(B4:C4)' }], [{ v: 'Total', t: 1 }, { f: '=SUM(B2:B4)' }, { f: '=SUM(C2:C4)' }, { f: '=SUM(D2:D4)' }], ]; return accessor.get(ICommandService).executeCommand(SetRangeValuesCommand.id, { ...params, range: { startRow: 0, endRow: 4, startColumn: 0, endColumn: 3 }, value, }); },};@DependentOn(UniverSheetsPlugin)export class SalesReportPlugin extends Plugin { static override pluginName = 'SALES_REPORT_PLUGIN'; static override type = UniverInstanceType.UNIVER_SHEET; constructor( _config: undefined, @Inject(Injector) protected override readonly _injector: Injector, @ICommandService private readonly _commands: ICommandService, ) { super(); } override onStarting(): void { this.disposeWithMe(this._commands.registerCommand(GenerateSalesReportCommand)); }}宿主先按照 Sheet 接入指南配置好产品与公式能力,再注册业务插件、导入 Facade 扩展并调用。这里的 univer 是已经完成该配置的实例;Node.js 的对应配置见 Headless Web SDK。
import { FUniver } from '@univerjs/core/facade';import '@univerjs/sheets/facade';import '@univerjs/engine-formula/facade';import { SalesReportPlugin } from './sales-report.plugin';import './sales-report.facade';univer.registerPlugin(SalesReportPlugin);const univerAPI = FUniver.newAPI(univer);const workbook = univerAPI.createWorkbook({ name: 'Quarterly Sales' });const worksheet = workbook.getActiveSheet();const target = { unitId: workbook.getId(), subUnitId: worksheet.getSheetId() };const generated = await univerAPI.generateSalesReport(target);if (!generated) throw new Error('Sales report could not be generated');await univerAPI.getFormula().onCalculationResultApplied();console.log(worksheet.getRange('D5').getValue()); // 719000命令返回成功表示内容写入已完成;读取公式值前还需等待计算结果应用。这个示例的 D5 为 719000。
应用按钮可在自己的点击处理器中调用 generateSalesReport(target);Agent 的工具处理器在 Headless 宿主中安装同一插件和 Facade 扩展后,也可调用该方法。业务动作及其目标保持一致,UI 只负责入口和反馈。安装自定义插件不会自动把它注册为 Agent 工具,工具暴露和 Unit 授权仍由应用安排。
实现边界与继续阅读
- 复用已有内容命令:本例复用
SetRangeValuesCommand。新增 Mutation 时还需设计反向操作与相应的协同适配;引入新的持久化资源时也需处理序列化,不要直接修改底层模型。 - 区分执行、计算与提交:多个 Facade 调用不是自动原子事务。本例使用一次范围写入;协同宿主还需按 Runtime 的规则提交并确认。
- 按环境组合:内容插件与 UI 插件分别接入,相关 Univer 包保持同一发布版本,界面文案使用语言资源。Agent 入口仍需应用自己的授权与参数检查。
了解更多:Univer 架构 · Facade 接入 · Facade API 参考 · UI 定制 · Agent 与自动化
你觉得这篇文档如何?