插件插槽

大多数 TUI 应用最初都是单一代码库。随着功能和团队的增长,您可能需要扩展点,而无需让用户 fork 应用。

插件插槽为您提供了解决方案。宿主应用在布局中定义命名位置(例如状态栏、侧边栏或面板),插件在运行时为这些位置贡献 UI。

宿主保持对布局、渲染模式和类型契约的控制。插件只能接收宿主有意暴露的上下文和插槽属性。

本页描述了 Core、React 和 Solid 绑定所构建的共享注册表 API。如果您正在构建应用,请从您使用的绑定对应的页面开始:

如果您在运行时从磁盘加载插件模块,请参阅 React 插槽页面Solid 插槽页面上的运行时支持说明。对于自定义宿主,请使用 @opentui/core/runtime-plugin-support 或从 @opentui/core/runtime-plugin 中的 createRuntimePlugin

如果您需要了解底层模型、想为其他框架构建自定义绑定,或需要直接访问注册表 API,请继续阅读。

概念

  • 宿主(Host):定义插槽名称和插槽属性类型。
  • 插件(Plugin):贡献一个或多个插槽渲染回调。可能包含生命周期钩子。
  • 注册表(Registry):注册插件并解析插槽的贡献。
  • 插槽模式(Slot mode):控制插件输出和回退 UI 的组合方式(appendreplacesingle_winner)。

定义插槽和宿主上下文

插槽名称映射到每个插槽接收的属性。宿主上下文是一个传递给每个插件渲染器的共享对象。

import type { PluginContext } from "@opentui/core"

type AppSlots = {
  statusbar: { user: string }
  sidebar: { section: "left" | "right" }
}

interface AppContext extends PluginContext {
  appName: string
  version: string
}

上下文可以是任何对象。PluginContextobject 的类型别名——扩展它不是必须的,但可以为您的插件提供一个可引用的命名类型。

创建注册表

import { createSlotRegistry } from "@opentui/core"

type AppSlots = {
  statusbar: { user: string }
  sidebar: { section: "left" | "right" }
}

const context = { appName: "my-app", version: "1.0.0" }

const registry = createSlotRegistry<string, AppSlots, typeof context>(renderer, "my-app:plugins", context)

第一个类型参数(TNode)是您的框架返回的节点类型——Core 为 BaseRenderable,React 为 ReactNode,Solid 为 JSX.Element。框架特定的辅助函数会为您填充这个类型。

createSlotRegistry 是以渲染器为作用域的。对于给定的 (renderer, key) 对,您总是得到同一个注册表实例。key 参数在渲染器内对注册表命名空间化,以便多个独立注册表可以共存。

使用相同的 (renderer, key) 对再次调用 createSlotRegistry 会返回现有注册表并通过 configure() 应用新的 optionscontext 参数必须是相同的对象引用——传递不同的上下文对象会抛出错误。这意味着您应该创建一次上下文对象并重复使用:

// 正确 — 相同的对象引用
const context = { appName: "my-app" }
const reg1 = createSlotRegistry(renderer, "my-key", context)
const reg2 = createSlotRegistry(renderer, "my-key", context) // 返回 reg1

// 抛出错误 — 即使结构相同,也是不同的对象
const reg3 = createSlotRegistry(renderer, "my-key", { appName: "my-app" })

当渲染器被销毁时,所有以它为作用域的注册表都会自动清除和释放。

框架特定的辅助函数(createCoreSlotRegistrycreateReactSlotRegistrycreateSolidSlotRegistry)内部使用固定键调用 createSlotRegistry。仅当您需要每个渲染器有多个独立注册表时才直接使用 createSlotRegistry

注册表选项

所有 create*SlotRegistry 函数接受一个可选的 SlotRegistryOptions 对象:

选项类型默认值描述
onPluginError(event: PluginErrorEvent) => void每次插件错误时调用的回调
debugPluginErrorsbooleanfalsetrue 时,错误也会通过 console.debug 记录
maxPluginErrorsnumber100缓冲区中最大错误数,超出后最旧的将被丢弃

注册插件

