6.5 组件组合

> **本节目标**:掌握组件的组合模式——父子通信、兄弟通信、依赖注入、高阶组件。

6.5.1 组合的核心思想

组合优于继承。通过组合小组件构建大组件,通过组合大组件构建页面:

页面
└── 布局组件
    ├── 导航栏组件
    │   ├── Logo 组件
    │   ├── 菜单组件
    │   └── 用户菜单组件
    ├── 侧边栏组件
    │   └── 菜单项组件
    └── 内容区组件
        ├── 搜索框组件
        ├── 列表组件
        │   └── 列表项组件
        └── 分页组件

6.5.2 父子组件通信

父 → 子:通过属性

<!-- 父视图 -->
<div>
    <ChildComponent
        title={my_title}
        data={my_data}
        on_change={handle_change}
    />
</div>
// 子组件
#[derive(IModel)]
#[component]
pub struct ChildComponent {
    pub title: SharedString,
    pub data: Vec<Item>,
    pub on_change: Option<Arc<dyn Fn(&ChangeEvent)>>,
}

子 → 父:通过事件

// 子组件内部
impl ChildComponent {
    #[command]
    pub fn on_internal_change(&mut self, ev: &ChangeEvent, cx: &mut Context<Self>) {
        // 处理内部变化...

        // 触发事件通知父视图
        if let Some(callback) = &self.on_change {
            callback(ev);
        }
    }
}

完整示例

<!-- 父视图 -->
<div class="user-management">
    <h1>用户管理</h1>

    <UserForm
        user={editing_user}
        on_save={handle_save}
        on_cancel={handle_cancel}
    />

    <UserList
        users={users}
        on_edit={handle_edit}
        on_delete={handle_delete}
    />
</div>
#[derive(IModel)]
#[component]
pub struct UserManagement {
    pub users: Vec<User>,
    pub editing_user: Option<User>,
}

impl UserManagement {
    #[command]
    pub fn handle_save(&mut self, user: User, _: &SaveEvent, cx: &mut Context<Self>) {
        if let Some(existing) = self.users.iter_mut().find(|u| u.id == user.id) {
            *existing = user;
        } else {
            self.users.push(user);
        }
        self.editing_user = None;
        cx.notify();
    }

    #[command]
    pub fn handle_cancel(&mut self, _: &ClickEvent, cx: &mut Context<Self>) {
        self.editing_user = None;
        cx.notify();
    }

    #[command]
    pub fn handle_edit(&mut self, user_id: u64, _: &ClickEvent, cx: &mut Context<Self>) {
        self.editing_user = self.users.iter().find(|u| u.id == user_id).cloned();
        cx.notify();
    }

    #[command]
    pub fn handle_delete(&mut self, user_id: u64, _: &ClickEvent, cx: &mut Context<Self>) {
        self.users.retain(|u| u.id != user_id);
        cx.notify();
    }
}

6.5.3 兄弟组件通信

兄弟组件不直接通信,通过共同的父视图协调:

兄弟 A ──事件──▶ 父视图 ──属性──▶ 兄弟 B

示例

<!-- 父视图 -->
<div class="search-panel">
    <SearchBox on_search={handle_search} />
    <SearchResults results={search_results} />
</div>
#[derive(IModel)]
#[component]
pub struct SearchPanel {
    pub search_results: Vec<SearchResult>,
}

impl SearchPanel {
    #[command]
    pub fn handle_search(&mut self, ev: &SearchEvent, cx: &mut Context<Self>) {
        let query = ev.query.clone();
        self.perform_search(&query, cx);
    }

    fn perform_search(&mut self, query: &str, cx: &mut Context<Self>) {
        // 执行搜索,更新 search_results
        cx.notify();
    }
}

数据流

1. `SearchBox` 触发 `on_search` 事件 2. `SearchPanel` 的 `handle_search` 处理事件 3. `SearchPanel` 更新 `search_results` 字段 4. `SearchResults` 接收新的 `results` 属性并重新渲染

6.5.4 跨层级通信:依赖注入

当组件层级很深时,通过属性逐层传递会很繁琐。RML 提供依赖注入机制:

根视图
└── 组件 A
    └── 组件 B
        └── 组件 C  ← 需要访问根视图的数据

提供依赖

use rml::prelude::*;

#[derive(IModel)]
#[component]
pub struct App {
    pub theme: Entity<Theme>,
    pub user_session: Entity<UserSession>,
    pub notification_service: Entity<NotificationService>,
}

