第九章:异步编程与数据获取
博客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