第十五章:异步编程与数据获取
博客v1.0系列教程(Dioxus)博客 v1.0 系列教程 (Dioxus)
第十五章:异步编程与数据获取
1. Dioxus 异步基础
Dioxus 应用运行在浏览器(WASM)或服务端(Tokio)上,两种环境都支持 async/await。异步编程在前端开发中无处不在:API 请求、定时器、文件读取、WebSocket 通信。
1.1 三种异步模式
// 模式一:use_resource —— 声明式数据获取(推荐)
#[component]
fn ArticlePage(slug: String) -> Element {
let article = use_resource(move || async move {
reqwest::get(&format!("/api/articles/{slug}"))
.await.ok()?
.json::<ArticleDto>().await.ok()
});
match article() {
Some(data) => render_article(&data),
None => rsx! { "加载中..." },
}
}
// 模式二:spawn —— 命令式异步任务
#[component]
fn ActionButton() -> Element {
let mut loading = use_signal(|| false);
rsx! {
button {
disabled: loading(),
onclick: move |_| {
loading.set(true);
spawn(async move {
do_something().await;
loading.set(false);
});
},
"执行"
}
}
}
// 模式三:use_effect —— 副作用
#[component]
fn Logger() -> Element {
use_effect(move || {
// 在组件挂载或依赖变化时执行
spawn(async move {
log_visit().await;
});
});
// ...
}
2. use_resource 深入
2.1 基本用法
#[component]
fn ArticleList() -> Element {
// use_resource 接收一个 async 闭包
// 闭包中读取的 Signal 被追踪,变化时自动重新执行
let articles = use_resource(move || async move {
fetch_articles().await
});
match articles() {
Some(Ok(list)) => rsx! {
for article in list {
ArticleCard { summary: article }
}
},
Some(Err(e)) => rsx! {
div { class: "text-red-500", "加载失败: {e}" }
},
None => rsx! {
div { class: "text-center py-8", "加载中..." }
},
}
}
2.2 依赖追踪
当闭包中读取了 Signal,Signal 变化时会自动重新执行:
#[component]
fn FilteredList(category: Signal<String>, search: Signal<String>) -> Element {
let items = use_resource(move || async move {
// 读取 Signal 值——这些依赖被追踪
let cat = category();
let q = search();
let url = format!("/api/articles?category={cat}&search={q}");
reqwest::get(&url).await.ok()?.json().await.ok()
});
// 当 category 或 search 变化时,items 自动重新加载
match items() {
Some(data) => render_list(data),
None => rsx! { "加载中..." },
}
}
2.3 手动刷新
let mut articles = use_resource(move || async move {
fetch_articles().await
});
// 调用 restart() 手动触发重新加载
rsx! {
button {
onclick: move |_| articles.restart(),
"刷新"
}
}
2.4 初始值
let articles = use_resource_with_initial(
move || async move { fetch_articles().await },
vec![article1, article2], // 初始值,立即显示
);
3. spawn 命令式并发
3.1 基础 spawn
use dioxus::prelude::*;
#[component]
fn AsyncAction() -> 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 竞态条件处理
多次快速点击按钮可能导致多个异步任务同时运行,后完成的任务覆盖先完成的结果:
#[component]
fn SearchBox() -> Element {
let mut query = use_signal(String::new);
let mut results = use_signal(|| Vec::<SearchResult>::new());
// 使用 use_resource 替代手动 spawn,自动取消旧请求
let search_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;
fetch_search(&q).await.unwrap_or_default()
});
// 或者使用 spawn 手动处理竞态
let mut version = use_signal(|| 0u64);
let do_search = move |q: String| {
version += 1;
let my_version = version();
spawn(async move {
let result = fetch_search(&q).await;
// 只在该版本仍是最新时更新
if version() == my_version {
results.set(result.unwrap_or_default());
}
});
};
rsx! {
input {
value: "{query}",
oninput: move |e| {
query.set(e.value());
do_search(e.value());
},
}
}
}
3.3 超时控制
fn fetch_with_timeout(url: &str, timeout_secs: u64) -> Result<Data, String> {
spawn(async move {
let result = tokio::time::timeout(
std::time::Duration::from_secs(timeout_secs),
reqwest::get(url),
).await;
match result {
Ok(Ok(resp)) => Ok(resp.json().await.unwrap()),
Ok(Err(e)) => Err(format!("请求失败: {e}")),
Err(_) => Err("请求超时".to_string()),
}
});
}
4. 错误处理模式
4.1 Result 类型封装
// 统一的 API 返回类型
#[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 use_api<T, F>(fetch: F) -> Signal<AsyncState<T>>
where
F: Fn() -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<T, String>>>> + 'static,
T: 'static + Clone,
{
let state = use_signal(|| AsyncState::<T>::Idle);
use_effect(move || {
state.set(AsyncState::Loading);
spawn(async move {
match fetch().await {
Ok(data) => state.set(AsyncState::Success(data)),
Err(e) => state.set(AsyncState::Error(e)),
}
});
});
state
}
4.2 错误展示组件
#[component]
fn ErrorBoundary(
state: AsyncState<impl Into<Element>>,
on_retry: Option<EventHandler>,
) -> Element {
match state {
AsyncState::Idle => rsx! { div {} },
AsyncState::Loading => rsx! {
div { class: "flex items-center justify-center py-8",
div { class: "animate-spin w-8 h-8 border-4 rounded-full",
style: "border-color: var(--primary); border-top-color: transparent;",
}
}
},
AsyncState::Success(data) => rsx! { {data.into()} },
AsyncState::Error(msg) => rsx! {
div { class: "text-center py-8",
div { class: "text-red-500 mb-2", "😥 {msg}" }
if let Some(retry) = on_retry {
button {
onclick: move |_| retry.call(()),
"重试"
}
}
}
},
}
}
5. 数据缓存策略
5.1 简单内存缓存
use std::collections::HashMap;
static CACHE: std::sync::Mutex<Option<HashMap<String, CachedData>>> =
std::sync::Mutex::new(None);
struct CachedData {
data: Vec<ArticleSummary>,
fetched_at: std::time::Instant,
}
fn get_cached(key: &str, ttl_secs: u64) -> Option<Vec<ArticleSummary>> {
let cache = CACHE.lock().ok()?;
let entry = cache.as_ref()?.get(key)?;
if entry.fetched_at.elapsed().as_secs() < ttl_secs {
Some(entry.data.clone())
} else {
None
}
}
fn set_cache(key: String, data: Vec<ArticleSummary>) {
if let Ok(mut cache) = CACHE.lock() {
cache.get_or_insert_with(HashMap::new).insert(key, CachedData {
data,
fetched_at: std::time::Instant::now(),
});
}
}
5.2 SWR 模式(Stale-While-Revalidate)
先显示缓存数据,再在后台刷新:
#[component]
fn ArticleList() -> Element {
let articles = use_signal(|| Option::<Vec<ArticleSummary>>::None);
// 首次加载
use_effect(move || {
spawn(async move {
// 先尝试从缓存读取
if let Some(cached) = get_cached("articles", 60) {
articles.set(Some(cached));
}
// 再发起网络请求更新
let fresh = fetch_articles().await;
articles.set(Some(fresh.clone()));
set_cache("articles".into(), fresh);
});
});
match articles() {
Some(list) => rsx! {
for a in list { ArticleCard { summary: a } }
},
None => rsx! { "加载中..." },
}
}
6. 实战:完整的文章加载组件
#[derive(Clone, PartialEq)]
struct ArticleDetail {
title: String,
html: String,
toc: Vec<TocEntry>,
prev: Option<ArticleLink>,
next: Option<ArticleLink>,
}
#[component]
fn ArticleLoader(slug: String) -> Element {
let article = use_resource(move || async move {
// 从 API 获取文章数据
let resp = reqwest::get(&format!("/api/article/{slug}"))
.await.ok()?;
if !resp.status().is_success() {
return Err("文章不存在".to_string());
}
let data = resp.json::<ArticleDetail>().await.map_err(|e| e.to_string())?;
Ok(data)
});
match article() {
Some(Ok(detail)) => rsx! {
article { class: "prose-content",
h1 { "{detail.title}" }
div { dangerous_inner_html: "{detail.html}" }
}
// 上下篇导航
div { class: "flex justify-between mt-8",
if let Some(ref p) = detail.prev {
a { href: "/article/{p.slug}", "← {p.title}" }
}
if let Some(ref n) = detail.next {
a { href: "/article/{n.slug}", "{n.title} →" }
}
}
},
Some(Err(msg)) => rsx! {
div { class: "text-center py-16",
h2 { "加载失败" }
p { "{msg}" }
}
},
None => rsx! {
div { class: "flex justify-center py-16",
// 骨架屏
div { class: "w-full max-w-2xl space-y-4",
div { class: "h-8 bg-gray-200 rounded animate-pulse w-3/4" }
div { class: "h-4 bg-gray-200 rounded animate-pulse w-1/4" }
div { class: "h-64 bg-gray-200 rounded animate-pulse" }
}
}
},
}
}
7. 并发请求
7.1 并行加载
#[component]
fn Dashboard() -> Element {
let data = use_resource(move || async move {
// 并行发起多个请求
let (articles, stats, recent) = tokio::join!(
fetch_articles(),
fetch_stats(),
fetch_recent_comments(),
);
DashboardData {
articles: articles.ok(),
stats: stats.ok(),
recent_comments: recent.ok(),
}
});
// ...
}
7.2 串行依赖请求
#[component]
fn UserProfile(user_id: String) -> Element {
let profile = use_resource(move || async move {
// 先获取用户信息
let user = fetch_user(&user_id).await.ok()?;
// 再根据用户信息获取其文章
let articles = fetch_user_articles(&user.id).await.ok().unwrap_or_default();
Some((user, articles))
});
match profile() {
Some((user, articles)) => rsx! { render_profile(user, articles) },
None => rsx! { "加载中..." },
}
}
8. 小结
use_resource是声明式数据获取的首选,自动依赖追踪和重新执行spawn适合命令式异步任务,如表单提交后的副作用- 竞态条件通过版本号或 use_resource 的自动取消机制解决
- 使用统一的
AsyncState<T>枚举管理加载/成功/错误状态 - 缓存策略(内存/SWR)减少不必要的网络请求
- 骨架屏提升加载时的视觉体验
tokio::join!并行发请求,减少等待时间
dioxusasyncfetchuse_resourcespawnloading