08 Dioxus 入门:组件、props、Signal、路由与数据流

本章目标

  • 你能读懂 Dioxus 的组件写法(#[component] + rsx!)。
  • 你能理解本项目的路由结构与布局组件。
  • 你能掌握 Signal 的基本用法:全局状态(Context)与页面局部状态(use_signal)。
  • 你能在 asset-light 里新增一个简单页面/组件并接入导航。

1. Dioxus 的核心概念(用 React 类比)

如果你熟悉 React,可以这样对照:

  • 组件函数:React function component ⇔ #[component] fn Foo() -> Element
  • JSX:JSX ⇔ rsx! { ... }
  • Props:props ⇔ #[derive(Props)] 的 struct 或组件参数
  • State:useState ⇔ use_signal
  • Effect:useEffect ⇔ use_effect
  • Router:react-router ⇔ dioxus_routerRoutable + Router

2. 项目路由与布局

2.1 根组件

文件:src/app.rs

关键点:

  • provide_app_state() 注入全局状态(Context)
  • Router::<Route> {} 渲染路由

2.2 路由枚举

文件:src/router.rs

你会看到:

  • #[derive(Routable)] pub enum Route { ... }
  • #[layout(Layout)]:用布局组件包裹多个页面

这相当于:所有页面都嵌在同一个 Layout(含 Sidebar)中。

2.3 Sidebar 导航

文件:src/components/layout/sidebar.rs

关键点:

  • Link { to: Route::HomePage {}, ... } 实现导航
  • use_route::<Route>() 获取当前路由
  • std::mem::discriminant 做“当前页面高亮”(按 enum variant 判断)

3. 状态管理:全局 Signal 与局部 Signal

3.1 全局状态 AppState

文件:src/state/app_state.rs

  • provide_app_state():提供 Signal<AppState>
  • use_app_state():在任意组件中获取它

你会在页面里看到典型写法:

  • let mut state = use_app_state();
  • let assets = state.read().assets.clone();
  • state.write().assets = new_assets;

关键学习点(和所有权/借用相关):

  • .read() / .write() 持有内部借用句柄,尽量缩短作用域
  • UI 渲染往往会 clone() 数据用于展示(学习期可以接受)

3.2 页面局部状态:use_signal

典型场景:模态框开关、编辑态、选择态。

例如 src/pages/assets.rs 里:

  • show_form: Signal<bool> 控制表单 modal
  • editing_asset: Signal<Option<Asset>> 当前编辑的资产
  • inventory_mode: Signal<bool> 盘点模式开关

你可以把它类比为 React 的:

  • const [showForm, setShowForm] = useState(false)

4. 副作用:加载数据与刷新

4.1 use_effect:在渲染之外做 IO

例子:src/pages/history.rs / src/pages/analysis.rs 使用 use_effect 在初始渲染时加载 DB 数据。

这类逻辑建议尽量放在 use_effect 里,避免“在 render 路径里写 state”导致的重复执行或难排查问题。

4.2 本项目里的一种“loaded flag”模式

你会在 src/pages/home.rs 看到:

  • loaded: Signal<bool> 保护“只执行一次”的加载逻辑

这在学习期能跑通,但从工程最佳实践看,你可以把它作为练习:

把 HomePage 的初始加载重构为 use_effect,并理解它与 loaded-flag 的差别。


5. 事件处理与回调(EventHandler)

在 Dioxus 里,事件回调通常是 closure:

  • on_click: move |_| { ... }

而组件间传递回调,常用 EventHandler<T>(你会在多处组件 props 中见到)。

这和 React 传 onSave={(x) => ...} 很像,但 Rust 的 closure 需要更明确的所有权移动(move)。


6. 本章练习(强烈推荐做)

练习 A:新增一个“设置页”占位并接入导航

目标:走一遍“新增页面 → 路由 → Sidebar”的完整流程。

建议步骤:

  1. 新建 src/pages/settings.rs,写一个最小页面组件(只显示标题即可)
  2. src/pages/mod.rs 里导出它
  3. src/router.rsRoute enum 增加一个 SettingsPage {} 路由
  4. src/components/layout/sidebar.rs 增加一个 NavItem 链接

练习 B:在页面顶部渲染一个全局错误提示条

目标:把上一章的“错误处理”落到 UI 体验中。

思路:

  • AppStateerror: Option<String>
  • 在 Layout 或每个页面顶部,如果有 error 就渲染一个红色 banner,并提供关闭按钮(清空 error)

练习 C(进阶):把 HomePage 的加载逻辑改成 use_effect

目标:理解“副作用与渲染”的边界,避免重复加载与不可控状态写入。