impl App {
    pub fn new(cx: &mut AppContext) -> Self {
        Self {
            theme: cx.new_model(|_| Theme::Light),
            user_session: cx.new_model(|_| UserSession::default()),
            notification_service: cx.new_model(|_| NotificationService::new()),
        }
    }

    #[on_loaded]
    pub fn on_loaded(&mut self, cx: &mut Context<Self>) {
        // 提供依赖
        cx.provide(self.theme.clone());
        cx.provide(self.user_session.clone());
        cx.provide(self.notification_service.clone());
    }
}

注入依赖

#[derive(IModel)]
#[component]
pub struct UserAvatar {
    pub user_id: u64,
    pub avatar_url: SharedString,

    // 注入的依赖
    user_session: Option<Entity<UserSession>>,
}

impl UserAvatar {
    pub fn new(user_id: u64) -> Self {
        Self {
            user_id,
            avatar_url: SharedString::default(),
            user_session: None,
        }
    }

    #[on_loaded]
    pub fn on_loaded(&mut self, cx: &mut Context<Self>) {
        // 注入依赖
        self.user_session = cx.use_provider::<Entity<UserSession>>();

        // 加载头像
        if let Some(session) = &self.user_session {
            let avatar_url = session.read(cx).get_avatar_url(self.user_id);
            self.avatar_url = avatar_url;
            cx.notify();
        }
    }
}

依赖注入的优点

| 优点 | 说明 | | ------ | --------------------------- | | 解耦 | 组件不依赖具体的父视图 | | 可测试 | 测试时可注入 mock 依赖 | | 灵活 | 可在不同层级注入不同实现 | | 减少属性传递 | 无需逐层传递 |

6.5.5 高阶组件

高阶组件(HOC)是接收一个组件返回新组件的函数,用于增强组件功能:

use rml::prelude::*;

/// 加载状态增强器
pub fn with_loading<T: Model>(inner: Entity<T>) -> impl Model {
    LoadingWrapper::new(inner)
}

#[derive(IModel)]
#[component]
pub struct LoadingWrapper {
    pub is_loading: bool,
    pub error: Option<SharedString>,
    inner: Option<Entity<dyn Model>>,
}

impl LoadingWrapper {
    pub fn new(inner: Entity<impl Model>) -> Self {
        Self {
            is_loading: false,
            error: None,
            inner: Some(inner.into()),
        }
    }
}
<!-- components/loading_wrapper.rml -->
<div class="loading-wrapper">
    <div if={is_loading} class="loading">加载中...</div>
    <div if={error.is_some()} class="error">{error}</div>
    <div if={!is_loading && error.is_none()}>
        <slot></slot>
    </div>
</div>

使用高阶组件

<LoadingWrapper is_loading={is_data_loading} error={data_error}>
    <DataView data={data} />
</LoadingWrapper>

6.5.6 组件的组合模式

模式一:容器组件 + 展示组件

// 容器组件:负责数据获取和状态管理
#[derive(IModel)]
#[component]
pub struct UserListContainer {
    pub users: Vec<User>,
    pub is_loading: bool,
    pub error: Option<SharedString>,
}

impl UserListContainer {
    #[on_loaded]
    pub fn on_loaded(&mut self, cx: &mut Context<Self>) {
        self.fetch_users(cx);
    }

    fn fetch_users(&mut self, cx: &mut Context<Self>) {
        self.is_loading = true;
        cx.notify();

        cx.spawn(|this, mut cx| async move {
            match fetch_users_from_api().await {
                Ok(users) => {
                    let _ = this.update(&mut cx, |this, cx| {
                        this.users = users;
                        this.is_loading = false;
                        cx.notify();
                    });
                }
                Err(e) => {
                    let _ = this.update(&mut cx, |this, cx| {
                        this.error = Some(e.to_string().into());
                        this.is_loading = false;
                        cx.notify();
                    });
                }
            }
        }).detach();
    }
}
<!-- 容器组件的 .rml -->
<div>
    <div if={is_loading}>加载中...</div>
    <div if={error.is_some()}>错误: {error}</div>
    <UserListPresentation
        if={!is_loading && error.is_none()}
        users={users}
        on_edit={handle_edit}
        on_delete={handle_delete}
    />
</div>
// 展示组件:只负责 UI 展示
#[derive(IModel)]
#[component]
pub struct UserListPresentation {
    pub users: Vec<User>,
    pub on_edit: Option<Arc<dyn Fn(u64, &ClickEvent)>>,
    pub on_delete: Option<Arc<dyn Fn(u64, &ClickEvent)>>,
}

模式二:复合组件

// 主组件
#[derive(IModel)]
#[component]
pub struct Tabs {
    pub active_tab: SharedString,
    pub tabs: Vec<TabItem>,
}

// 子组件
#[derive(IModel)]
#[component]
pub struct TabItem {
    pub id: SharedString,
    pub title: SharedString,
    pub is_active: bool,
}

#[derive(IModel)]
#[component]
pub struct TabPanel {
    pub id: SharedString,
    pub is_active: bool,
}
<!-- 使用复合组件 -->
<Tabs active_tab={active_tab}>
    <TabItem id="profile" title="个人信息" />
    <TabItem id="settings" title="设置" />

    <TabPanel id="profile">
        <ProfileForm user={user} />
    </TabPanel>
    <TabPanel id="settings">
        <SettingsForm settings={settings} />
    </TabPanel>
</Tabs>

模式三:Render Props

通过插槽传递渲染逻辑:

<!-- components/data_loader.rml -->
<div>
    <div if={is_loading}>加载中...</div>
    <div if={error.is_some()}>错误: {error}</div>
    <div if={!is_loading && error.is_none()}>
        <slot let-data={data}></slot>
    </div>
</div>
<!-- 使用 Render Props -->
<DataLoader url="/api/users">
    <template let-data>
        <ul>
            <li each={user in data} key={user.id}>
                {user.name}
            </li>
        </ul>
    </template>
</DataLoader>

6.5.7 组件组合的最佳实践

1. 单向数据流

父 → 子(属性)
子 → 父(事件)

避免双向数据流导致的难以追踪问题。

2. 组件无状态化

// ✅ 无状态展示组件
#[component]
pub struct UserCard {
    pub user: User,
    pub on_click: Option<Arc<dyn Fn(u64)>>,
}

// ❌ 有状态展示组件
#[component]
pub struct UserCard {
    pub user: User,
    pub is_hovered: bool,  // 内部状态,增加复杂度
}

3. 合理拆分粒度

// ✅ 合理拆分
pub struct UserCard {
    pub user: User,
}

pub struct UserList {
    pub users: Vec<User>,
}

// ❌ 过度拆分
pub struct UserName { ... }
pub struct UserEmail { ... }
pub struct UserAvatar { ... }
// 过度拆分增加复杂度,没有实际收益

4. 显式依赖

// ✅ 显式声明依赖
#[component]
pub struct UserAvatar {
    pub user_id: u64,
    pub avatar_url: SharedString,
}

// ❌ 隐式依赖(通过全局状态)
fn get_user_avatar(user_id: u64) -> SharedString {
    // 从全局状态获取,难以测试和追踪
    GLOBAL_STATE.get_avatar(user_id)
}

6.5.8 完整示例:用户管理页面

<!-- views/user_management.rml -->
<div class="user-management">
    <header class="page-header">
        <h1>用户管理</h1>
        <Button variant="primary" on-click={show_add_form}>添加用户</Button>
    </header>

    <SearchBox
        placeholder="搜索用户..."
        on_search={handle_search}
        on_clear={handle_clear_search}
    />

    <div class="content-area">
        <UserList
            if={!is_form_visible}
            users={filtered_users}
            on_edit={handle_edit}
            on_delete={handle_delete}
        />

        <UserForm
            if={is_form_visible}
            user={editing_user}
            on_save={handle_save}
            on_cancel={handle_cancel}
        />
    </div>

    <Dialog
        if={is_delete_dialog_open}
        title="确认删除"
        on_close={handle_delete_dialog_close}
    >
        <template>
            <p>确定要删除用户 {deleting_user_name} 吗?</p>
        </template>
        <template slot="footer">
            <Button on-click={handle_delete_cancel}>取消</Button>
            <Button variant="danger" on-click={handle_delete_confirm}>确认删除</Button>
        </template>
    </Dialog>
</div>
// views/user_management.rml.rs
use rml::prelude::*;
use crate::components::{search_box::SearchBox, dialog::Dialog, button::Button};
use crate::views::user_management::{user_list::UserList, user_form::UserForm};

#[derive(IModel)]
#[component]
pub struct UserManagement {
    pub users: Vec<User>,
    pub search_query: SharedString,
    pub filtered_users: Vec<User>,
    pub is_form_visible: bool,
    pub editing_user: Option<User>,
    pub is_delete_dialog_open: bool,
    pub deleting_user_id: Option<u64>,
    pub deleting_user_name: SharedString,
}

