5月31日 01:21

Yew 里异步数据该用 spawn_local 还是 use_effect_with?

Yew 里处理异步数据,先记住一个边界:组件渲染本身不能 await,异步任务必须被放到浏览器事件循环里执行,再把结果写回状态。最常见的选择是 spawn_localuse_effect_withuse_async 或自己封装状态机。它们没有绝对高低,关键看异步任务是由用户动作触发,还是由组件生命周期和参数变化触发。

点击触发的请求适合 spawn_local

按钮提交、刷新列表、重试失败请求这类动作,用 spawn_local 最直观。它的优点是写法接近普通 Rust async,缺点是任务一旦启动就不会因为组件卸载自动取消,所以回调里要避免闭包持有过多状态。实际项目里还要处理重复点击,否则第二次请求可能比第一次先返回,把新数据覆盖成旧数据。

rust
use gloo_net::http::Request; use wasm_bindgen_futures::spawn_local; use yew::prelude::*; #[function_component(UserPanel)] fn user_panel() -> Html { let user = use_state(|| None::<String>); let loading = use_state(|| false); let onclick = { let user = user.clone(); let loading = loading.clone(); Callback::from(move |_| { loading.set(true); let user = user.clone(); let loading = loading.clone(); spawn_local(async move { let text = Request::get("/api/user").send().await .and_then(|r| r.text()) .await .unwrap_or_else(|_| "load failed".into()); user.set(Some(text)); loading.set(false); }); }) }; html! { <button {onclick} disabled={*loading}>{ user.as_deref().unwrap_or("Load user") }</button> } }

参数变化的请求适合 use_effect_with

如果请求依赖 id、分页、筛选条件,就不要把逻辑散落在多个按钮里。use_effect_with 能表达“依赖变化就重新拉取”,读起来更像 React 里的 effect。坑在于依赖值必须稳定,复杂对象频繁创建会导致请求被反复触发。遇到搜索框输入时,通常还要加 debounce,否则每个键盘事件都会打到后端。

rust
#[derive(Clone, PartialEq, Properties)] struct Props { pub user_id: u32 } #[function_component(Profile)] fn profile(props: &Props) -> Html { let name = use_state(|| String::new()); { let name = name.clone(); let id = props.user_id; use_effect_with(id, move |_| { spawn_local(async move { let url = format!("/api/users/{id}"); if let Ok(resp) = Request::get(&url).send().await { name.set(resp.text().await.unwrap_or_default()); } }); || () }); } html! { <p>{ (*name).clone() }</p> } }

状态别只放 data

异步 UI 至少要区分 idleloadingsuccesserror,否则页面会出现“空白到底是没数据还是请求失败”的尴尬。小组件可以用三个 use_state,列表页或表单页更建议定义枚举,避免 loading 结束但 error 没清掉的脏状态。缓存也不是越早做越好,Yew 端缓存适合低频、可容忍短暂旧数据的接口;强一致数据仍应以服务端返回为准。

错误和重试要克制

请求失败后直接 unwrap 是 Yew 新手最常见的坑,浏览器网络环境比本地测试脆弱得多。建议把错误展示成用户能理解的文案,同时保留日志给调试用。重试也要有边界:普通查询可以手动重试,支付、下单、写入接口必须考虑幂等,否则一次网络抖动可能造成重复提交。

如果接口支持取消,可以把 AbortController 或请求 token 也纳入封装;如果不支持取消,至少要在响应回来时判断它是否仍然属于当前页面状态。这样做牺牲了一点代码简洁度,但能换来更可靠的用户体验。

追问

为什么不能在组件函数里直接 await?

Yew 的函数组件必须同步返回 Html,否则虚拟 DOM 没法完成本轮渲染。把 await 放进组件函数会破坏这个约定,所以要用 spawn_local 或 effect 把异步任务挪出去。取舍是代码多了一层状态管理,但边界更清楚:渲染只读状态,异步只改状态。

spawn_local 会不会导致内存泄漏?

它本身不等于泄漏,但任务不会因为组件消失自动停下,这是浏览器端异步的常见坑。若任务里捕获了大对象,或请求返回后还更新已经无意义的状态,就会造成额外开销。边界做法是让任务只捕获必要的 UseStateHandle,长轮询、WebSocket 这类任务要在 effect 清理函数里显式关闭。

HTTP 请求用 reqwest 还是 gloo-net?

浏览器端 Yew 项目通常优先选 gloo-net,依赖轻,和 Web API 的行为更贴近。reqwest 在 Rust 生态里更通用,但 wasm 目标下功能会受限制,包体也可能更重。取舍很简单:前端只做普通 JSON 请求选 gloo-net,共享跨端 SDK 或已有封装时再考虑 reqwest

如何避免旧请求覆盖新结果?

可以给每次请求加递增版本号,只接受当前版本的响应。也可以在输入搜索时先 debounce,再发请求,减少乱序返回的概率。踩坑点是不要只靠 loading 判断,因为两个请求并发时,第一个完成会把 loading 置回 false,让用户以为第二个也结束了。

WebSocket 应该放在哪里管理?

WebSocket 不适合散落在多个组件的点击回调里,最好放在上层组件或自定义 hook 中统一管理。它需要连接、重连、心跳、关闭四个状态,普通一次性请求的写法不够用。边界是实时协作、行情、日志流适合 WebSocket;偶尔刷新一次的通知列表,用 HTTP 轮询反而更简单。

小结

Yew 的异步处理不是把 Rust 后端那套原样搬到浏览器,而是围绕组件状态做编排。用户动作触发用 spawn_local,参数变化用 use_effect_with,复杂页面把 loading、error、data 收进明确的状态模型。只要处理好重复请求、卸载清理和错误展示,异步代码就不会在页面越写越大时失控。

标签:Yew