Renderables 与 Constructs
OpenTUI 提供两种构建 UI 的方式:命令式 Renderable API 和声明式 Construct API。两种方式各有不同的权衡。
命令式(Renderables)
你使用 RenderContext 创建 Renderable 实例,并使用 add() 组合它们。你通过 setter 和方法直接修改实例上的状态和行为。
import { BoxRenderable, TextRenderable, InputRenderable, createCliRenderer, type RenderContext } from "@opentui/core"
const renderer = await createCliRenderer()
const loginForm = new BoxRenderable(renderer, {
id: "login-form",
width: 40,
height: 10,
padding: 1,
})
// 将多个 Renderable 组合成一个
function createLabeledInput(renderer: RenderContext, props: { label: string; placeholder: string; id: string }) {
const container = new BoxRenderable(renderer, {
id: `${props.id}-container`,
flexDirection: "row",
})
container.add(
new TextRenderable(renderer, {
id: `${props.id}-label`,
content: props.label + " ",
}),
)
container.add(
new InputRenderable(renderer, {
id: `${props.id}-input`,
placeholder: props.placeholder,
width: 20,
}),
)
return container
}
const username = createLabeledInput(renderer, {
id: "username",
label: "Username:",
placeholder: "Enter username...",
})
loginForm.add(username)
// 你必须导航到嵌套组件才能聚焦它
username.getRenderable("username-input")?.focus()
renderer.root.add(loginForm)
特点
- 创建时需要
RenderContext - 直接修改实例
- 嵌套组件访问需要手动导航
- 对组件生命周期有显式控制
声明式(Constructs)
使用函数式 constructs 构建轻量级的 VNode 图。实例在将节点添加到树之前不存在。VNodes 排队方法调用并在实例化时重放它们。
import { Text, Input, Box, createCliRenderer, delegate } from "@opentui/core"
const renderer = await createCliRenderer()
function LabeledInput(props: { id: string; label: string; placeholder: string }) {
return delegate(
{ focus: `${props.id}-input` },
Box(
{ flexDirection: "row" },
Text({ content: props.label + " " }),
Input({
id: `${props.id}-input`,
placeholder: props.placeholder,
width: 20,
}),
),
)
}
const usernameInput = LabeledInput({
id: "username",
label: "Username:",
placeholder: "Enter username...",
})
// delegate() 自动将焦点路由到嵌套的 input
usernameInput.focus()
const loginForm = Box(
{ width: 40, height: 10, padding: 1 },
usernameInput,
LabeledInput({
id: "password",
label: "Password:",
placeholder: "Enter password...",
}),
)
renderer.root.add(loginForm)
特点
- 实例化之前不需要
RenderContext - VNodes 排队方法调用
delegate()将 API 路由到嵌套组件- 声明式、类 React 的语法
delegate() 函数
delegate() 函数通过将方法调用从父元素路由到特定子元素,使 constructs 更易于使用:
function Button(props: { id: string; label: string; onClick: () => void }) {
return delegate(
{
focus: `${props.id}-box`, // 将 focus() 路由到 box
},
Box(
{
id: `${props.id}-box`,
border: true,
onMouseDown: props.onClick,
},
Text({ content: props.label }),
),
)
}
const button = Button({ id: "submit", label: "Submit", onClick: handleSubmit })
button.focus() // 聚焦内部的 Box
何时使用哪种方式
使用 Renderables 当
- 你需要对组件生命周期进行细粒度控制
- 你正在构建底层自定义组件
- 你需要立即访问 Renderable 方法
- 性能至关重要,你想避免 VNode 开销
使用 Constructs 当
- 你偏好声明式、组合式的代码
- 你正在构建更高级的 UI 组件
- 你想要更清晰、更可读的组件定义
- 你熟悉 React/Solid 模式
混合使用
你可以在同一应用中混合使用两种方式:
import { BoxRenderable, Text, Input } from "@opentui/core"
// 创建 Renderable 容器
const container = new BoxRenderable(renderer, {
id: "container",
flexDirection: "column",
})
// 向其添加 Constructs
container.add(Text({ content: "Title" }), Input({ placeholder: "Type here..." }))
renderer.root.add(container)