impl UserManagement {
    pub fn new() -> Self {
        Self {
            users: Vec::new(),
            search_query: SharedString::default(),
            filtered_users: Vec::new(),
            is_form_visible: false,
            editing_user: None,
            is_delete_dialog_open: false,
            deleting_user_id: None,
            deleting_user_name: SharedString::default(),
        }
    }

    #[on_loaded]
    pub fn on_loaded(&mut self, cx: &mut Context<Self>) {
        self.fetch_users(cx);
    }

    fn fetch_users(&mut self, cx: &mut Context<Self>) {
        cx.spawn(|this, mut cx| async move {
            let users = fetch_users_from_api().await.unwrap_or_default();
            let _ = this.update(&mut cx, |this, cx| {
                this.users = users.clone();
                this.filtered_users = users;
                cx.notify();
            });
        }).detach();
    }

    #[command]
    pub fn show_add_form(&mut self, _: &ClickEvent, cx: &mut Context<Self>) {
        self.editing_user = None;
        self.is_form_visible = true;
        cx.notify();
    }

    #[command]
    pub fn handle_edit(&mut self, user_id: u64, _: &ClickEvent, cx: &mut Context<Self>) {
        self.editing_user = self.users.iter().find(|u| u.id == user_id).cloned();
        self.is_form_visible = true;
        cx.notify();
    }

    #[command]
    pub fn handle_save(&mut self, user: User, _: &SaveEvent, cx: &mut Context<Self>) {
        if let Some(existing) = self.users.iter_mut().find(|u| u.id == user.id) {
            *existing = user;
        } else {
            self.users.push(user);
        }
        self.is_form_visible = false;
        self.editing_user = None;
        self.apply_filter();
        cx.notify();
    }

    #[command]
    pub fn handle_cancel(&mut self, _: &ClickEvent, cx: &mut Context<Self>) {
        self.is_form_visible = false;
        self.editing_user = None;
        cx.notify();
    }

    #[command]
    pub fn handle_delete(&mut self, user_id: u64, _: &ClickEvent, cx: &mut Context<Self>) {
        if let Some(user) = self.users.iter().find(|u| u.id == user_id) {
            self.deleting_user_id = Some(user_id);
            self.deleting_user_name = user.name.clone();
            self.is_delete_dialog_open = true;
            cx.notify();
        }
    }

    #[command]
    pub fn handle_delete_confirm(&mut self, _: &ClickEvent, cx: &mut Context<Self>) {
        if let Some(id) = self.deleting_user_id.take() {
            self.users.retain(|u| u.id != id);
            self.apply_filter();
        }
        self.is_delete_dialog_open = false;
        cx.notify();
    }

    #[command]
    pub fn handle_delete_cancel(&mut self, _: &ClickEvent, cx: &mut Context<Self>) {
        self.is_delete_dialog_open = false;
        self.deleting_user_id = None;
        cx.notify();
    }

    #[command]
    pub fn handle_delete_dialog_close(&mut self, _: &DialogCloseEvent, cx: &mut Context<Self>) {
        self.is_delete_dialog_open = false;
        self.deleting_user_id = None;
        cx.notify();
    }

    #[command]
    pub fn handle_search(&mut self, ev: &SearchEvent, cx: &mut Context<Self>) {
        self.search_query = ev.query.clone();
        self.apply_filter();
        cx.notify();
    }

    #[command]
    pub fn handle_clear_search(&mut self, cx: &mut Context<Self>) {
        self.search_query = SharedString::default();
        self.filtered_users = self.users.clone();
        cx.notify();
    }

    fn apply_filter(&mut self) {
        let query = self.search_query.to_lowercase();
        if query.is_empty() {
            self.filtered_users = self.users.clone();
        } else {
            self.filtered_users = self
                .users
                .iter()
                .filter(|u| u.name.to_lowercase().contains(&query))
                .cloned()
                .collect();
        }
    }
}

6.5.9 小结

组件组合是构建大型应用的核心:

- **父子通信**:父 → 子(属性),子 → 父(事件) - **兄弟通信**:通过共同父视图协调 - **依赖注入**:跨层级通信,避免属性逐层传递 - **高阶组件**:增强组件功能 - **组合模式**:容器+展示、复合组件、Render Props

掌握组件组合,你就能构建出结构清晰、易于维护的大型应用。

下一章 → [第 7 章 · 样式与主题](../07-styling/INDEX.md)