插件插槽
大多数 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 的组合方式(
append、replace或single_winner)。
定义插槽和宿主上下文
插槽名称映射到每个插槽接收的属性。宿主上下文是一个传递给每个插件渲染器的共享对象。
import type { PluginContext } from "@opentui/core"
type AppSlots = {
statusbar: { user: string }
sidebar: { section: "left" | "right" }
}
interface AppContext extends PluginContext {
appName: string
version: string
}
上下文可以是任何对象。PluginContext 是 object 的类型别名——扩展它不是必须的,但可以为您的插件提供一个可引用的命名类型。
创建注册表
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() 应用新的 options。context 参数必须是相同的对象引用——传递不同的上下文对象会抛出错误。这意味着您应该创建一次上下文对象并重复使用:
// 正确 — 相同的对象引用
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" })
当渲染器被销毁时,所有以它为作用域的注册表都会自动清除和释放。
框架特定的辅助函数(createCoreSlotRegistry、createReactSlotRegistry、createSolidSlotRegistry)内部使用固定键调用 createSlotRegistry。仅当您需要每个渲染器有多个独立注册表时才直接使用 createSlotRegistry。
注册表选项
所有 create*SlotRegistry 函数接受一个可选的 SlotRegistryOptions 对象:
| 选项 | 类型 | 默认值 | 描述 |
|---|---|---|---|
onPluginError | (event: PluginErrorEvent) => void | — | 每次插件错误时调用的回调 |
debugPluginErrors | boolean | false | 为 true 时,错误也会通过 console.debug 记录 |
maxPluginErrors | number | 100 | 缓冲区中最大错误数,超出后最旧的将被丢弃 |
注册插件
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 钩子。
插件接口
| 字段 | 类型 | 必需 | 描述 |
|---|---|---|---|
id | string | 是 | 唯一标识符。重复的 id 会抛出错误。 |
order | number | 否 | 排序优先级(升序)。默认为 0。 |
setup | (ctx, renderer) => void | 否 | 注册时调用一次。如果抛出错误,插件将不会被注册。 |
dispose | () => void | 否 | 插件被注销或注册表被清除时调用。 |
slots | { [slotName]: (ctx, props) => TNode } | 是 | 插槽渲染回调。每个回调接收宿主上下文和插槽特定的属性。 |
Core 绑定通过托管插槽对象扩展了此接口,添加了生命周期钩子。
排序
插件按以下顺序解析:
order升序(数字小的优先)- 注册顺序(先注册的优先)
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:
| 字段 | 类型 | 必需 | 描述 |
|---|---|---|---|
pluginId | string | 是 | 导致错误的插件 |
slot | string | undefined | 否 | 插槽名称(如果错误是插槽特定的) |
phase | PluginErrorPhase | 是 | "setup"、"render"、"dispose" 或 "error_placeholder" |
source | PluginErrorSource | 否 | 如果省略则默认为 "registry" |
error | unknown | 是 | 原始错误 — 内部会规范化为 Error |
PluginErrorEvent
| 字段 | 类型 | 描述 |
|---|---|---|
pluginId | string | 导致错误的插件 |
slot | string | undefined | 插槽名称(如果错误是插槽特定的) |
phase | PluginErrorPhase | "setup"、"render"、"dispose" 或 "error_placeholder" |
source | PluginErrorSource | "registry"、"core" 或框架定义的来源字符串 |
error | Error | 规范化的错误对象 |
timestamp | number | 错误发生时的 Date.now() |
下一步:具体宿主 API
将上述共享模型与以下宿主集成之一配合使用: