第十一章:组件通信进阶——Context、回调与跨层级数据流
博客v1.0系列教程(Dioxus)博客 v1.0 系列教程 (Dioxus)
第十一章:组件通信进阶——Context、回调与跨层级数据流
1. 组件通信的几种模式
在 Dioxus 中,组件之间的通信主要有以下几种方式:
| 模式 | 适用场景 | 数据流向 | |------|---------|---------| | Props 传递 | 父→子 直接传值 | 单向向下 | | 回调函数 | 子→父 通知事件 | 单向向上 | | Context | 跨层级共享 | 任意层级 | | Signal 提升 | 兄弟组件共享 | 提升到共同父级 |
2. Props 深度用法
2.1 可选 Props 与默认值
#[component]
fn Card(
title: String,
subtitle: Option<String>, // 可选
variant: Option<String>, // 可选
children: Element, // 子元素插槽
) -> Element {
let sub = subtitle.unwrap_or_default();
let variant_cls = match variant.as_deref() {
Some("primary") => "border-primary",
Some("danger") => "border-red-500",
_ => "border-gray-200",
};
rsx! {
div { class: "rounded-lg border p-4 {variant_cls}",
h3 { class: "font-bold", "{title}" }
if !sub.is_empty() {
p { class: "text-sm text-gray-500", "{sub}" }
}
{children}
}
}
}
// 使用
Card {
title: "Hello".to_string(),
variant: Some("primary".to_string()),
p { "这是卡片内容" }
}
2.2 组件 Props 与泛型
当需要传递不同类型的数据时,可以使用泛型 Props:
#[derive(Props, Clone, PartialEq)]
struct ListProps<T: 'static + Clone + PartialEq> {
items: Vec<T>,
render_item: Box<dyn Fn(&T) -> Element>,
}
// 但 Dioxus 推荐使用更简单的方式 —— 用回调
#[component]
fn ItemList<T>(
items: Vec<T>,
render_item: EventHandler<(T,)>,
) -> Element where T: 'static + Clone + PartialEq {
// 实践中直接用 EventHandler 更简洁
}
3. 回调函数与子传父
3.1 基础回调
#[component]
fn SearchInput(on_search: EventHandler<String>) -> Element {
let mut value = use_signal(String::new);
rsx! {
input {
value: "{value}",
oninput: move |e| {
let v = e.value();
value.set(v.clone());
on_search.call(v);
}
}
}
}
// 父组件
#[component]
fn Parent() -> Element {
let results = use_signal(|| Vec::<String>::new());
rsx! {
SearchInput {
on_search: move |query| {
// 当子组件搜索时,父组件更新状态
let data = perform_search(&query);
results.set(data);
}
}
}
}
3.2 多参数回调
// 使用元组传递多个值
#[component]
fn Pagination(on_page_change: EventHandler<(usize, usize)>) -> Element {
// ...
on_page_change.call((current_page, total_pages));
}
4. Context 跨层级共享
这是最强大的通信方式,适合主题、用户信息、配置等需要跨多层组件的数据。
4.1 提供 Context
#[component]
fn App() -> Element {
// 创建一个可共享的信号
let user = use_signal(|| Option::<User>::None);
let theme = use_signal(|| "light".to_string());
// 通过 Context Provider 注入到整个子树
use_context_provider(|| user);
use_context_provider(|| theme);
rsx! {
Router::<Route> {}
}
}
4.2 消费 Context
任意深度的子组件都可以直接读取:
#[component]
fn NavBar() -> Element {
// 从 Context 读取用户信息和主题
let user = use_context::<Signal<Option<User>>>();
let theme = use_context::<Signal<String>>();
rsx! {
header {
style: "background: if theme() == \"dark\" { \"#1a1a2e\" } else { \"#fff\" }",
if let Some(u) = &*user.read() {
span { "欢迎, {u.username}" }
button { onclick: move |_| user.set(None), "退出" }
} else {
a { href: "/login", "登录" }
}
}
}
}
4.3 更新 Context
// 登录页面中更新用户 Context
#[component]
fn LoginPage() -> Element {
let mut user = use_context::<Signal<Option<User>>>();
let do_login = move |username, password| {
// ... 登录逻辑
user.set(Some(User {
id: 1,
username: username.clone(),
avatar: None,
}));
};
// ...
}
5. Signal 提升(Lifting State Up)
当两个兄弟组件需要共享数据时,将状态提升到它们的共同父级:
#[component]
fn BlogPage() -> Element {
// 状态提升到这里
let selected_category = use_signal(|| "all".to_string());
let search_query = use_signal(String::new);
rsx! {
div { class: "flex gap-4",
// 左侧筛选栏 —— 可以修改 selected_category
SidebarFilter {
selected: selected_category,
on_select: move |cat| selected_category.set(cat),
}
// 右侧文章列表 —— 读取 selected_category 和 search_query
ArticleList {
category: selected_category(),
query: search_query(),
}
}
}
}
6. 实战:全局通知系统
使用 Context 实现一个全局的消息通知组件:
// 通知类型
#[derive(Clone)]
struct Notification {
id: u64,
message: String,
level: String, // "info", "success", "error"
}
// 通知管理器
#[component]
fn NotificationProvider(children: Element) -> Element {
let notifications = use_signal(|| Vec::<Notification>::new());
use_context_provider(|| notifications);
rsx! {
div { class: "relative",
// 通知浮层
div { class: "fixed top-4 right-4 z-50 flex flex-col gap-2",
for notif in notifications.iter() {
div {
class: "px-4 py-2 rounded-lg shadow-lg text-sm text-white transition-all",
style: "background: match notif.level.as_str() {
\"error\" => \"#ef4444\",
\"success\" => \"#22c55e\",
_ => \"#3b82f6\",
}",
"{notif.message}"
}
}
}
{children}
}
}
}
// 任意组件中发送通知
fn use_notify() -> impl Fn(String, String) {
let notifications = use_context::<Signal<Vec<Notification>>>();
move |msg: String, level: String| {
let id = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_millis() as u64;
notifications.write().push(Notification { id, message: msg, level });
}
}
7. 选择通信方式的决策树
需要传递数据?
├── 父→子 直接传递 → Props
├── 子→父 通知事件 → EventHandler 回调
├── 兄弟组件共享 → Signal 提升到共同父级
├── 跨多层组件共享 → Context
└── 全局一次性数据(路由参数等)→ 路由状态
8. 小结
- Props 是最基础最安全的通信方式,类型安全、显式
EventHandler实现了子传父的事件通知模式use_context/use_context_provider适合跨层级共享状态- Signal 提升是兄弟组件共享状态的标准做法
- 合理选择通信方式可以保持组件树的简洁和可维护性
dioxuscomponentscontextcallbackdata-flow