第九章:异步编程与数据获取

博客v1.0系列教程(Dioxus)博客 v1.0 系列教程 (Dioxus)

第九章:异步编程与数据获取

1. 为什么需要异步

前端开发中大量操作是异步的:

API 请求     → 等待服务器响应
文件读取     → 等待磁盘
定时器       → 等待时间到达
用户输入     → 等待事件触发

Dioxus 在 WASM 环境中运行,支持完整的 async/await

2. use_resource —— 声明式数据获取

2.1 基本用法

use_resource 是 Dioxus 中最常用的数据获取 Hook,它会自动在组件挂载时执行异步闭包:

#[component]
fn ArticleList() -> Element {
    // use_resource 接收 async 闭包,返回 Resource
    let articles = use_resource(move || async move {
        fetch_articles().await
    });

    // Resource() 返回 Option<T>
    // None    = 仍在加载中
    // Some(T) = 加载完成
    match articles() {
        Some(list) => rsx! {
            for article in list {
                div { "{article.title}" }
            }
        },
        None => rsx! {
            div { class: "text-center py-8", "加载中..." }
        },
    }
}

// 模拟的异步函数
async fn fetch_articles() -> Vec<ArticleSummary> {
    let resp = reqwest::get("/api/articles")
        .await
        .unwrap();
    resp.json().await.unwrap()
}

2.2 带错误处理

#[component]
fn SafeArticleList() -> Element {
    let articles = use_resource(move || async move {
        // 使用 Result 处理错误
        fetch_articles().await
    });

    match articles() {
        // 成功
        Some(Ok(list)) => rsx! {
            for a in list {
                ArticleCard { summary: a }
            }
        },
        // 失败
        Some(Err(e)) => rsx! {
            div { class: "text-center py-8 text-red-500",
                p { "加载失败: {e}" }
                button {
                    onclick: move |_| articles.restart(),  // 重试
                    "重新加载"
                }
            }
        },
        // 加载中
        None => rsx! {
            div { class: "text-center py-8", "加载中..." }
        },
    }
}

2.3 依赖追踪

当闭包中读取了 Signal,Signal 变化时会自动重新执行:

#[component]
fn FilteredList() -> Element {
    let category = use_signal(|| "all".to_string());

    let articles = use_resource(move || async move {
        // 读取 Signal —— 建立依赖追踪
        let cat = category();
        // 当 category 变化时,此闭包自动重新执行
        fetch_filtered(&cat).await
    });

    rsx! {
        select {
            value: "{category}",
            oninput: move |e| category.set(e.value()),
            option { value: "all", "全部" }
            option { value: "rust", "Rust" }
            option { value: "dioxus", "Dioxus" }
        }
        // category 变化时,articles 自动重新加载
        render_list(articles())
    }
}

2.4 手动刷新

let articles = use_resource(move || async move {
    fetch_articles().await
});

rsx! {
    button {
        onclick: move |_| articles.restart(),  // 手动重新加载
        "刷新"
    }
}

3. spawn —— 命令式异步任务

3.1 在事件中使用

#[component]
fn ActionButton() -> Element {
    let mut status = use_signal(|| "idle".to_string());

    rsx! {
        button {
            onclick: move |_| {
                status.set("processing".to_string());
                // spawn 启动一个独立异步任务
                spawn(async move {
                    tokio::time::sleep(
                        std::time::Duration::from_secs(2)
                    ).await;
                    status.set("done".to_string());
                });
            },
            "{status}"
        }
    }
}

3.2 在 use_effect 中使用

#[component]
fn PageViewTracker() -> Element {
    use_effect(move || {
        // 组件挂载时触发
        spawn(async move {
            track_page_view().await;
        });
    });

    rsx! { div {} }
}

4. 加载状态管理

4.1 统一定义

#[derive(Clone, PartialEq)]
enum AsyncState<T> {
    Idle,
    Loading,
    Success(T),
    Error(String),
}