const unregister = registry.register({
  id: "clock-plugin",
  order: 0,
  setup(ctx, renderer) {
    // 注册时调用一次 — 在此处初始化资源
  },
  dispose() {
    // 插件被注销时调用 — 在此处清理资源
  },
  slots: {
    statusbar(ctx, props) {
      return `${ctx.appName}:${props.user}`
    },
  },
})

// 稍后:移除此插件
unregister()

register() 返回一个注销函数。调用它会移除插件并调用其 dispose 钩子。

插件接口

字段类型必需描述
idstring唯一标识符。重复的 id 会抛出错误。
ordernumber排序优先级(升序)。默认为 0
setup(ctx, renderer) => void注册时调用一次。如果抛出错误,插件将不会被注册。
dispose() => void插件被注销或注册表被清除时调用。
slots{ [slotName]: (ctx, props) => TNode }插槽渲染回调。每个回调接收宿主上下文和插槽特定的属性。

Core 绑定通过托管插槽对象扩展了此接口,添加了生命周期钩子。

排序

插件按以下顺序解析:

  1. order 升序(数字小的优先)
  2. 注册顺序(先注册的优先)
  3. id 字典序(平局时使用)

插槽模式

每个插槽挂载或 <Slot> 组件都接受一个 mode。该模式控制插件输出和回退 UI 的组合方式。

模式行为
append回退内容在前,然后是所有插件输出(默认)
replace仅插件输出;仅当没有插件产生输出时才显示回退内容
single_winner仅按解析顺序取第一个插件;如果它没有产生输出则显示回退内容

解析贡献

const entries = registry.resolveEntries("statusbar")
// Array<{ id: string, renderer: (ctx, props) => TNode }>

const slotRenderers = registry.resolve("statusbar")
// Array<(ctx, props) => TNode>

这里的 renderer 指的是插件的插槽渲染回调,而不是 CliRenderer 实例。

当您需要插件 id 和回调一起使用时,请使用 resolveEntries。当您只需要回调时,请使用 resolve

注册表方法

方法描述
register(plugin)添加一个插件。返回一个注销函数。
unregister(id)按 id 移除插件。如果找到则返回 true
updateOrder(id, order)更改插件的排序顺序。如果找到则返回 true
clear()移除并释放所有插件。
resolve(slot)返回有序的插槽渲染回调。
resolveEntries(slot)返回有序的 { id, renderer } 条目。
subscribe(listener)监听注册变更。返回一个取消订阅函数。React 和 Solid 在内部使用此方法来触发重新渲染。
configure(options)创建后更新 SlotRegistryOptions
onPluginError(listener)监听插件错误。返回一个取消订阅函数。
getPluginErrors()返回缓冲的 PluginErrorEvent 数组。
clearPluginErrors()清除错误缓冲区。
reportPluginError(report)手动报告插件错误。框架集成使用此方法。
renderer获取器 — 此注册表所绑定的 CliRenderer
context获取器 — 宿主上下文对象。

错误处理

注册表暴露插件错误事件:

registry.onPluginError((event) => {
  console.error(event.pluginId, event.phase, event.source, event.error.message)
})

您也可以读取和清除缓冲的错误:

const history = registry.getPluginErrors()
registry.clearPluginErrors()

PluginErrorReport

reportPluginError 方法接受一个 PluginErrorReport

字段类型必需描述
pluginIdstring导致错误的插件
slotstring | undefined插槽名称(如果错误是插槽特定的)
phasePluginErrorPhase"setup""render""dispose""error_placeholder"
sourcePluginErrorSource如果省略则默认为 "registry"
errorunknown原始错误 — 内部会规范化为 Error

PluginErrorEvent

字段类型描述
pluginIdstring导致错误的插件
slotstring | undefined插槽名称(如果错误是插槽特定的)
phasePluginErrorPhase"setup""render""dispose""error_placeholder"
sourcePluginErrorSource"registry""core" 或框架定义的来源字符串
errorError规范化的错误对象
timestampnumber错误发生时的 Date.now()

下一步:具体宿主 API

将上述共享模型与以下宿主集成之一配合使用: