Yew 中的异步数据处理
Yew 提供了强大的异步数据处理能力,通过 wasm-bindgen-futures 和 Rust 的 async/await 语法,可以优雅地处理异步操作。
基础异步操作
1. 使用 send_future 处理异步任务
rustuse yew::prelude::*; use wasm_bindgen_futures::spawn_local; #[function_component(AsyncExample)] fn async_example() -> Html { let data = use_state(|| String::new()); let loading = use_state(|| false); let fetch_data = { let data = data.clone(); let loading = loading.clone(); Callback::from(move |_| { loading.set(true); spawn_local(async move { // 模拟异步操作 gloo_timers::future::sleep(std::time::Duration::from_secs(1)).await; data.set("Async data loaded!".to_string()); loading.set(false); }); }) }; html! { <div> <button onclick={fetch_data} disabled={*loading}> { if *loading { "Loading..." } else { "Fetch Data" } } </button> <p>{ &*data }</p> </div> } }
2. 使用 use_future Hook
rustuse yew::prelude::*; use yew_hooks::prelude::*; #[function_component(UseFutureExample)] fn use_future_example() -> Html { let state = use_future(|| async { // 模拟 API 调用 gloo_timers::future::sleep(std::time::Duration::from_secs(2)).await; "Data from future!".to_string() }); html! { <div> { match &*state { UseFutureState::Pending => html! { <p>{ "Loading..." }</p> }, UseFutureState::Ready(data) => html! { <p>{ data }</p> }, UseFutureState::Failed(_) => html! { <p>{ "Error loading data" }</p> }, }} </div> } }
HTTP 请求处理
1. 使用 reqwest 进行 HTTP 请求
toml[dependencies] reqwest = { version = "0.11", features = ["json"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0"
rustuse serde::{Deserialize, Serialize}; use yew::prelude::*; #[derive(Debug, Serialize, Deserialize, Clone)] pub struct User { pub id: u32, pub name: String, pub email: String, } #[function_component(UserFetcher)] fn user_fetcher() -> Html { let user = use_state(|| None::<User>); let loading = use_state(|| false); let error = use_state(|| None::<String>); let fetch_user = { let user = user.clone(); let loading = loading.clone(); let error = error.clone(); Callback::from(move |_| { loading.set(true); error.set(None); spawn_local(async move { let client = reqwest::Client::new(); match client .get("https://jsonplaceholder.typicode.com/users/1") .send() .await { Ok(response) => { match response.json::<User>().await { Ok(data) => { user.set(Some(data)); } Err(e) => { error.set(Some(format!("Parse error: {}", e))); } } } Err(e) => { error.set(Some(format!("Request error: {}", e))); } } loading.set(false); }); }) }; html! { <div> <button onclick={fetch_user} disabled={*loading}> { if *loading { "Loading..." } else { "Fetch User" } } </button> { if let Some(ref err) = *error { html! { <p class="error">{ err }</p> } } else if let Some(ref u) = *user { html! { <div class="user-card"> <h2>{ &u.name }</h2> <p>{ "ID: " }{ u.id }</p> <p>{ "Email: " }{ &u.email }</p> </div> } } else { html! { <p>{ "No user data" }</p> }} </div> } }
2. 使用 gloo-net 进行 HTTP 请求
toml[dependencies] gloo-net = { version = "0.4", features = ["http", "fetch"] } serde = { version = "1.0", features = ["derive"] }
rustuse gloo_net::http::Request; use serde::{Deserialize, Serialize}; use yew::prelude::*; #[derive(Debug, Serialize, Deserialize, Clone)] pub struct Post { pub id: u32, pub title: String, pub body: String, } #[function_component(PostFetcher)] fn post_fetcher() -> Html { let posts = use_state(|| Vec::<Post>::new()); let loading = use_state(|| false); let fetch_posts = { let posts = posts.clone(); let loading = loading.clone(); Callback::from(move |_| { loading.set(true); spawn_local(async move { match Request::get("https://jsonplaceholder.typicode.com/posts") .send() .await { Ok(response) => { if response.ok() { match response.json::<Vec<Post>>().await { Ok(data) => { posts.set(data); } Err(e) => { web_sys::console::error_1(&format!("Parse error: {}", e).into()); } } } } Err(e) => { web_sys::console::error_1(&format!("Request error: {}", e).into()); } } loading.set(false); }); }) }; html! { <div> <button onclick={fetch_posts} disabled={*loading}> { if *loading { "Loading..." } else { "Fetch Posts" } } </button> <div class="posts"> { posts.iter().map(|post| { html! { <div key={post.id} class="post-card"> <h3>{ &post.title }</h3> <p>{ &post.body }</p> </div> } }).collect::<Html>() } </div> </div> } }
WebSocket 通信
1. 使用 gloo-net WebSocket
toml[dependencies] gloo-net = { version = "0.4", features = ["websocket"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0"
rustuse gloo_net::websocket::{futures::WebSocket, Message, WebSocketError}; use serde::{Deserialize, Serialize}; use yew::prelude::*; #[derive(Debug, Serialize, Deserialize, Clone)] pub struct ChatMessage { pub id: String, pub text: String, pub timestamp: u64, } #[function_component(ChatApp)] fn chat_app() -> Html { let messages = use_state(|| Vec::<ChatMessage>::new()); let input_value = use_state(|| String::new()); let connected = use_state(|| false); let connect = { let messages = messages.clone(); let connected = connected.clone(); Callback::from(move |_| { let ws = WebSocket::open("wss://echo.websocket.org").unwrap(); let onmessage = { let messages = messages.clone(); Callback::from(move |msg: Message| { if let Message::Text(text) = msg { if let Ok(chat_msg) = serde_json::from_str::<ChatMessage>(&text) { let mut msgs = (*messages).clone(); msgs.push(chat_msg); messages.set(msgs); } } }) }; ws.set_onmessage(onmessage); connected.set(true); }) }; let send_message = { let input_value = input_value.clone(); let messages = messages.clone(); Callback::from(move |_| { if !input_value.is_empty() { let chat_msg = ChatMessage { id: uuid::Uuid::new_v4().to_string(), text: (*input_value).clone(), timestamp: chrono::Utc::now().timestamp() as u64, }; let mut msgs = (*messages).clone(); msgs.push(chat_msg.clone()); messages.set(msgs); input_value.set(String::new()); } }) }; let oninput = { let input_value = input_value.clone(); Callback::from(move |e: InputEvent| { let input: HtmlInputElement = e.target_unchecked_into(); input_value.set(input.value()); }) }; html! { <div class="chat-app"> <div class="chat-header"> <h2>{ "Chat Room" }</h2> <button onclick={connect} disabled={*connected}> { if *connected { "Connected" } else { "Connect" } } </button> </div> <div class="messages"> { messages.iter().map(|msg| { html! { <div key={msg.id.clone()} class="message"> <span class="timestamp">{ msg.timestamp }</span> <span class="text">{ &msg.text }</span> </div> } }).collect::<Html>() } </div> <div class="input-area"> <input type="text" value={(*input_value).clone()} oninput={oninput} placeholder="Type a message..." /> <button onclick={send_message} disabled={input_value.is_empty()}> { "Send" } </button> </div> </div> } }
错误处理和重试机制
1. 实现重试逻辑
rustuse yew::prelude::*; #[function_component(RetryExample)] fn retry_example() -> Html { let data = use_state(|| None::<String>); let loading = use_state(|| false); let error = use_state(|| None::<String>); let retry_count = use_state(|| 0); let fetch_with_retry = { let data = data.clone(); let loading = loading.clone(); let error = error.clone(); let retry_count = retry_count.clone(); Callback::from(move |_| { loading.set(true); error.set(None); retry_count.set(*retry_count + 1); spawn_local(async move { let max_retries = 3; let mut attempt = 0; let mut result: Option<String> = None; while attempt < max_retries { attempt += 1; // 模拟可能失败的请求 gloo_timers::future::sleep(std::time::Duration::from_millis(500)).await; // 模拟 50% 失败率 if rand::random::<bool>() { result = Some(format!("Success on attempt {}", attempt)); break; } } if let Some(value) = result { data.set(Some(value)); error.set(None); } else { error.set(Some("Max retries exceeded".to_string())); } loading.set(false); }); }) }; html! { <div> <button onclick={fetch_with_retry} disabled={*loading}> { if *loading { "Retrying..." } else { "Fetch with Retry" } } </button> <p>{ "Attempts: " }{ *retry_count }</p> { if let Some(ref err) = *error { html! { <p class="error">{ err }</p> } } else if let Some(ref value) = *data { html! { <p class="success">{ value }</p> } }} </div> } }
2. 使用 use_async Hook
rustuse yew::prelude::*; use yew_hooks::prelude::*; #[function_component(UseAsyncExample)] fn use_async_example() -> Html { let async_data = use_async(async { // 模拟异步操作 gloo_timers::future::sleep(std::time::Duration::from_secs(1)).await; Ok::<String, String>("Async operation completed!".to_string()) }); html! { <div> <button onclick={|_| async_data.run()} disabled={async_data.loading}> { if async_data.loading { "Running..." } else { "Run Async" } } </button> { match &async_data.data { Some(data) => html! { <p class="success">{ data }</p> }, None => html! { <p>{ "No data yet" }</p> }, }} { match &async_data.error { Some(error) => html! { <p class="error">{ error }</p> }, None => html! {}, }} </div> } }
数据缓存和状态管理
1. 实现简单的数据缓存
rustuse std::collections::HashMap; use yew::prelude::*; #[function_component(CachedData)] fn cached_data() -> Html { let cache = use_mut_ref(|| HashMap::<String, String>::new()); let data = use_state(|| None::<String>); let loading = use_state(|| false); let fetch_cached = { let cache = cache.clone(); let data = data.clone(); let loading = loading.clone(); Callback::from(move |key: String| { // 检查缓存 if let Some(cached_value) = cache.borrow().get(&key) { data.set(Some(cached_value.clone())); return; } // 从服务器获取 loading.set(true); spawn_local(async move { // 模拟 API 调用 gloo_timers::future::sleep(std::time::Duration::from_secs(1)).await; let value = format!("Data for {}", key); // 更新缓存 cache.borrow_mut().insert(key.clone(), value.clone()); data.set(Some(value)); loading.set(false); }); }) }; html! { <div> <button onclick={Callback::from(move |_| fetch_cached.emit("key1".to_string()))}> { "Fetch Key 1" } </button> <button onclick={Callback::from(move |_| fetch_cached.emit("key2".to_string()))}> { "Fetch Key 2" } </button> { if *loading { html! { <p>{ "Loading..." }</p> } } else if let Some(ref value) = *data { html! { <p>{ value }</p> } }} </div> } }
最佳实践
- 错误处理:始终处理异步操作中的错误
- 加载状态:提供清晰的加载反馈
- 取消操作:实现取消未完成请求的机制
- 数据缓存:合理使用缓存减少不必要的请求
- 重试机制:对关键操作实现重试逻辑
- 类型安全:使用 Rust 的类型系统确保数据安全