impl<T> AsyncState<T> {
    fn is_loading(&self) -> bool {
        matches!(self, AsyncState::Loading)
    }

    fn data(self) -> Option<T> {
        match self {
            AsyncState::Success(data) => Some(data),
            _ => None,
        }
    }
}

4.2 加载指示器

#[component]
fn LoadingSpinner() -> Element {
    rsx! {
        div { class: "flex items-center justify-center py-8",
            div {
                class: "w-8 h-8 border-4 border-blue-500 border-t-transparent rounded-full",
                style: "animation: spin 0.8s linear infinite;",
            }
        }
    }
}

// 在 CSS 中定义
// @keyframes spin { to { transform: rotate(360deg); } }

4.3 骨架屏

#[component]
fn SkeletonCard() -> Element {
    rsx! {
        div { class: "border rounded-lg p-4 space-y-3",
            style: "background: var(--card); border-color: var(--border);",
            div { class: "h-4 bg-gray-200 rounded animate-pulse w-3/4" }
            div { class: "h-4 bg-gray-200 rounded animate-pulse" }
            div { class: "h-4 bg-gray-200 rounded animate-pulse w-5/6" }
            div { class: "h-20 bg-gray-200 rounded animate-pulse" }
        }
    }
}

5. 竞态条件处理

#[component]
fn SearchBox() -> Element {
    let query = use_signal(String::new);

    // use_resource 自动处理竞态
    // 当 query 快速变化时,前一个请求自动"失效"
    let results = use_resource(move || async move {
        let q = query();
        if q.len() < 2 { return Vec::new(); }

        // 模拟网络延迟
        tokio::time::sleep(
            std::time::Duration::from_millis(300)
        ).await;

        search_api(&q).await.unwrap_or_default()
    });

    rsx! {
        input {
            value: "{query}",
            oninput: move |e| query.set(e.value()),
            placeholder: "搜索...",
        }
        render_results(results())
    }
}

6. 并行请求

#[component]
function Dashboard() -> Element {
    let data = use_resource(move || async move {
        // 并行发送多个请求
        let (articles, stats, comments) = tokio::join!(
            fetch_articles(),
            fetch_stats(),
            fetch_recent_comments(),
        );

        DashboardData {
            articles: articles.ok(),
            stats: stats.ok(),
            recent_comments: comments.ok(),
        }
    });

    // ...
}

7. 实战:文章详情页

#[component]
function ArticlePage(slug: String) -> Element {
    let article = use_resource(move || async move {
        fetch_article(&slug).await
    });

    match article() {
        Some(Ok(detail)) => rsx! {
            article { class: "prose-content",
                h1 { "{detail.title}" }
                div { class: "text-sm mb-4",
                    style: "color: var(--tertiary);",
                    span { "发布于 {detail.created_at}" }
                    span { " | 阅读 {detail.reading_time} 分钟" }
                }
                div { dangerous_inner_html: "{detail.html}" }
            }
        },
        Some(Err(msg)) => rsx! {
            div { class: "text-center py-16",
                h2 { class: "text-xl mb-2", "加载失败" }
                p { style: "color: var(--tertiary);", "{msg}" }
                button {
                    class: "mt-4 px-4 py-2 rounded-lg text-white",
                    style: "background: var(--primary);",
                    onclick: move |_| article.restart(),
                    "重试"
                }
            }
        },
        None => rsx! {
            div { class: "space-y-4 max-w-2xl mx-auto py-8",
                SkeletonCard {}
                SkeletonCard {}
            }
        },
    }
}

8. 小结

  • use_resource 是声明式数据获取的首选,自动依赖追踪和重执行
  • spawn 适合命令式异步任务,如表单提交后的副作用
  • 使用 Result 类型统一处理成功/失败
  • 骨架屏(Skeleton)提升加载时的视觉体验
  • restart() 方法手动触发刷新
  • tokio::join! 并行发请求减少等待
  • 下一章将学习暗夜模式与主题系统
dioxusasyncuse_resourcespawnloadingfetch