乐闻世界logo
搜索文章和话题

Yew

Yew 是一个用 Rust 编写的现代 Rust 框架,用于创建多线程前端 web 应用。它使用 WebAssembly (Wasm) 运行在浏览器中,使得开发者能够利用 Rust 的性能和安全性来构建高效的网页应用。Yew 类似于 JavaScript 的 React 框架,采用组件化的开发模式,支持 JSX-like 模板语法(称为 "macro HTML")。
Yew
查看更多相关内容
Yew 和 React 有哪些主要区别,如何选择适合的框架?## Yew 与 React 的对比分析 Yew 和 React 都是现代前端框架,但它们在语言、运行时和性能方面有显著差异。 ### 核心差异对比 | 特性 | Yew | React | |------|-----|-------| | **编程语言** | Rust | JavaScript/TypeScript | | **运行时** | WebAssembly (Wasm) | JavaScript | | **类型系统** | 静态强类型 | 动态类型(可选 TypeScript) | | **编译时** | 编译到 Wasm | JIT 编译 | | **性能** | 接近原生 | 依赖 JS 引擎优化 | | **包大小** | 较大(包含 Wasm 运行时) | 较小 | | **学习曲线** | 较陡(需要学习 Rust) | 较平缓 | | **生态系统** | 较新,较小 | 成熟,庞大 | | **开发工具** | 有限 | 丰富(DevTools, ESLint 等) | ### 代码示例对比 #### 组件定义 **Yew:** ```rust use yew::prelude::*; #[function_component(HelloWorld)] fn hello_world() -> Html { html! { <div class="greeting"> <h1>{ "Hello, World!" }</h1> </div> } } ``` **React:** ```jsx function HelloWorld() { return ( <div className="greeting"> <h1>Hello, World!</h1> </div> ); } ``` #### 状态管理 **Yew:** ```rust #[function_component(Counter)] fn counter() -> Html { let counter = use_state(|| 0); let onclick = { let counter = counter.clone(); Callback::from(move |_| counter.set(*counter + 1)) }; html! { <div> <button onclick={onclick}>{ "Increment" }</button> <p>{ *counter }</p> </div> } } ``` **React:** ```jsx function Counter() { const [counter, setCounter] = useState(0); return ( <div> <button onClick={() => setCounter(counter + 1)}>Increment</button> <p>{counter}</p> </div> ); } ``` #### Props 传递 **Yew:** ```rust #[derive(Properties, PartialEq)] pub struct ButtonProps { pub label: String, #[prop_or_default] pub disabled: bool, pub onclick: Callback<MouseEvent>, } #[function_component(Button)] fn button(props: &ButtonProps) -> Html { html! { <button disabled={props.disabled} onclick={props.onclick.clone()} > { &props.label } </button> } } ``` **React:** ```jsx function Button({ label, disabled = false, onClick }) { return ( <button disabled={disabled} onClick={onClick}> {label} </button> ); } ``` ### 性能对比 #### 执行性能 **Yew 优势:** - 编译为 WebAssembly,执行速度接近原生代码 - Rust 的零成本抽象和优化 - 更好的内存管理和安全性 - 适合计算密集型任务 **React 优势:** - JavaScript 引擎持续优化 - 更小的初始加载时间(无需 Wasm 运行时) - 更快的热重载和开发体验 #### 基准测试场景 | 场景 | Yew | React | |------|-----|-------| | 简单列表渲染 | 相当 | 相当 | | 复杂计算 | 更快 | 较慢 | | 大型应用启动 | 较慢 | 较快 | | DOM 操作 | 相当 | 相当 | ### 开发体验对比 #### Yew 开发体验 **优点:** - 类型安全,编译时错误检测 - 更好的代码可维护性 - Rust 的工具链(cargo, clippy) - 强大的并发支持 **缺点:** - 编译时间较长 - Wasm 调试相对困难 - 生态系统较小 - 学习曲线陡峭 #### React 开发体验 **优点:** - 快速开发迭代 - 丰富的生态系统和库 - 成熟的开发工具 - 庞大的社区支持 **缺点:** - 运行时错误 - 类型安全需要 TypeScript - 性能优化需要更多手动工作 ### 适用场景 #### 选择 Yew 的场景 1. **高性能要求**:需要处理大量计算或数据的应用 2. **安全敏感**:金融、医疗等对安全性要求高的领域 3. **Rust 团队**:团队熟悉 Rust 语言 4. **长期维护**:大型项目需要长期维护和稳定性 5. **WebAssembly 优势**:需要充分利用 Wasm 的能力 #### 选择 React 的场景 1. **快速原型**:需要快速开发和迭代 2. **团队熟悉 JS**:团队主要使用 JavaScript/TypeScript 3. **生态系统需求**:需要使用丰富的第三方库 4. **开发效率**:优先考虑开发速度而非运行时性能 5. **传统 Web 应用**:典型的 CRUD 应用 ### 迁移考虑 #### 从 React 迁移到 Yew **挑战:** - 需要学习 Rust 语言 - 重写所有组件和逻辑 - 适配不同的 API 和模式 - 生态系统差异 **收益:** - 更好的性能 - 类型安全 - 更少的运行时错误 #### 从 Yew 迁移到 React **挑战:** - 失去类型安全(除非使用 TypeScript) - 性能可能下降 - 需要适应不同的开发模式 **收益:** - 更快的开发速度 - 更丰富的生态系统 - 更好的开发工具 ### 未来展望 **Yew:** - WebAssembly 生态系统持续发展 - Rust 前端社区增长 - 工具链和调试工具改进 **React:** - Server Components 和 RSC - 更好的性能优化 - 继续主导前端市场 ### 结论 选择 Yew 还是 React 取决于项目需求、团队技能和长期目标。Yew 提供了更好的性能和类型安全,但需要更高的学习成本;React 提供了更好的开发体验和生态系统,但可能在性能和类型安全方面有所妥协。
服务端 · 2月19日 16:25
Yew 如何与 WebAssembly 集成,有哪些性能优化技巧?## Yew 与 WebAssembly 的集成 Yew 框架的核心优势在于它与 WebAssembly (Wasm) 的深度集成,这使得用 Rust 编写的前端应用能够在浏览器中高效运行。 ### WebAssembly 基础 #### 什么是 WebAssembly? WebAssembly (Wasm) 是一种低级类汇编语言,可以在现代 Web 浏览器中运行。它设计为与 JavaScript 并存,允许开发者使用 Rust、C++、Go 等语言编写高性能的 Web 应用。 **主要特性:** - 二进制格式,体积小、加载快 - 接近原生代码的执行性能 - 与 JavaScript 互操作 - 安全的内存模型 - 跨平台兼容性 ### Yew 的 Wasm 架构 #### 编译流程 ``` Rust 源代码 ↓ Cargo 编译 ↓ wasm-pack 打包 ↓ WebAssembly 二进制文件 ↓ 浏览器加载执行 ``` #### 项目配置 **Cargo.toml:** ```toml [package] name = "yew-app" version = "0.1.0" edition = "2021" [lib] crate-type = ["cdylib", "rlib"] [dependencies] yew = { version = "0.21", features = ["csr"] } wasm-bindgen = "0.2" wasm-bindgen-futures = "0.4" web-sys = { version = "0.3", features = [ "Window", "Document", "Element", "HtmlElement", "console", ] } [dev-dependencies] wasm-bindgen-test = "0.3" ``` **index.html:** ```html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Yew Wasm App</title> <link data-trunk rel="css" href="index.css"> </head> <body> <div id="app"></div> <script type="module"> import init from './wasm/yew_app.js'; init().then(() => { console.log('Wasm module loaded'); }); </script> </body> </html> ``` ### Wasm 与 JavaScript 互操作 #### 1. 调用 JavaScript 函数 ```rust use wasm_bindgen::prelude::*; #[wasm_bindgen] extern "C" { #[wasm_bindgen(js_namespace = console)] fn log(s: &str); #[wasm_bindgen(js_namespace = window, js_name = alert)] fn alert(s: &str); } #[function_component(JSInterop)] fn js_interop() -> Html { let onclick = Callback::from(|_| { log("Button clicked from Rust!"); alert("Hello from WebAssembly!"); }); html! { <button onclick={onclick}> { "Call JavaScript" } </button> } } ``` #### 2. 从 JavaScript 调用 Rust 函数 ```rust use wasm_bindgen::prelude::*; #[wasm_bindgen] pub fn greet(name: &str) -> String { format!("Hello, {}!", name) } #[wasm_bindgen] pub fn add(a: i32, b: i32) -> i32 { a + b } #[wasm_bindgen] pub struct Calculator { value: i32, } #[wasm_bindgen] impl Calculator { #[wasm_bindgen(constructor)] pub fn new(initial: i32) -> Calculator { Calculator { value: initial } } #[wasm_bindgen] pub fn add(&mut self, amount: i32) { self.value += amount; } #[wasm_bindgen] pub fn get_value(&self) -> i32 { self.value } } ``` **在 JavaScript 中使用:** ```javascript // 调用简单函数 const greeting = wasm.greet("World"); console.log(greeting); // "Hello, World!" // 调用计算函数 const result = wasm.add(5, 3); console.log(result); // 8 // 使用 Rust 结构体 const calc = new wasm.Calculator(10); calc.add(5); console.log(calc.get_value()); // 15 ``` #### 3. 复杂数据类型传递 ```rust use serde::{Deserialize, Serialize}; use wasm_bindgen::prelude::*; #[derive(Serialize, Deserialize)] pub struct UserData { pub name: String, pub age: u32, pub email: String, } #[wasm_bindgen] pub fn process_user_data(json: &str) -> Result<String, JsValue> { let user: UserData = serde_json::from_str(json) .map_err(|e| JsValue::from_str(&e.to_string()))?; let processed = format!( "User: {} ({} years old) - {}", user.name, user.age, user.email ); Ok(processed) } #[wasm_bindgen] pub fn create_user_data(name: String, age: u32, email: String) -> JsValue { let user = UserData { name, age, email }; serde_wasm_bindgen::to_value(&user).unwrap() } ``` ### 性能优化 #### 1. 减少序列化开销 ```rust // 不好的做法:频繁序列化 #[wasm_bindgen] pub fn process_large_data(data: JsValue) -> JsValue { let parsed: Vec<i32> = serde_wasm_bindgen::from_value(data).unwrap(); let result: Vec<i32> = parsed.iter().map(|x| x * 2).collect(); serde_wasm_bindgen::to_value(&result).unwrap() } // 好的做法:使用内存共享 use wasm_bindgen::JsCast; #[wasm_bindgen] pub fn process_array_in_place(array: &js_sys::Uint32Array) { for i in 0..array.length() { let value = array.get(i); array.set(i, value * 2); } } ``` #### 2. 使用 Web Workers ```rust use wasm_bindgen::prelude::*; use web_sys::{DedicatedWorkerGlobalScope, WorkerOptions}; #[wasm_bindgen] pub fn start_worker() { let worker = web_sys::Worker::new("worker.js").unwrap(); worker.set_onmessage(Some(Closure::wrap(Box::new(|event: MessageEvent| { let data = event.data(); web_sys::console::log_1(&data); }) as Box<dyn FnMut(_)>).into_js_value().unchecked_ref())); } // worker.js self.importScripts('wasm/yew_app.js'); wasm.run_worker(); ``` #### 3. 优化 Wasm 包大小 ```toml # Cargo.toml [profile.release] opt-level = "z" # 优化大小 lto = true # 链接时优化 codegen-units = 1 panic = "abort" # 减少恐慌处理代码 [dependencies] wee_alloc = { version = "0.4", optional = true } [features] default = ["wee_alloc"] ``` ```rust // lib.rs #[cfg(feature = "wee_alloc")] #[global_allocator] static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT; ``` ### 调试 Wasm 代码 #### 1. 使用浏览器开发工具 ```rust use web_sys::console; #[function_component(DebugExample)] fn debug_example() -> Html { let onclick = Callback::from(|_| { console::log_1(&"Debug message".into()); console::error_1(&"Error message".into()); console::warn_1(&"Warning message".into()); // 格式化日志 console::log_2( &"Value:".into(), &42.into() ); }); html! { <button onclick={onclick}> { "Log to Console" } </button> } } ``` #### 2. 使用 wasm-pack 测试 ```bash # 运行测试 wasm-pack test --firefox --headless # 运行特定测试 wasm-pack test --firefox --headless test_name ``` ### 实际应用场景 #### 1. 图像处理 ```rust use wasm_bindgen::prelude::*; #[wasm_bindgen] pub fn process_image(data: &[u8], width: u32, height: u32) -> Vec<u8> { let mut result = data.to_vec(); for i in (0..result.len()).step_by(4) { // 简单的灰度转换 let r = result[i] as f32; let g = result[i + 1] as f32; let b = result[i + 2] as f32; let gray = (0.299 * r + 0.587 * g + 0.114 * b) as u8; result[i] = gray; result[i + 1] = gray; result[i + 2] = gray; } result } ``` #### 2. 数据加密 ```rust use sha2::{Sha256, Digest}; use wasm_bindgen::prelude::*; #[wasm_bindgen] pub fn hash_data(data: &str) -> String { let mut hasher = Sha256::new(); hasher.update(data.as_bytes()); let result = hasher.finalize(); format!("{:x}", result) } ``` #### 3. 复杂计算 ```rust use wasm_bindgen::prelude::*; #[wasm_bindgen] pub fn fibonacci(n: u64) -> u64 { match n { 0 => 0, 1 => 1, _ => { let mut a = 0u64; let mut b = 1u64; for _ in 2..=n { let temp = a + b; a = b; b = temp; } b } } } #[wasm_bindgen] pub fn calculate_primes(limit: u32) -> Vec<u32> { let mut primes = Vec::new(); let mut is_prime = vec![true; (limit + 1) as usize]; for i in 2..=limit { if is_prime[i as usize] { primes.push(i); let mut j = i * i; while j <= limit { is_prime[j as usize] = false; j += i; } } } primes } ``` ### 最佳实践 1. **最小化 Wasm-JS 边界**:减少跨边界调用,批量处理数据 2. **使用类型安全的绑定**:利用 `wasm-bindgen` 的类型系统 3. **优化包大小**:使用 `wee_alloc`、LTO 和适当的优化级别 4. **错误处理**:使用 `Result` 类型正确处理错误 5. **内存管理**:注意 Wasm 的线性内存限制 6. **测试**:使用 `wasm-bindgen-test` 进行单元测试 ### 性能对比 | 操作 | JavaScript | WebAssembly (Yew) | 提升 | |------|-----------|-------------------|------| | 简单计算 | 100ms | 20ms | 5x | | 图像处理 | 500ms | 50ms | 10x | | 数据加密 | 200ms | 30ms | 6.7x | | 复杂算法 | 1000ms | 150ms | 6.7x | ### 总结 Yew 与 WebAssembly 的集成提供了强大的性能优势,特别适合计算密集型任务。通过合理使用 Wasm-JS 互操作、优化包大小和利用 Rust 的类型系统,可以构建高性能、安全的前端应用。
服务端 · 2月19日 16:24
Yew 中如何使用 Hooks,有哪些常用的 Hook 函数?## Yew 中的 Hooks 使用详解 Yew 从 0.19 版本开始支持 Hooks API,提供了类似 React Hooks 的功能,使组件编写更加简洁和函数式。 ### 基础 Hooks #### 1. `use_state` `use_state` 用于在函数组件中管理状态,类似于 React 的 `useState`。 ```rust #[function_component(Counter)] fn counter() -> Html { let counter = use_state(|| 0); let increment = { let counter = counter.clone(); Callback::from(move |_| counter.set(*counter + 1)) }; let decrement = { let counter = counter.clone(); Callback::from(move |_| counter.set(*counter - 1)) }; html! { <div> <button onclick={decrement}>{ "-" }</button> <span>{ *counter }</span> <button onclick={increment}>{ "+" }</button> </div> } } ``` **使用技巧:** - 初始值通过闭包提供,只在首次渲染时计算 - 返回的 `UseStateHandle` 可以克隆以在闭包中使用 - 使用 `set()` 方法更新状态 - 使用 `*` 解引用获取当前值 #### 2. `use_reducer` `use_reducer` 用于复杂的状态逻辑,类似于 React 的 `useReducer`。 ```rust #[derive(Clone, PartialEq)] pub enum CounterAction { Increment, Decrement, IncrementBy(i32), Reset, } fn counter_reducer(state: i32, action: CounterAction) -> i32 { match action { CounterAction::Increment => state + 1, CounterAction::Decrement => state - 1, CounterAction::IncrementBy(amount) => state + amount, CounterAction::Reset => 0, } } #[function_component(ReducerCounter)] fn reducer_counter() -> Html { let (counter, dispatch) = use_reducer(counter_reducer, 0); let increment = dispatch.reform(|_| CounterAction::Increment); let decrement = dispatch.reform(|_| CounterAction::Decrement); let increment_by_5 = dispatch.reform(|_| CounterAction::IncrementBy(5)); let reset = dispatch.reform(|_| CounterAction::Reset); html! { <div> <button onclick={decrement}>{ "-" }</button> <span>{ *counter }</span> <button onclick={increment}>{ "+" }</button> <button onclick={increment_by_5}>{ "+5" }</button> <button onclick={reset}>{ "Reset" }</button> </div> } } ``` #### 3. `use_effect` `use_effect` 用于处理副作用,类似于 React 的 `useEffect`。 ```rust #[function_component(EffectExample)] fn effect_example() -> Html { let count = use_state(|| 0); // 基本效果 use_effect(move || { web_sys::console::log_1(&"Component mounted".into()); // 清理函数 || { web_sys::console::log_1(&"Component unmounted".into()); } }); // 带依赖的效果 let count_effect = { let count = count.clone(); use_effect(move || { web_sys::console::log_2( &"Count changed:".into(), &(*count).into() ); || {} }) }; let onclick = { let count = count.clone(); Callback::from(move |_| count.set(*count + 1)) }; html! { <div> <button onclick={onclick}>{ "Increment" }</button> <p>{ *count }</p> </div> } } ``` #### 4. `use_ref` `use_ref` 用于存储可变值,类似于 React 的 `useRef`。 ```rust #[function_component(RefExample)] fn ref_example() -> Html { let input_ref = use_node_ref(); let count_ref = use_ref(|| 0); let onfocus = Callback::from(|_| { web_sys::console::log_1(&"Input focused".into()); }); let onclick = Callback::from({ let input_ref = input_ref.clone(); let count_ref = count_ref.clone(); move |_| { if let Some(input) = input_ref.cast::<HtmlInputElement>() { web_sys::console::log_2( &"Input value:".into(), &input.value().into() ); } *count_ref.borrow_mut() += 1; web_sys::console::log_2( &"Click count:".into(), &(*count_ref.borrow()).into() ); } }); html! { <div> <input ref={input_ref} type="text" placeholder="Focus me" onfocus={onfocus} /> <button onclick={onclick}>{ "Log Value" }</button> </div> } } ``` ### 高级 Hooks #### 5. `use_context` `use_context` 用于消费 Context,类似于 React 的 `useContext`。 ```rust #[derive(Clone, PartialEq, Default)] pub struct ThemeContext { pub is_dark: bool, } #[function_component(ThemeProvider)] fn theme_provider() -> Html { let is_dark = use_state(|| false); let context = ThemeContext { is_dark: *is_dark, }; html! { <ContextProvider<ThemeContext> context={context}> <ThemedComponent /> </ContextProvider<ThemeContext>> } } #[function_component(ThemedComponent)] fn themed_component() -> Html { let theme = use_context::<ThemeContext>().unwrap(); let class = if theme.is_dark { "dark-theme" } else { "light-theme" }; html! { <div class={class}> { "Themed content" } </div> } } ``` #### 6. `use_memo` `use_memo` 用于记忆化计算结果,类似于 React 的 `useMemo`。 ```rust #[function_component(MemoExample)] fn memo_example() -> Html { let count = use_state(|| 0); let other = use_state(|| 0); // 记忆化计算 let expensive_value = use_memo( (*count, *other), |(count, other)| { web_sys::console::log_1(&"Computing expensive value".into()); count * 2 + other * 3 } ); let increment_count = { let count = count.clone(); Callback::from(move |_| count.set(*count + 1)) }; let increment_other = { let other = other.clone(); Callback::from(move |_| other.set(*other + 1)) }; html! { <div> <button onclick={increment_count}>{ "Count: " }{ *count }</button> <button onclick={increment_other}>{ "Other: " }{ *other }</button> <p>{ "Expensive value: " }{ **expensive_value }</p> </div> } } ``` #### 7. `use_callback` `use_callback` 用于记忆化回调函数,类似于 React 的 `useCallback`。 ```rust #[function_component(CallbackExample)] fn callback_example() -> Html { let count = use_state(|| 0); // 记忆化回调 let increment = use_callback(count.clone(), |count, _| { count.set(*count + 1); }); // 带参数的记忆化回调 let add_value = use_callback(count.clone(), |count, value: i32| { count.set(*count + value); }); html! { <div> <button onclick={increment}>{ "Increment" }</button> <button onclick={Callback::from(move |_| add_value.emit(5))}> { "Add 5" } </button> <p>{ *count }</p> </div> } } ``` ### 自定义 Hooks 自定义 Hooks 是复用逻辑的强大方式。 ```rust // 自定义 Hook:使用本地存储 pub fn use_local_storage<T>(key: &str, initial_value: T) -> UseStateHandle<T> where T: Clone + PartialEq + serde::Serialize + serde::de::DeserializeOwned + 'static, { let state = use_state(|| { if let Ok(window) = web_sys::window() { if let Ok(Some(storage)) = window.local_storage() { if let Ok(Some(value)) = storage.get_item(key) { if let Ok(deserialized) = serde_json::from_str::<T>(&value) { return deserialized; } } } } initial_value }); let key = key.to_string(); let state_effect = state.clone(); use_effect(move || { if let Ok(window) = web_sys::window() { if let Ok(Some(storage)) = window.local_storage() { if let Ok(value) = serde_json::to_string(&*state_effect) { let _ = storage.set_item(&key, &value); } } } || {} }); state } // 使用自定义 Hook #[function_component(StorageExample)] fn storage_example() -> Html { let name = use_local_storage("username", String::new()); let oninput = { let name = name.clone(); Callback::from(move |e: InputEvent| { let input: HtmlInputElement = e.target_unchecked_into(); name.set(input.value()); }) }; html! { <div> <input type="text" value={(*name).clone()} oninput={oninput} placeholder="Enter your name" /> <p>{ "Your name: " }{ &*name }</p> </div> } } ``` ### Hooks 规则 使用 Hooks 时必须遵循以下规则: 1. **只在函数组件或自定义 Hook 中调用 Hooks** ```rust // ✅ 正确 #[function_component(MyComponent)] fn my_component() -> Html { let count = use_state(|| 0); html! { <div>{ *count }</div> } } // ❌ 错误 fn regular_function() { let count = use_state(|| 0); // 不能在普通函数中调用 } ``` 2. **只在顶层调用 Hooks** ```rust // ✅ 正确 #[function_component(MyComponent)] fn my_component() -> Html { let count = use_state(|| 0); let name = use_state(|| String::new()); html! { <div>{ *count }</div> } } // ❌ 错误 #[function_component(MyComponent)] fn my_component() -> Html { let count = use_state(|| 0); if *count > 0 { let name = use_state(|| String::new()); // 不能在条件中调用 } html! { <div>{ *count }</div> } } ``` 3. **保持 Hooks 调用顺序一致** ```rust // ✅ 正确:每次渲染都按相同顺序调用 #[function_component(MyComponent)] fn my_component() -> Html { let count = use_state(|| 0); let name = use_state(|| String::new()); html! { <div>{ *count }</div> } } ``` ### 性能优化技巧 #### 1. 使用 `use_memo` 避免重复计算 ```rust let expensive_result = use_memo( (*input1, *input2), |(a, b)| { // 只在 input1 或 input2 变化时重新计算 complex_calculation(a, b) } ); ``` #### 2. 使用 `use_callback` 稳定回调引用 ```rust let handle_click = use_callback( dependency.clone(), |dep, _| { // 只在 dependency 变化时创建新的回调 do_something(dep) } ); ``` #### 3. 合理使用 `use_ref` 避免不必要的状态更新 ```rust let ref_count = use_ref(|| 0); // 不会触发重新渲染 *ref_count.borrow_mut() += 1; ```
服务端 · 2月19日 16:24
Yew 中如何处理事件,支持哪些类型的事件?## Yew 中的事件处理机制 Yew 提供了强大的事件处理机制,类似于 React,但使用 Rust 的类型系统来确保类型安全。 ### 基本事件处理 #### 1. 点击事件 ```rust #[function_component(ClickHandler)] fn click_handler() -> Html { let onclick = Callback::from(|_| { web_sys::console::log_1(&"Button clicked!".into()); }); html! { <button onclick={onclick}> { "Click Me" } </button> } } ``` #### 2. 输入事件 ```rust #[function_component(InputHandler)] fn input_handler() -> Html { let value = use_state(|| String::new()); let oninput = { let value = value.clone(); Callback::from(move |e: InputEvent| { let input: HtmlInputElement = e.target_unchecked_into(); value.set(input.value()); }) }; html! { <div> <input type="text" value={(*value).clone()} oninput={oninput} placeholder="Type something..." /> <p>{ "You typed: " }{ &*value }</p> </div> } } ``` #### 3. 表单提交事件 ```rust #[function_component(FormHandler)] fn form_handler() -> Html { let username = use_state(|| String::new()); let password = use_state(|| String::new()); let onsubmit = { let username = username.clone(); let password = password.clone(); Callback::from(move |e: SubmitEvent| { e.prevent_default(); web_sys::console::log_2( &"Username:".into(), &(*username).clone().into() ); }) }; let onusername_input = { let username = username.clone(); Callback::from(move |e: InputEvent| { let input: HtmlInputElement = e.target_unchecked_into(); username.set(input.value()); }) }; let onpassword_input = { let password = password.clone(); Callback::from(move |e: InputEvent| { let input: HtmlInputElement = e.target_unchecked_into(); password.set(input.value()); }) }; html! { <form onsubmit={onsubmit}> <div> <label>{ "Username:" }</label> <input type="text" value={(*username).clone()} oninput={onusername_input} /> </div> <div> <label>{ "Password:" }</label> <input type="password" value={(*password).clone()} oninput={onpassword_input} /> </div> <button type="submit">{ "Submit" }</button> </form> } } ``` ### 事件类型 Yew 支持多种事件类型,每种类型都有对应的 Rust 类型: | 事件类型 | Rust 类型 | 常见用途 | |---------|-----------|---------| | `onclick` | `MouseEvent` | 鼠标点击 | | `ondblclick` | `MouseEvent` | 鼠标双击 | | `onmouseover` | `MouseEvent` | 鼠标悬停 | | `onmouseout` | `MouseEvent` | 鼠标移出 | | `oninput` | `InputEvent` | 输入变化 | | `onchange` | `Event` | 值改变 | | `onsubmit` | `SubmitEvent` | 表单提交 | | `onkeydown` | `KeyboardEvent` | 键盘按下 | | `onkeyup` | `KeyboardEvent` | 键盘抬起 | | `onfocus` | `FocusEvent` | 获得焦点 | | `onblur` | `FocusEvent` | 失去焦点 | ### 高级事件处理 #### 1. 事件冒泡和捕获 ```rust #[function_component(EventBubbling)] fn event_bubbling() -> Html { let parent_click = Callback::from(|e: MouseEvent| { web_sys::console::log_1(&"Parent clicked".into()); e.stop_propagation(); }); let child_click = Callback::from(|_| { web_sys::console::log_1(&"Child clicked".into()); }); html! { <div onclick={parent_click} style="padding: 20px; background: lightblue;"> { "Parent" } <div onclick={child_click} style="padding: 10px; background: lightgreen;"> { "Child" } </div> </div> } } ``` #### 2. 自定义事件 ```rust #[function_component(CustomEvent)] fn custom_event() -> Html { let on_custom = Callback::from(|data: String| { web_sys::console::log_1(&format!("Custom event received: {}", data).into()); }); html! { <ChildComponent on_custom={on_custom} /> } } #[derive(Properties, PartialEq)] pub struct ChildProps { pub on_custom: Callback<String>, } #[function_component(ChildComponent)] fn child_component(props: &ChildProps) -> Html { let trigger_custom = { let on_custom = props.on_custom.clone(); Callback::from(move |_| { on_custom.emit("Custom data from child".to_string()); }) }; html! { <button onclick={trigger_custom}> { "Trigger Custom Event" } </button> } } ``` #### 3. 防抖和节流 ```rust use std::rc::Rc; use std::cell::RefCell; use std::time::Duration; #[function_component(DebouncedInput)] fn debounced_input() -> Html { let value = use_state(|| String::new()); let debounced_value = use_state(|| String::new()); let timeout_handle = use_mut_ref(|| None::<gloo_timers::callback::Timeout>); let oninput = { let value = value.clone(); let debounced_value = debounced_value.clone(); let timeout_handle = timeout_handle.clone(); Callback::from(move |e: InputEvent| { let input: HtmlInputElement = e.target_unchecked_into(); let new_value = input.value(); value.set(new_value.clone()); // 清除之前的定时器 if let Some(handle) = timeout_handle.borrow_mut().take() { handle.cancel(); } // 设置新的防抖定时器 let debounced_value = debounced_value.clone(); let timeout = gloo_timers::callback::Timeout::new(300, move || { debounced_value.set(new_value); }); *timeout_handle.borrow_mut() = Some(timeout); }) }; html! { <div> <input type="text" value={(*value).clone()} oninput={oninput} placeholder="Type with debounce..." /> <p>{ "Debounced value: " }{ &*debounced_value }</p> </div> } } ``` ### 事件处理最佳实践 #### 1. 使用 `Callback::from` 简化代码 ```rust // 简单事件处理 let onclick = Callback::from(|_| { web_sys::console::log_1(&"Clicked".into()); }); // 带状态的事件处理 let onclick = { let count = count.clone(); Callback::from(move |_| count.set(*count + 1)) }; ``` #### 2. 避免不必要的克隆 ```rust // 不好的做法:每次渲染都克隆 let onclick = Callback::from({ let value = value.clone(); move |_| { // 使用 value } }); // 好的做法:使用 Rc 或引用 let onclick = Callback::from({ let value = Rc::clone(&value); move |_| { // 使用 value } }); ``` #### 3. 类型安全的事件处理 ```rust // 使用具体的事件类型 let oninput = Callback::from(|e: InputEvent| { let input: HtmlInputElement = e.target_unchecked_into(); let value = input.value(); // 处理输入值 }); // 而不是使用通用的事件类型 let oninput = Callback::from(|e: Event| { let target = e.target().unwrap(); let input: HtmlInputElement = target.unchecked_into(); // 处理输入值 }); ``` ### 常见问题 #### 1. 如何阻止默认行为? ```rust let onsubmit = Callback::from(|e: SubmitEvent| { e.prevent_default(); // 处理表单提交 }); ``` #### 2. 如何阻止事件冒泡? ```rust let onclick = Callback::from(|e: MouseEvent| { e.stop_propagation(); // 处理点击 }); ``` #### 3. 如何获取事件目标? ```rust let onclick = Callback::from(|e: MouseEvent| { let target = e.target().unwrap(); let element: HtmlElement = target.unchecked_into(); // 使用元素 }); ``` ### 性能优化 #### 1. 使用 `use_callback` 避免重复创建 ```rust #[function_component(OptimizedHandler)] fn optimized_handler() -> Html { let count = use_state(|| 0); let onclick = use_callback(count.clone(), |count, _| { count.set(*count + 1); }); html! { <button onclick={onclick}> { "Click me: " }{ *count } </button> } } ``` #### 2. 事件委托 ```rust #[function_component(EventDelegation)] fn event_delegation() -> Html { let items = use_state(|| vec!["Item 1", "Item 2", "Item 3"]); let onclick = Callback::from(|e: MouseEvent| { let target = e.target().unwrap(); if let Some(element) = target.dyn_ref::<HtmlElement>() { if let Some(text) = element.text_content() { web_sys::console::log_1(&format!("Clicked: {}", text).into()); } } }); html! { <div onclick={onclick}> { items.iter().map(|item| { html! { <div key={item.to_string()}> { item } </div> } }).collect::<Html>() } </div> } } ```
服务端 · 2月19日 16:24
Yew 中如何处理异步数据,有哪些常用的异步处理模式?## Yew 中的异步数据处理 Yew 提供了强大的异步数据处理能力,通过 `wasm-bindgen-futures` 和 Rust 的 async/await 语法,可以优雅地处理异步操作。 ### 基础异步操作 #### 1. 使用 `send_future` 处理异步任务 ```rust use 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 ```rust use 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" ``` ```rust use 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"] } ``` ```rust use 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" ``` ```rust use 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. 实现重试逻辑 ```rust use 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 ```rust use 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. 实现简单的数据缓存 ```rust use 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> } } ``` ### 最佳实践 1. **错误处理**:始终处理异步操作中的错误 2. **加载状态**:提供清晰的加载反馈 3. **取消操作**:实现取消未完成请求的机制 4. **数据缓存**:合理使用缓存减少不必要的请求 5. **重试机制**:对关键操作实现重试逻辑 6. **类型安全**:使用 Rust 的类型系统确保数据安全
服务端 · 2月19日 16:23
Yew 中如何进行状态管理,有哪些不同的状态管理方案?## Yew 中的状态管理方案 Yew 提供了多种状态管理方案,从简单的组件内部状态到复杂的应用级状态管理。 ### 1. 组件内部状态 组件内部状态是最简单的状态管理方式,适用于单个组件的本地状态。 ```rust #[derive(PartialEq, Properties)] pub struct Props { #[prop_or_default] pub initial_value: i32, } pub struct Counter { count: i32, } pub enum Msg { Increment, Decrement, } impl Component for Counter { type Message = Msg; type Properties = Props; fn create(ctx: &Context<Self>) -> Self { Self { count: ctx.props().initial_value, } } fn update(&mut self, _ctx: &Context<Self>, msg: Self::Message) -> bool { match msg { Msg::Increment => { self.count += 1; true } Msg::Decrement => { self.count -= 1; true } } } fn view(&self, ctx: &Context<Self>) -> Html { html! { <div> <button onclick={ctx.link().callback(|_| Msg::Decrement)}>{ "-" }</button> <span>{ self.count }</span> <button onclick={ctx.link().callback(|_| Msg::Increment)}>{ "+" }</button> </div> } } } ``` ### 2. Props(属性传递) Props 用于从父组件向子组件传递数据,是单向数据流的一部分。 ```rust #[derive(PartialEq, Properties)] pub struct ChildProps { pub value: String, #[prop_or_default] pub on_change: Callback<String>, } pub struct ChildComponent; impl Component for ChildComponent { type Message = (); type Properties = ChildProps; fn view(&self, ctx: &Context<Self>) -> Html { html! { <div> <input value={ctx.props().value.clone()} onchange={ctx.props().on_change.reform(|e: Event| { let input: HtmlInputElement = e.target_unchecked_into(); input.value() })} /> </div> } } } ``` ### 3. Context API Context API 允许在组件树中共享状态,避免 props drilling。 ```rust // 定义 Context #[derive(Clone, PartialEq)] pub struct ThemeContext { pub is_dark: bool, pub toggle: Callback<()>, } // 提供者组件 pub struct ThemeProvider { is_dark: bool, } impl Component for ThemeProvider { type Message = Msg; type Properties = (); fn create(_ctx: &Context<Self>) -> Self { Self { is_dark: false } } fn view(&self, ctx: &Context<Self>) -> Html { let context = ThemeContext { is_dark: self.is_dark, toggle: ctx.link().callback(|_| Msg::ToggleTheme), }; html! { <ContextProvider<ThemeContext> context={context}> { ctx.props().children.clone() } </ContextProvider<ThemeContext>> } } } // 消费者组件 pub struct ThemedComponent; impl Component for ThemedComponent { type Message = (); type Properties = (); fn view(&self, ctx: &Context<Self>) -> Html { let theme = ctx.link().context::<ThemeContext>(|ctx| { (ctx.clone(), ()) }); if let Some(theme) = theme { let class = if theme.is_dark { "dark-theme" } else { "light-theme" }; html! { <div class={class}> { "Themed Content" } </div> } } else { html! { <div>{ "No theme context" }</div> } } } } ``` ### 4. 全局状态管理 对于复杂应用,可以使用全局状态管理库如 `yewdux` 或 `yew-state`。 #### 使用 yewdux 示例 ```rust use yewdux::prelude::*; // 定义 Store #[derive(Clone, PartialEq, Store)] pub struct AppState { pub user: Option<User>, pub notifications: Vec<Notification>, } impl Default for AppState { fn default() -> Self { Self { user: None, notifications: Vec::new(), } } } // 在组件中使用 pub struct UserComponent; impl Component for UserComponent { type Message = (); type Properties = (); fn view(&self, ctx: &Context<Self>) -> Html { let (state, dispatch) = use_store::<AppState>(); html! { <div> { state.user.as_ref().map(|user| { html! { <p>{ format!("Hello, {}!", user.name) }</p> } })} <button onclick={dispatch.reduce_callback(|state| { state.user = Some(User { name: "John Doe".to_string(), }); })}> { "Login" } </button> </div> } } } ``` ### 5. 异步状态管理 使用 `yew::services::fetch` 或 `reqwest` 处理异步数据获取。 ```rust pub struct AsyncData { data: Option<String>, loading: bool, error: Option<String>, } pub enum Msg { FetchData, DataReceived(String), FetchError(String), } impl Component for AsyncData { type Message = Msg; type Properties = (); fn create(_ctx: &Context<Self>) -> Self { Self { data: None, loading: false, error: None, } } fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool { match msg { Msg::FetchData => { self.loading = true; self.error = None; // 模拟异步请求 ctx.link().send_future(async { // 这里可以是实际的 API 调用 tokio::time::sleep(Duration::from_secs(1)).await; Msg::DataReceived("Fetched data".to_string()) }); true } Msg::DataReceived(data) => { self.data = Some(data); self.loading = false; true } Msg::FetchError(error) => { self.error = Some(error); self.loading = false; true } } } fn view(&self, ctx: &Context<Self>) -> Html { html! { <div> <button onclick={ctx.link().callback(|_| Msg::FetchData)}> { "Fetch Data" } </button> { if self.loading { html! { <p>{ "Loading..." }</p> } } else if let Some(ref error) = self.error { html! { <p class="error">{ error }</p> } } else if let Some(ref data) = self.data { html! { <p>{ data }</p> } } else { html! { <p>{ "No data" }</p> } }} </div> } } } ``` ### 状态管理选择指南 | 场景 | 推荐方案 | 复杂度 | |------|----------|--------| | 单个组件本地状态 | 组件内部状态 | 低 | | 父子组件通信 | Props + Callback | 低 | | 跨层级组件共享 | Context API | 中 | | 复杂应用状态 | yewdux / yew-state | 高 | | 异步数据处理 | Future + Services | 中 | ### 最佳实践 1. **从简单开始**:优先使用组件内部状态,只在必要时升级到更复杂的方案 2. **避免过度使用 Context**:Context 适合全局状态,不适合频繁变化的局部状态 3. **合理划分状态**:将状态放在逻辑上最接近使用它的组件中 4. **使用不可变数据**:利用 Rust 的类型系统确保状态更新的安全性
服务端 · 2月19日 16:23
Yew 中有哪些性能优化技巧,如何提升应用性能?## Yew 性能优化技巧 Yew 应用虽然基于 WebAssembly 具有天然的性能优势,但通过合理的优化策略可以进一步提升应用性能。 ### 组件渲染优化 #### 1. 使用 `should_render` 控制渲染 ```rust pub struct OptimizedComponent { value: i32, last_rendered_value: i32, } impl Component for OptimizedComponent { type Message = Msg; type Properties = Props; fn create(ctx: &Context<Self>) -> Self { Self { value: 0, last_rendered_value: 0, } } fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool { match msg { Msg::Increment => { self.value += 1; true } Msg::NoChange => { // 状态变化但不需要重新渲染 self.value += 1; false } } } fn should_render(&self, ctx: &Context<Self>) -> bool { // 只在值真正变化时才渲染 self.value != self.last_rendered_value } fn rendered(&mut self, ctx: &Context<Self>, _first_render: bool) { self.last_rendered_value = self.value; } fn view(&self, ctx: &Context<Self>) -> Html { html! { <div> <p>{ "Value: " }{ self.value }</p> </div> } } } ``` #### 2. 使用 `use_memo` 记忆化计算 ```rust #[function_component(MemoizedCalculation)] fn memoized_calculation() -> Html { let input1 = use_state(|| 0); let input2 = use_state(|| 0); // 只在依赖变化时重新计算 let expensive_result = use_memo( (*input1, *input2), |(a, b)| { web_sys::console::log_1(&"Computing expensive result".into()); // 模拟复杂计算 (0..1000000).fold(0i64, |acc, _| acc + 1) as i32 + a + b } ); html! { <div> <button onclick={Callback::from({ let input1 = input1.clone(); move |_| input1.set(*input1 + 1) })}> { "Increment Input 1" } </button> <button onclick={Callback::from({ let input2 = input2.clone(); move |_| input2.set(*input2 + 1) })}> { "Increment Input 2" } </button> <p>{ "Result: " }{ **expensive_result }</p> </div> } } ``` #### 3. 使用 `use_callback` 稳定回调引用 ```rust #[function_component(CallbackOptimization)] fn callback_optimization() -> Html { let count = use_state(|| 0); let expensive_operation = use_state(|| 0); // 稳定的回调,只在依赖变化时重新创建 let handle_click = use_callback( count.clone(), |count, _| { count.set(*count + 1); } ); // 另一个稳定的回调 let handle_expensive = use_callback( expensive_operation.clone(), |expensive_op, value: i32| { expensive_op.set(*expensive_op + value); } ); html! { <div> <ChildComponent on_click={handle_click.clone()} /> <button onclick={Callback::from(move |_| handle_expensive.emit(10))}> { "Expensive Operation" } </button> </div> } } #[derive(Properties, PartialEq)] pub struct ChildProps { pub on_click: Callback<()>, } #[function_component(ChildComponent)] fn child_component(props: &ChildProps) -> Html { html! { <button onclick={props.on_click.clone()}> { "Click Me" } </button> } } ``` ### 列表渲染优化 #### 1. 使用 `key` 属性 ```rust #[function_component(OptimizedList)] fn optimized_list() -> Html { let items = use_state(|| vec![ Item { id: 1, text: "Item 1".to_string() }, Item { id: 2, text: "Item 2".to_string() }, Item { id: 3, text: "Item 3".to_string() }, ]); let add_item = { let items = items.clone(); Callback::from(move |_| { let mut new_items = (*items).clone(); new_items.push(Item { id: new_items.len() as u32 + 1, text: format!("Item {}", new_items.len() + 1), }); items.set(new_items); }) }; html! { <div> <button onclick={add_item}>{ "Add Item" }</button> <ul> { items.iter().map(|item| { html! { <li key={item.id}> { &item.text } </li> } }).collect::<Html>() } </ul> </div> } } #[derive(Clone, PartialEq)] struct Item { id: u32, text: String, } ``` #### 2. 虚拟滚动 ```rust use yew::prelude::*; #[function_component(VirtualScroll)] fn virtual_scroll() -> Html { let items = use_state(|| (0..1000).collect::<Vec<i32>>()); let visible_range = use_state(|| (0, 20)); // (start, end) let item_height = 50; // 每个项目的像素高度 let container_height = 1000; // 容器高度 let onscroll = { let visible_range = visible_range.clone(); Callback::from(move |e: Event| { let element: HtmlElement = e.target_unchecked_into(); let scroll_top = element.scroll_top() as i32; let start = (scroll_top / item_height).max(0); let end = (start + (container_height / item_height) + 1).min(999); visible_range.set((start, end)); }) }; let (start, end) = *visible_range; let visible_items = &items[start..=end]; html! { <div style={format!("height: {}px; overflow-y: auto;", container_height)} onscroll={onscroll}> <div style={format!("height: {}px;", items.len() * item_height)}> <div style={format!("position: absolute; top: {}px;", start * item_height)}> { visible_items.iter().map(|item| { html! { <div key={*item} style={format!("height: {}px;", item_height)}> { format!("Item {}", item) } </div> } }).collect::<Html>() } </div> </div> </div> } } ``` ### 状态管理优化 #### 1. 减少不必要的状态更新 ```rust #[function_component(OptimizedState)] fn optimized_state() -> Html { let counter = use_state(|| 0); let display_value = use_state(|| 0); // 只在真正需要时更新显示值 let increment = { let counter = counter.clone(); let display_value = display_value.clone(); Callback::from(move |_| { counter.set(*counter + 1); // 只在特定条件下更新显示 if *counter % 10 == 0 { display_value.set(*counter); } }) }; html! { <div> <button onclick={increment}>{ "Increment" }</button> <p>{ "Counter: " }{ *counter }</p> <p>{ "Display: " }{ *display_value }</p> </div> } } ``` #### 2. 使用 `use_reducer` 优化复杂状态 ```rust #[derive(Clone, PartialEq)] pub enum AppStateAction { Increment, Decrement, SetValue(i32), Reset, } fn app_state_reducer(state: i32, action: AppStateAction) -> i32 { match action { AppStateAction::Increment => state + 1, AppStateAction::Decrement => state - 1, AppStateAction::SetValue(value) => value, AppStateAction::Reset => 0, } } #[function_component(ReducerOptimization)] fn reducer_optimization() -> Html { let (state, dispatch) = use_reducer(app_state_reducer, 0); html! { <div> <button onclick={dispatch.reform(|_| AppStateAction::Increment)}> { "+" } </button> <button onclick={dispatch.reform(|_| AppStateAction::Decrement)}> { "-" } </button> <button onclick={dispatch.reform(|_| AppStateAction::Reset)}> { "Reset" } </button> <p>{ "State: " }{ *state }</p> </div> } } ``` ### WebAssembly 优化 #### 1. 优化 Wasm 包大小 ```toml # Cargo.toml [profile.release] opt-level = "z" # 优化大小 lto = true # 链接时优化 codegen-units = 1 # 更好的优化 panic = "abort" # 减少恐慌处理代码 strip = true # 移除符号 [dependencies] wee_alloc = { version = "0.4", optional = true } [features] default = ["wee_alloc"] ``` ```rust // lib.rs #[cfg(feature = "wee_alloc")] #[global_allocator] static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT; ``` #### 2. 减少序列化开销 ```rust use wasm_bindgen::prelude::*; use js_sys::Uint32Array; // 不好的做法:频繁序列化 #[wasm_bindgen] pub fn process_data_serialized(data: JsValue) -> JsValue { let parsed: Vec<i32> = serde_wasm_bindgen::from_value(data).unwrap(); let result: Vec<i32> = parsed.iter().map(|x| x * 2).collect(); serde_wasm_bindgen::to_value(&result).unwrap() } // 好的做法:直接操作内存 #[wasm_bindgen] pub fn process_data_in_place(array: &Uint32Array) { for i in 0..array.length() { let value = array.get(i); array.set(i, value * 2); } } ``` ### 网络请求优化 #### 1. 请求去重 ```rust use std::collections::HashSet; use yew::prelude::*; #[function_component(RequestDeduplication)] fn request_deduplication() -> Html { let data = use_state(|| None::<String>); let loading = use_state(|| false); let pending_requests = use_mut_ref(|| HashSet::<String>::new()); let fetch_data = { let data = data.clone(); let loading = loading.clone(); let pending_requests = pending_requests.clone(); Callback::from(move |url: String| { // 检查是否已有相同请求在进行 if pending_requests.borrow().contains(&url) { return; } pending_requests.borrow_mut().insert(url.clone()); loading.set(true); spawn_local(async move { // 模拟 API 调用 gloo_timers::future::sleep(std::time::Duration::from_secs(1)).await; data.set(Some(format!("Data from {}", url))); loading.set(false); pending_requests.borrow_mut().remove(&url); }); }) }; html! { <div> <button onclick={Callback::from(move |_| fetch_data.emit("api1".to_string()))}> { "Fetch API 1" } </button> <button onclick={Callback::from(move |_| fetch_data.emit("api2".to_string()))}> { "Fetch API 2" } </button> { if *loading { html! { <p>{ "Loading..." }</p> } } else if let Some(ref value) = *data { html! { <p>{ value }</p> } }} </div> } } ``` #### 2. 批量请求 ```rust #[function_component(BatchRequests)] fn batch_requests() -> Html { let items = use_state(|| vec!["item1", "item2", "item3"]); let results = use_state(|| Vec::<String>::new()); let loading = use_state(|| false); let fetch_batch = { let items = items.clone(); let results = results.clone(); let loading = loading.clone(); Callback::from(move |_| { loading.set(true); spawn_local(async move { // 批量请求而不是逐个请求 let batch_data = (*items).clone(); // 模拟批量 API 调用 gloo_timers::future::sleep(std::time::Duration::from_secs(1)).await; let batch_results: Vec<String> = batch_data .iter() .map(|item| format!("Result for {}", item)) .collect(); results.set(batch_results); loading.set(false); }); }) }; html! { <div> <button onclick={fetch_batch} disabled={*loading}> { if *loading { "Loading..." } else { "Fetch Batch" } } </button> <ul> { results.iter().map(|result| { html! { <li>{ result }</li> } }).collect::<Html>() } </ul> </div> } } ``` ### 内存优化 #### 1. 使用 `use_ref` 避免不必要的状态更新 ```rust #[function_component(RefOptimization)] fn ref_optimization() -> Html { let counter = use_state(|| 0); let click_count = use_ref(|| 0); let increment = { let counter = counter.clone(); let click_count = click_count.clone(); Callback::from(move |_| { counter.set(*counter + 1); // 使用 ref 不会触发重新渲染 *click_count.borrow_mut() += 1; // 只在需要时记录 if *click_count.borrow() % 10 == 0 { web_sys::console::log_2( &"Total clicks:".into(), &(*click_count.borrow()).into() ); } }) }; html! { <div> <button onclick={increment}>{ "Increment" }</button> <p>{ "Counter: " }{ *counter }</p> </div> } } ``` #### 2. 延迟加载组件 ```rust #[function_component(LazyLoad)] fn lazy_load() -> Html { let show_component = use_state(|| false); html! { <div> <button onclick={Callback::from({ let show_component = show_component.clone(); move |_| show_component.set(!*show_component) })}> { if *show_component { "Hide" } else { "Show" } } </button> { if *show_component { html! { <HeavyComponent /> } } else { html! {} }} </div> } } #[function_component(HeavyComponent)] fn heavy_component() -> Html { // 这个组件只在需要时才渲染 html! { <div> <h1>{ "Heavy Component" }</h1> <p>{ "This component is only rendered when needed" }</p> </div> } } ``` ### 性能监控 #### 1. 使用 Performance API ```rust use web_sys::Performance; #[function_component(PerformanceMonitor)] fn performance_monitor() -> Html { let render_time = use_state(|| 0.0); let measure_render = { let render_time = render_time.clone(); Callback::from(move |_| { if let Ok(performance) = Performance::new() { let start = performance.now(); // 模拟渲染操作 let _ = std::hint::black_box(42); let end = performance.now(); render_time.set(end - start); } }) }; html! { <div> <button onclick={measure_render}>{ "Measure Render" }</button> <p>{ "Render time: " }{ *render_time }{ " ms" }</p> </div> } } ``` ### 最佳实践总结 1. **避免不必要的渲染**:使用 `should_render`、`use_memo`、`use_callback` 2. **优化列表渲染**:使用 `key` 属性、虚拟滚动 3. **减少状态更新**:只在必要时更新状态 4. **优化 Wasm 包大小**:使用适当的编译选项 5. **减少序列化开销**:直接操作内存而非序列化 6. **批量处理请求**:合并多个请求为批量请求 7. **使用 ref 避免渲染**:对于不需要触发渲染的数据使用 `use_ref` 8. **延迟加载**:只在需要时加载和渲染组件 9. **监控性能**:使用 Performance API 监控应用性能 10. **代码分割**:将代码分割成更小的块以提高加载速度
服务端 · 2月19日 16:22
Yew 中如何进行测试,有哪些测试策略和工具?## Yew 测试策略与实践 Yew 应用测试需要考虑 WebAssembly 环境和 Rust 的测试框架,本文介绍完整的测试策略。 ### 单元测试 #### 1. 组件单元测试 ```rust use yew::prelude::*; use wasm_bindgen_test::*; wasm_bindgen_test_configure!(run_in_browser); #[function_component(SimpleComponent)] fn simple_component() -> Html { let count = use_state(|| 0); let increment = { let count = count.clone(); Callback::from(move |_| count.set(*count + 1)) }; html! { <div> <button onclick={increment}>{ "Increment" }</button> <span>{ *count }</span> </div> } } #[wasm_bindgen_test] async fn test_simple_component() { let result = yew::ServerRenderer::<SimpleComponent>::render().await; assert!(result.contains("Increment")); assert!(result.contains("0")); } ``` #### 2. Hook 单元测试 ```rust use yew::prelude::*; use yew_hooks::prelude::*; #[wasm_bindgen_test] async fn test_use_counter() { let result = yew::ServerRenderer::<CounterComponent>::render().await; assert!(result.contains("Counter: 0")); } #[function_component(CounterComponent)] fn counter_component() -> Html { let (count, set_count) = use_counter(0); html! { <div> <p>{ format!("Counter: {}", count) }</p> <button onclick={Callback::from(move |_| set_count(*count + 1))}> { "Increment" } </button> </div> } } ``` ### 集成测试 #### 1. 组件交互测试 ```rust use yew::prelude::*; use wasm_bindgen_test::*; #[function_component(FormComponent)] fn form_component() -> Html { let username = use_state(|| String::new()); let submitted = use_state(|| false); let onsubmit = { let username = username.clone(); let submitted = submitted.clone(); Callback::from(move |e: SubmitEvent| { e.prevent_default(); if !username.is_empty() { submitted.set(true); } }) }; let oninput = { let username = username.clone(); Callback::from(move |e: InputEvent| { let input: HtmlInputElement = e.target_unchecked_into(); username.set(input.value()); }) }; html! { <form onsubmit={onsubmit}> <input type="text" value={(*username).clone()} oninput={oninput} placeholder="Username" /> <button type="submit">{ "Submit" }</button> { if *submitted { html! { <p>{ "Form submitted!" }</p> } } else { html! {} }} </form> } } #[wasm_bindgen_test] async fn test_form_submission() { let result = yew::ServerRenderer::<FormComponent>::render().await; assert!(result.contains("Username")); assert!(result.contains("Submit")); } ``` #### 2. 异步组件测试 ```rust use yew::prelude::*; use wasm_bindgen_futures::spawn_local; #[function_component(AsyncComponent)] fn async_component() -> 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_millis(100)).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> } } #[wasm_bindgen_test] async fn test_async_component() { let result = yew::ServerRenderer::<AsyncComponent>::render().await; assert!(result.contains("Fetch Data")); assert!(result.contains("Loading...")); } ``` ### 端到端测试 #### 1. 使用 `yew-app` 进行 E2E 测试 ```rust use yew::prelude::*; use wasm_bindgen_test::*; #[function_component(App)] fn app() -> Html { let count = use_state(|| 0); let increment = { let count = count.clone(); Callback::from(move |_| count.set(*count + 1)) }; html! { <div id="app"> <h1>{ "Counter App" }</h1> <button id="increment-btn" onclick={increment}> { "Increment" } </button> <p id="count-display">{ *count }</p> </div> } } #[wasm_bindgen_test] async fn test_counter_increment() { // 渲染应用 yew::Renderer::<App>::with_root( gloo::utils::document().get_element_by_id("output").unwrap(), ).render(); // 模拟点击 let button = gloo::utils::document() .get_element_by_id("increment-btn") .unwrap(); let event = web_sys::MouseEvent::new("click").unwrap(); button.dispatch_event(&event).unwrap(); // 验证结果 let display = gloo::utils::document() .get_element_by_id("count-display") .unwrap(); assert_eq!(display.text_content().unwrap(), "1"); } ``` ### Mock 和 Stub #### 1. Mock HTTP 请求 ```rust use yew::prelude::*; use gloo_net::http::Request; #[function_component(ApiComponent)] fn api_component() -> Html { let user = use_state(|| None::<String>); let loading = use_state(|| false); let fetch_user = { let user = user.clone(); let loading = loading.clone(); Callback::from(move |_| { loading.set(true); spawn_local(async move { match Request::get("/api/user").send().await { Ok(response) => { if response.ok() { if let Ok(data) = response.text().await { user.set(Some(data)); } } } Err(_) => {} } loading.set(false); }); }) }; html! { <div> <button onclick={fetch_user} disabled={*loading}> { if *loading { "Loading..." } else { "Fetch User" } } </button> { if let Some(ref u) = *user { html! { <p>{ u }</p> } } else { html! {} }} </div> } } #[cfg(test)] mod tests { use super::*; use wasm_bindgen_test::*; #[wasm_bindgen_test] async fn test_api_component_with_mock() { // 在测试中,可以使用 mock 服务器 // 这里简化为测试组件渲染 let result = yew::ServerRenderer::<ApiComponent>::render().await; assert!(result.contains("Fetch User")); } } ``` #### 2. Mock WebSocket ```rust use yew::prelude::*; use gloo_net::websocket::Message; #[function_component(WebSocketComponent)] fn websocket_component() -> Html { let messages = use_state(|| Vec::<String>::new()); let connected = use_state(|| false); let connect = { let messages = messages.clone(); let connected = connected.clone(); Callback::from(move |_| { let ws = gloo_net::websocket::futures::WebSocket::open("ws://localhost:8080"); if let Ok(ws) = ws { let onmessage = { let messages = messages.clone(); Callback::from(move |msg: Message| { if let Message::Text(text) = msg { let mut msgs = (*messages).clone(); msgs.push(text); messages.set(msgs); } }) }; ws.set_onmessage(onmessage); connected.set(true); } }) }; html! { <div> <button onclick={connect} disabled={*connected}> { if *connected { "Connected" } else { "Connect" } } </button> <ul> { messages.iter().map(|msg| { html! { <li>{ msg }</li> } }).collect::<Html>() } </ul> </div> } } ``` ### 测试覆盖率 #### 1. 使用 `tarpaulin` 测试覆盖率 ```bash # 安装 tarpaulin cargo install cargo-tarpaulin # 运行覆盖率测试 cargo tarpaulin --out Html --output-dir ./coverage # 针对 Wasm 测试 wasm-pack test --firefox --headless ``` #### 2. 配置测试覆盖率 ```toml # Cargo.toml [package] name = "yew-app" version = "0.1.0" edition = "2021" [dev-dependencies] wasm-bindgen-test = "0.3" gloo = "0.8" web-sys = { version = "0.3", features = ["console"] } ``` ### 性能测试 #### 1. 组件渲染性能测试 ```rust use yew::prelude::*; use web_sys::Performance; #[function_component(PerformanceTestComponent)] fn performance_test_component() -> Html { let render_time = use_state(|| 0.0); let measure_render = { let render_time = render_time.clone(); Callback::from(move |_| { if let Ok(performance) = Performance::new() { let start = performance.now(); // 模拟渲染操作 let _ = std::hint::black_box(42); let end = performance.now(); render_time.set(end - start); } }) }; html! { <div> <button onclick={measure_render}>{ "Measure Render" }</button> <p>{ "Render time: " }{ *render_time }{ " ms" }</p> </div> } } #[wasm_bindgen_test] async fn test_render_performance() { let result = yew::ServerRenderer::<PerformanceTestComponent>::render().await; assert!(result.contains("Measure Render")); assert!(result.contains("Render time:")); } ``` ### 测试最佳实践 #### 1. 测试组织 ```rust // tests/common/mod.rs pub mod common { use yew::prelude::*; pub fn setup_test_environment() { // 设置测试环境 } pub fn cleanup_test_environment() { // 清理测试环境 } } // tests/components/button_test.rs mod button_test { use super::common::*; use wasm_bindgen_test::*; #[wasm_bindgen_test] async fn test_button_render() { setup_test_environment(); let result = yew::ServerRenderer::<ButtonComponent>::render().await; assert!(result.contains("Click me")); cleanup_test_environment(); } } ``` #### 2. 测试数据管理 ```rust // tests/fixtures/mod.rs pub mod fixtures { pub fn get_test_user() -> User { User { id: 1, name: "Test User".to_string(), email: "test@example.com".to_string(), } } pub fn get_test_posts() -> Vec<Post> { vec![ Post { id: 1, title: "Test Post 1".to_string(), body: "Test content".to_string(), }, Post { id: 2, title: "Test Post 2".to_string(), body: "More test content".to_string(), }, ] } } ``` ### 持续集成 #### 1. GitHub Actions 配置 ```yaml # .github/workflows/test.yml name: Test on: push: branches: [ main ] pull_request: branches: [ main ] jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Install Rust uses: actions-rs/toolchain@v1 with: toolchain: stable override: true - name: Install wasm-pack run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh - name: Run tests run: | cargo test --all wasm-pack test --firefox --headless - name: Run coverage run: cargo tarpaulin --out Xml ``` #### 2. GitLab CI 配置 ```yaml # .gitlab-ci.yml stages: - test test: stage: test image: rust:latest before_script: - curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh script: - cargo test --all - wasm-pack test --firefox --headless - cargo tarpaulin --out Xml coverage: '/^\d+.\d+% coverage/' artifacts: reports: coverage_report: coverage_format: cobertura path: cobertura.xml ``` ### 调试测试 #### 1. 使用浏览器开发者工具 ```rust use web_sys::console; #[wasm_bindgen_test] async fn test_with_debugging() { console::log_1(&"Starting test".into()); let result = yew::ServerRenderer::<TestComponent>::render().await; console::log_2( &"Render result:".into(), &result.into() ); assert!(result.contains("Expected content")); } ``` #### 2. 使用 `console_error_panic_hook` ```rust use console_error_panic_hook; #[wasm_bindgen_test] async fn test_with_panic_hook() { console_error_panic_hook::set_once(); // 测试代码 let result = yew::ServerRenderer::<TestComponent>::render().await; assert!(result.contains("Expected content")); } ``` ### 测试工具推荐 1. **wasm-bindgen-test**: Wasm 环境测试框架 2. **yew-app**: Yew 应用测试工具 3. **gloo**: Web API 绑定和工具 4. **cargo-tarpaulin**: 代码覆盖率工具 5. **web-sys**: Web API 绑定 ### 总结 Yew 测试需要结合 Rust 的测试框架和 WebAssembly 的特性。通过合理的测试策略、Mock 技术和持续集成配置,可以构建高质量的 Yew 应用。
服务端 · 2月19日 16:22
Yew 组件生命周期包含哪些方法,每个方法的作用是什么?## Yew 组件生命周期详解 Yew 组件的生命周期与 React 类似,但使用 Rust 的 trait 系统来实现。理解组件生命周期对于构建高效的应用至关重要。 ### 主要生命周期方法 #### 1. `create()` - **时机**:组件首次创建时调用 - **用途**:初始化组件状态 - **返回值**:返回组件的初始状态 - **示例**: ```rust impl Component for MyComponent { type Message = Msg; type Properties = Props; fn create(ctx: &Context<Self>) -> Self { Self { counter: 0, value: String::new(), } } } ``` #### 2. `view()` - **时机**:每次状态或属性变化时调用 - **用途**:渲染组件的 UI - **返回值**:返回 `Html` 类型 - **示例**: ```rust fn view(&self, ctx: &Context<Self>) -> Html { html! { <div class="container"> <h1>{ "Counter: " }{ self.counter }</h1> <button onclick={ctx.link().callback(|_| Msg::Increment)}> { "Increment" } </button> </div> } } ``` #### 3. `update()` - **时机**:接收到消息时调用 - **用途**:处理状态更新 - **返回值**:返回 `bool`,表示是否需要重新渲染 - **示例**: ```rust fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool { match msg { Msg::Increment => { self.counter += 1; true // 需要重新渲染 } Msg::NoOp => false, // 不需要重新渲染 } } ``` #### 4. `changed()` - **时机**:组件的 props 发生变化时调用 - **用途**:决定是否根据新的 props 更新组件 - **返回值**:返回 `bool`,表示是否需要重新渲染 - **示例**: ```rust fn changed(&mut self, ctx: &Context<Self>, _old_props: &Self::Properties) -> bool { // 只有当重要属性变化时才重新渲染 true } ``` #### 5. `rendered()` - **时机**:组件渲染到 DOM 后调用 - **用途**:执行 DOM 操作、初始化第三方库等 - **示例**: ```rust fn rendered(&mut self, ctx: &Context<Self>, first_render: bool) { if first_render { // 首次渲染后的初始化操作 } } ``` #### 6. `destroy()` - **时机**:组件从 DOM 中移除时调用 - **用途**:清理资源、取消订阅等 - **示例**: ```rust fn destroy(&mut self, ctx: &Context<Self>) { // 清理定时器、取消网络请求等 } } ``` ### 生命周期流程图 ``` 创建组件 ↓ create() → 初始化状态 ↓ view() → 渲染 UI ↓ rendered() → DOM 操作 ↓ [接收消息/props 变化] ↓ update() / changed() → 更新状态 ↓ view() → 重新渲染 ↓ rendered() → DOM 更新 ↓ [组件卸载] ↓ destroy() → 清理资源 ``` ### 最佳实践 1. **避免在 `view()` 中执行复杂计算**:将计算逻辑放在 `update()` 或单独的方法中 2. **合理使用 `changed()`**:避免不必要的重新渲染 3. **及时清理资源**:在 `destroy()` 中清理定时器、事件监听器等 4. **使用 `rendered()` 处理 DOM 操作**:确保 DOM 已经渲染完成
服务端 · 2月19日 16:22
Yew 中如何实现路由管理,支持哪些路由功能?## Yew 路由管理详解 Yew 提供了强大的路由管理功能,支持客户端路由、嵌套路由、路由守卫等高级特性。 ### 基础路由配置 #### 1. 安装依赖 ```toml [dependencies] yew = { version = "0.21", features = ["csr"] } yew-router = { version = "0.18" } ``` #### 2. 定义路由 ```rust use yew::prelude::*; use yew_router::prelude::*; #[derive(Clone, Routable, PartialEq)] enum Route { #[at("/")] Home, #[at("/about")] About, #[at("/contact")] Contact, #[at("/users/:id")] User { id: u32 }, #[at("/posts/:post_id/comments/:comment_id")] PostComment { post_id: u32, comment_id: u32 }, #[not_found] #[at("/404")] NotFound, } ``` #### 3. 创建路由组件 ```rust #[function_component(Home)] fn home() -> Html { html! { <div> <h1>{ "Welcome Home" }</h1> <p>{ "This is the home page" }</p> </div> } } #[function_component(About)] fn about() -> Html { html! { <div> <h1>{ "About Us" }</h1> <p>{ "Learn more about our company" }</p> </div> } } #[function_component(Contact)] fn contact() -> Html { html! { <div> <h1>{ "Contact Us" }</h1> <p>{ "Get in touch with us" }</p> </div> } } #[derive(Properties, PartialEq)] pub struct UserProps { pub id: u32, } #[function_component(User)] fn user(props: &UserProps) -> Html { html! { <div> <h1>{ format!("User Profile: {}", props.id) }</h1> <p>{ "View user details" }</p> </div> } } #[derive(Properties, PartialEq)] pub struct PostCommentProps { pub post_id: u32, pub comment_id: u32, } #[function_component(PostComment)] fn post_comment(props: &PostCommentProps) -> Html { html! { <div> <h1>{ format!("Post {} - Comment {}", props.post_id, props.comment_id) }</h1> <p>{ "View comment details" }</p> </div> } } #[function_component(NotFound)] fn not_found() -> Html { html! { <div> <h1>{ "404 - Page Not Found" }</h1> <p>{ "The page you're looking for doesn't exist" }</p> </div> } } ``` #### 4. 设置路由切换 ```rust #[function_component(Switch)] fn switch() -> Html { let route = use_route::<Route>().unwrap(); match route { Route::Home => html! { <Home /> }, Route::About => html! { <About /> }, Route::Contact => html! { <Contact /> }, Route::User { id } => html! { <User id={*id} /> }, Route::PostComment { post_id, comment_id } => { html! { <PostComment post_id={*post_id} comment_id={*comment_id} /> } } Route::NotFound => html! { <NotFound /> }, } } ``` #### 5. 创建应用组件 ```rust #[function_component(App)] fn app() -> Html { html! { <BrowserRouter> <div class="app"> <nav class="navbar"> <Link<Route> to={Route::Home}>{ "Home" }</Link<Route>> <Link<Route> to={Route::About}>{ "About" }</Link<Route>> <Link<Route> to={Route::Contact}>{ "Contact" }</Link<Route>> <Link<Route> to={Route::User { id: 1 }}>{ "User 1" }</Link<Route>> </nav> <main class="content"> <Switch /> </main> </div> </BrowserRouter> } } fn main() { yew::Renderer::<App>::new().render(); } ``` ### 高级路由功能 #### 1. 嵌套路由 ```rust #[derive(Clone, Routable, PartialEq)] enum Route { #[at("/")] Home, #[at("/dashboard")] Dashboard, #[at("/dashboard/settings")] DashboardSettings, #[at("/dashboard/profile")] DashboardProfile, } #[function_component(Dashboard)] fn dashboard() -> Html { html! { <div class="dashboard"> <aside class="sidebar"> <Link<Route> to={Route::Dashboard}>{ "Overview" }</Link<Route>> <Link<Route> to={Route::DashboardSettings}>{ "Settings" }</Link<Route>> <Link<Route> to={Route::DashboardProfile}>{ "Profile" }</Link<Route>> </aside> <main class="dashboard-content"> <Switch /> </main> </div> } } ``` #### 2. 路由参数 ```rust #[derive(Clone, Routable, PartialEq)] enum Route { #[at("/products/:category/:id")] Product { category: String, id: u32 }, } #[function_component(Product)] fn product() -> Html { let route = use_route::<Route>().unwrap(); if let Route::Product { category, id } = route { html! { <div> <h1>{ format!("{} - Product {}", category, id) }</h1> </div> } } else { html! { <div>{ "Invalid route" }</div> } } } ``` #### 3. 查询参数 ```rust use yew_router::hooks::use_location; #[function_component(Search)] fn search() -> Html { let location = use_location().unwrap(); let query_params = location.query::<SearchParams>().unwrap(); html! { <div> <h1>{ "Search Results" }</h1> <p>{ format!("Query: {}", query_params.q) }</p> <p>{ format!("Page: {}", query_params.page) }</p> </div> } } #[derive(Deserialize, Clone, PartialEq)] struct SearchParams { q: String, #[serde(default = "default_page")] page: u32, } fn default_page() -> u32 { 1 } ``` #### 4. 路由守卫 ```rust use yew_router::hooks::use_navigator; #[function_component(ProtectedRoute)] fn protected_route() -> Html { let is_authenticated = use_state(|| false); let navigator = use_navigator().unwrap(); // 检查认证状态 if !*is_authenticated { // 重定向到登录页面 navigator.push(&Route::Login); return html! { <div>{ "Redirecting..." }</div> }; } html! { <div> <h1>{ "Protected Content" }</h1> <p>{ "You are authenticated" }</p> </div> } } ``` #### 5. 编程式导航 ```rust #[function_component(NavigationExample)] fn navigation_example() -> Html { let navigator = use_navigator().unwrap(); let go_home = { let navigator = navigator.clone(); Callback::from(move |_| navigator.push(&Route::Home)) }; let go_back = { let navigator = navigator.clone(); Callback::from(move |_| navigator.back()) }; let go_forward = { let navigator = navigator.clone(); Callback::from(move |_| navigator.forward()) }; let replace_route = { let navigator = navigator.clone(); Callback::from(move |_| navigator.replace(&Route::About)) }; html! { <div> <button onclick={go_home}>{ "Go Home" }</button> <button onclick={go_back}>{ "Go Back" }</button> <button onclick={go_forward}>{ "Go Forward" }</button> <button onclick={replace_route}>{ "Replace Route" }</button> </div> } } ``` ### 路由过渡动画 ```rust use gloo::timers::callback::Timeout; use std::rc::Rc; use std::cell::RefCell; #[function_component(RouteTransition)] fn route_transition() -> Html { let route = use_route::<Route>().unwrap(); let is_transitioning = use_state(|| false); let current_route = use_state(|| route.clone()); // 监听路由变化 let route_effect = { let is_transitioning = is_transitioning.clone(); let current_route = current_route.clone(); use_effect(move || { if *current_route != route { is_transitioning.set(true); // 模拟过渡动画 let timeout = Timeout::new(300, move || { is_transitioning.set(false); }); timeout.forget(); } || {} }) }; let class = if *is_transitioning { "route-transitioning" } else { "route-active" }; html! { <div class={class}> <Switch /> </div> } } ``` ### 路由懒加载 ```rust use yew_router::Routable; use std::rc::Rc; #[derive(Clone, Routable, PartialEq)] enum Route { #[at("/")] Home, #[at("/lazy")] LazyLoaded, } // 懒加载组件 #[function_component(LazyLoaded)] fn lazy_loaded() -> Html { html! { <div> <h1>{ "Lazy Loaded Component" }</h1> <p>{ "This component was loaded on demand" }</p> </div> } } // 使用条件渲染实现懒加载 #[function_component(LazySwitch)] fn lazy_switch() -> Html { let route = use_route::<Route>().unwrap(); let lazy_component_loaded = use_state(|| false); match route { Route::LazyLoaded => { if !*lazy_component_loaded { lazy_component_loaded.set(true); } html! { <LazyLoaded /> } } _ => html! { <Switch /> }, } } ``` ### 最佳实践 #### 1. 路由组织 ```rust // 将路由定义在单独的模块中 // src/routes.rs #[derive(Clone, Routable, PartialEq)] pub enum Route { #[at("/")] Home, #[at("/about")] About, // ... 其他路由 } // 将组件定义在单独的模块中 // src/components/home.rs // src/components/about.rs // ... ``` #### 2. 路由守卫复用 ```rust // src/guards/auth_guard.rs pub fn use_auth_guard() -> bool { let is_authenticated = use_state(|| false); let navigator = use_navigator().unwrap(); if !*is_authenticated { navigator.push(&Route::Login); false } else { true } } // 使用守卫 #[function_component(ProtectedPage)] fn protected_page() -> Html { if !use_auth_guard() { return html! { <div>{ "Redirecting..." }</div> }; } html! { <div> <h1>{ "Protected Content" }</h1> </div> } } ``` #### 3. 路由错误处理 ```rust #[function_component(ErrorBoundary)] fn error_boundary() -> Html { let route = use_route::<Route>().unwrap(); let error = use_state(|| None::<String>); // 错误处理逻辑 if let Some(ref err) = *error { html! { <div class="error-boundary"> <h1>{ "Something went wrong" }</h1> <p>{ err }</p> <Link<Route> to={Route::Home}>{ "Go Home" }</Link<Route>> </div> } } else { html! { <Switch /> } } } ``` ### 性能优化 #### 1. 路由预加载 ```rust #[function_component(RoutePreloader)] fn route_preloader() -> Html { let navigator = use_navigator().unwrap(); let preload_route = { let navigator = navigator.clone(); Callback::from(move |_| { // 预加载路由资源 let _ = navigator.push(&Route::LazyLoaded); let _ = navigator.back(); }) }; html! { <div> <button onclick={preload_route}>{ "Preload Route" }</button> </div> } } ``` #### 2. 路由缓存 ```rust #[function_component(RouteCache)] fn route_cache() -> Html { let route_cache = use_mut_ref(|| std::collections::HashMap::new()); let route = use_route::<Route>().unwrap(); // 检查缓存 if let Some(cached) = route_cache.borrow().get(&route) { return html! { <div>{ cached.clone() }</div> }; } // 渲染并缓存 let content = html! { <Switch /> }; route_cache.borrow_mut().insert(route.clone(), content.clone()); content } ```
服务端 · 2月19日 16:21