DEV Community

tengxgfyrz67s
tengxgfyrz67s

Posted on

构建组件

项目代码:https://github.com/euv-dev/euv

组件简介

组件是任何 euv 应用的构建块。它们让你将 UI 拆分为独立的、可复用的片段,每个片段都有自己的逻辑和展示。在 euv 中,组件是接受一个 VirtualNode(携带 props 和子节点)并返回一个 VirtualNode(描述渲染输出)的函数

定义组件

要在 euv 中定义组件,你需要两个东西:

  1. 一个 Props 结构体,定义组件接受的数据
  2. 一个用 #[component] 属性装饰的组件函数

Props 结构体

Props 结构体定义了组件接收的数据的形状。它必须派生 CloneDefault

#[derive(Clone, Default)]
struct MyCardProps {
    title: &'static str,
}
Enter fullscreen mode Exit fullscreen mode

Props 可以是各种类型,包括:

  • &'static str — 静态字符串切片,适合固定文本
  • String — 拥有的字符串,适合动态文本
  • bool — 布尔值,用于标志位
  • Signal<bool> — 响应式布尔信号
  • i32f64 — 数值类型
  • 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
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

让我们逐步分析:

  1. #[component] — 这个属性将函数标记为组件,使其能够通过 VirtualNode 包装器接收 props 和子节点。

  2. node: VirtualNode<MyCardProps> — 函数接收一个用 props 类型参数化的 VirtualNode。这个节点携带 props 和任何子节点。

  3. node.try_get_props() — 该方法尝试从虚拟节点中提取类型化的 props。unwrap_or_default() 调用在提取失败时提供默认值(例如,当组件在没有 props 的情况下使用时)。

  4. node.try_get_child_node() — 该方法提取传递给组件的子节点。这些可以在组件的模板内部渲染。

  5. html! { ... } — 组件返回一棵描述要渲染内容的 VirtualNode 树。

使用组件

定义完成后,可以在 html! 宏中通过引用函数名来使用组件:

fn app() -> VirtualNode {
    html! {
        div {
            MyCard {
                title: "Welcome to euv"
                p { "This content is passed as children." }
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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!"
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

在这个示例中,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" }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

card 组件接收 title prop 和子节点(pbutton),然后在自己的布局中渲染它们。

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);
                }))
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

共享信号实现双向绑定

要在父组件和子组件之间实现真正的双向绑定,可以直接共享一个 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:" }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

通过共享 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" }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

在这里,watch! 监控 celsius 信号,并在 celsius 变化时自动更新 fahrenheit。这个模式在保持多个状态片段同步时很有用。

Keep-Alive 模式

有时你想隐藏组件而不卸载它(保留其状态)。euv 通过 CSS display: none/blockKeep-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"
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

与其使用条件渲染(会卸载并丢失状态),不如使用 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 应用。


项目代码:https://github.com/euv-dev/euv

Top comments (0)