组件简介
组件是任何 euv 应用的构建块。它们让你将 UI 拆分为独立的、可复用的片段,每个片段都有自己的逻辑和展示。在 euv 中,组件是接受一个 VirtualNode(携带 props 和子节点)并返回一个 VirtualNode(描述渲染输出)的函数。
定义组件
要在 euv 中定义组件,你需要两个东西:
- 一个 Props 结构体,定义组件接受的数据
- 一个用
#[component]属性装饰的组件函数
Props 结构体
Props 结构体定义了组件接收的数据的形状。它必须派生 Clone 和 Default:
#[derive(Clone, Default)]
struct MyCardProps {
title: &'static str,
}
Props 可以是各种类型,包括:
-
&'static str— 静态字符串切片,适合固定文本 -
String— 拥有的字符串,适合动态文本 -
bool— 布尔值,用于标志位 -
Signal<bool>— 响应式布尔信号 -
i32、f64— 数值类型 -
Option<Rc<dyn Fn(Event)>>— 可选的回调函数 -
Signal<T>— 任意类型的响应式信号 -
VirtualNode— 子虚拟节点 -
Css— CSS 样式值
组件函数
组件函数接受一个 VirtualNode<YourProps> 并返回一个 VirtualNode:
#[component]
pub fn my_card(node: VirtualNode<MyCardProps>) -> VirtualNode {
let MyCardProps { title, .. } = node.try_get_props().unwrap_or_default();
let children: VirtualNode = node.try_get_child_node();
html! {
div {
h3 { title }
children
}
}
}
让我们逐步分析:
#[component]— 这个属性将函数标记为组件,使其能够通过VirtualNode包装器接收 props 和子节点。node: VirtualNode<MyCardProps>— 函数接收一个用 props 类型参数化的VirtualNode。这个节点携带 props 和任何子节点。node.try_get_props()— 该方法尝试从虚拟节点中提取类型化的 props。unwrap_or_default()调用在提取失败时提供默认值(例如,当组件在没有 props 的情况下使用时)。node.try_get_child_node()— 该方法提取传递给组件的子节点。这些可以在组件的模板内部渲染。html! { ... }— 组件返回一棵描述要渲染内容的VirtualNode树。
使用组件
定义完成后,可以在 html! 宏中通过引用函数名来使用组件:
fn app() -> VirtualNode {
html! {
div {
MyCard {
title: "Welcome to euv"
p { "This content is passed as children." }
}
}
}
}
Props 被指定为命名属性(title: "Welcome to euv"),子节点嵌套在组件的花括号内。
组件嵌套
组件可以嵌套在其他组件内部,以构建复杂的 UI:
#[derive(Clone, Default)]
struct AlertProps {
message: &'static str,
}
#[component]
pub fn alert(node: VirtualNode<AlertProps>) -> VirtualNode {
let AlertProps { message, .. } = node.try_get_props().unwrap_or_default();
html! {
div {
p { message }
}
}
}
#[derive(Clone, Default)]
struct DashboardProps {
username: &'static str,
}
#[component]
pub fn dashboard(node: VirtualNode<DashboardProps>) -> VirtualNode {
let DashboardProps { username, .. } = node.try_get_props().unwrap_or_default();
html! {
div {
h1 { username }
Alert {
message: "Welcome to your dashboard!"
}
}
}
}
在这个示例中,dashboard 组件在其模板内部嵌套了 alert 组件。这种组合能力是组件系统的关键优势之一。
向组件传递子节点
子节点是一种创建包装器组件的强大模式。父组件传递内容,子组件决定在哪里渲染它:
#[derive(Clone, Default)]
struct CardProps {
title: &'static str,
}
#[component]
pub fn card(node: VirtualNode<CardProps>) -> VirtualNode {
let CardProps { title, .. } = node.try_get_props().unwrap_or_default();
let children: VirtualNode = node.try_get_child_node();
html! {
div {
h3 { title }
div {
children
}
}
}
}
fn app() -> VirtualNode {
html! {
Card {
title: "My Card"
p { "This is the card body." }
button { onclick: move |event: Event| { /* handler */ } "Action" }
}
}
}
card 组件接收 title prop 和子节点(p 和 button),然后在自己的布局中渲染它们。
Props Down / Callback Up
euv 遵循 "Props Down, Callback Up" 模式:
- Props Down:父组件通过 props 向子组件传递数据
- Callback Up:子组件通过回调函数向父组件传递信息
#[derive(Clone, Default)]
struct ChildProps {
on_click: Option<Rc<dyn Fn(Event)>>,
}
#[component]
pub fn child(node: VirtualNode<ChildProps>) -> VirtualNode {
let ChildProps { on_click, .. } = node.try_get_props().unwrap_or_default();
html! {
button {
onclick: on_click
"Click me"
}
}
}
fn app() -> VirtualNode {
let count: Signal<i32> = use_signal(|| 0);
html! {
div {
Child {
on_click: Some(Rc::new(move |event: Event| {
count.set(count.get() + 1);
}))
}
}
}
}
共享信号实现双向绑定
要在父组件和子组件之间实现真正的双向绑定,可以直接共享一个 Signal:
#[derive(Clone, Default)]
struct InputProps {
value: Signal<String>,
}
#[component]
pub fn text_input(node: VirtualNode<InputProps>) -> VirtualNode {
let InputProps { value, .. } = node.try_get_props().unwrap_or_default();
html! {
input {
oninput: on_input_value(value)
}
}
}
fn app() -> VirtualNode {
let name: Signal<String> = use_signal(|| "".to_string());
html! {
div {
TextInput {
value: name
}
p { "You typed:" }
}
}
}
通过共享 name 信号,父组件和子组件都可以读取和写入同一个状态,实现双向绑定。
使用 watch! 实现跨组件响应式绑定
watch! 宏通过监控信号并在信号变化时运行回调来实现跨组件响应式绑定:
fn app() -> VirtualNode {
let celsius: Signal<f64> = use_signal(|| 0.0);
let fahrenheit: Signal<f64> = use_signal(|| 32.0);
watch!(celsius, |celsius_value: f64| {
fahrenheit.set(celsius_value * 9.0 / 5.0 + 32.0);
});
html! {
div {
input {
oninput: on_input_value(celsius)
}
p { "Fahrenheit" }
}
}
}
在这里,watch! 监控 celsius 信号,并在 celsius 变化时自动更新 fahrenheit。这个模式在保持多个状态片段同步时很有用。
Keep-Alive 模式
有时你想隐藏组件而不卸载它(保留其状态)。euv 通过 CSS display: none/block 的 Keep-Alive 模式来支持这一点:
fn app() -> VirtualNode {
let show: Signal<bool> = use_signal(|| true);
html! {
div {
if { show.get() } {
div { class: euv-fade-in "Content visible" }
} else {
div { style: "display: none" "Content hidden but alive" }
}
button {
onclick: use_toggle(show)
"Toggle"
}
}
}
}
与其使用条件渲染(会卸载并丢失状态),不如使用 CSS 来切换可见性,同时保持组件存活。
总结
组件是 euv 应用架构的基础:
-
Props 结构体:使用
#[derive(Clone, Default)]和类型化字段定义 -
组件函数:用
#[component]装饰,接受VirtualNode<Props>,返回VirtualNode -
提取 props:使用
node.try_get_props().unwrap_or_default() -
提取子节点:使用
node.try_get_child_node() -
组件嵌套:在
html!宏中使用组件,传入命名 props 和嵌套子节点 - Props Down / Callback Up:通过 props 向下传递数据,通过回调向上通信
-
共享信号:共享
Signal实现双向绑定 -
watch!:使用
watch!实现跨组件响应式绑定 -
Keep-Alive:使用 CSS
display: none/block隐藏而不卸载
使用这些模式,你可以在 euv 中构建复杂、可维护且可复用的 UI 应用。
Top comments (0)