Constructs
Constructs 让你以声明式的、类似 React 的方式组合 UI。它们是创建 VNodes(虚拟节点)的工厂函数。VNode 是组件的轻量级描述。当你将 VNode 添加到树中时,它会变成一个真正的 Renderable。
基本用法
Constructs 看起来像是返回组件描述的函数调用:
import { createCliRenderer, Box, Text, Input } from "@opentui/core"
const renderer = await createCliRenderer()
renderer.root.add(
Box(
{ width: 40, height: 10, borderStyle: "rounded", padding: 1 },
Text({ content: "Welcome!" }),
Input({ placeholder: "Enter your name..." }),
),
)
将子元素作为属性对象之后的额外参数传递,而不是作为属性。
可用的 Constructs
大多数组核心 Renderable 类都有对应的 construct 函数:
import {
ASCIIFont,
Box,
Code,
FrameBuffer,
Input,
ScrollBox,
Select,
SyntaxStyle,
TabSelect,
Text,
RGBA,
} from "@opentui/core"
// 这些创建的是 VNodes,而不是真正的 Renderables
const syntaxStyle = SyntaxStyle.fromStyles({
default: { fg: RGBA.fromHex("#FFFFFF") },
})
const box = Box({ border: true })
const text = Text({ content: "Hello" })
const input = Input({ placeholder: "Type here..." })
const code = Code({ content: "const x = 1", filetype: "typescript", syntaxStyle })
const scrollBox = ScrollBox({ width: 40, height: 10 })
const frameBuffer = FrameBuffer({ width: 20, height: 10 })
const ascii = ASCIIFont({ text: "OPEN", font: "tiny" })
Constructs 的工作原理
当你调用 Box() 这样的 construct 时,它会创建一个 VNode —— 一个描述要创建什么的普通对象:
// 这创建的是 VNode,而不是真正的 BoxRenderable
const myBox = Box({ width: 20, height: 10 })
// VNode 在添加到树时才会被实例化
renderer.root.add(myBox) // 现在它变成了真正的 BoxRenderable
这种延迟创建让你可以:
- 在没有渲染上下文的情况下组合 UI
- 在组件存在之前排队方法调用
- 编写更清晰、更具声明性的代码
创建自定义 Constructs
通过编写返回 VNodes 的函数来创建可复用组件:
function LabeledInput(props: { label: string; placeholder: string }) {
return Box(
{ flexDirection: "row", gap: 1 },
Text({ content: props.label }),
Input({ placeholder: props.placeholder, width: 20 }),
)
}
renderer.root.add(
Box(
{ flexDirection: "column", padding: 1 },
LabeledInput({ label: "Name:", placeholder: "Enter name..." }),
LabeledInput({ label: "Email:", placeholder: "Enter email..." }),
),
)
VNodes 上的方法链
VNodes 支持方法调用。系统会排队这些调用并在创建组件后应用它们:
const input = Input({ id: "my-input", placeholder: "Type here..." })
// VNode 会排队这个调用
input.focus()
// 当添加到树时,系统会创建 input 并调用 focus()
renderer.root.add(input)
你也可以设置属性:
const box = Box({ id: "my-box" })
box.backgroundColor = RGBA.fromHex("#333366")
renderer.root.add(box)
delegate() 函数
组合组件通常需要将外部方法调用路由到特定的内部组件。delegate() 函数将方法和属性名称映射到后代 ID:
import { delegate, Box, Text, Input } from "@opentui/core"
function LabeledInput(props: { id: string; label: string; placeholder: string }) {
return delegate(
{
focus: `${props.id}-input`, // 将 focus() 路由到 input
value: `${props.id}-input`, // 将 value 属性访问路由到 input
},
Box(
{ flexDirection: "row" },
Text({ content: props.label }),
Input({
id: `${props.id}-input`,
placeholder: props.placeholder,
width: 20,
}),
),
)
}
const username = LabeledInput({ id: "username", label: "Username:", placeholder: "Enter username..." })
// 这实际上会让嵌套的 Input 获得焦点,而不是外层的 Box
username.focus()
renderer.root.add(username)
委托映射
映射对象的键是方法或属性名称,值是后代 ID:
delegate(
{
focus: "inner-input", // .focus() -> 查找后代 "inner-input" 并调用 focus()
blur: "inner-input", // .blur() -> 同上
add: "content-area", // .add() -> 将子元素添加到 "content-area"
value: "inner-input", // .value 的 get/set -> 代理到 "inner-input"
},
vnode,
)
使用子元素组合
自定义 constructs 可以接受并传递子元素:
function Card(props: { title: string }, ...children: VChild[]) {
return Box(
{ border: true, padding: 1, flexDirection: "column" },
Text({ content: props.title, fg: "#FFFF00" }),
Box({ flexDirection: "column" }, ...children),
)
}
renderer.root.add(Card({ title: "User Info" }, Text({ content: "Name: Alice" }), Text({ content: "Role: Admin" })))
混合使用 Renderables 和 Constructs
你可以混合使用两种方式:
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)
或者使用包含 Renderables 的 constructs:
const customRenderable = new CustomRenderable(renderer, { id: "custom" })
renderer.root.add(
Box(
{ padding: 1 },
Text({ content: "Header" }),
customRenderable, // 混入的普通 Renderable
Text({ content: "Footer" }),
),
)
下一步
请参阅 Renderables 与 Constructs 了解何时使用每种方式的详细比较。