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

面试题手册

Yew 和 React 有哪些主要区别,如何选择适合的框架?

Yew 与 React 的对比分析Yew 和 React 都是现代前端框架,但它们在语言、运行时和性能方面有显著差异。核心差异对比| 特性 | Yew | React ||------|-----|-------|| 编程语言 | Rust | JavaScript/TypeScript || 运行时 | WebAssembly (Wasm) | JavaScript || 类型系统 | 静态强类型 | 动态类型(可选 TypeScript) || 编译时 | 编译到 Wasm | JIT 编译 || 性能 | 接近原生 | 依赖 JS 引擎优化 || 包大小 | 较大(包含 Wasm 运行时) | 较小 || 学习曲线 | 较陡(需要学习 Rust) | 较平缓 || 生态系统 | 较新,较小 | 成熟,庞大 || 开发工具 | 有限 | 丰富(DevTools, ESLint 等) |代码示例对比组件定义Yew:use yew::prelude::*;#[function_component(HelloWorld)]fn hello_world() -> Html { html! { <div class="greeting"> <h1>{ "Hello, World!" }</h1> </div> }}React:function HelloWorld() { return ( <div className="greeting"> <h1>Hello, World!</h1> </div> );}状态管理Yew:#[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:function Counter() { const [counter, setCounter] = useState(0); return ( <div> <button onClick={() => setCounter(counter + 1)}>Increment</button> <p>{counter}</p> </div> );}Props 传递Yew:#[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: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 的场景高性能要求:需要处理大量计算或数据的应用安全敏感:金融、医疗等对安全性要求高的领域Rust 团队:团队熟悉 Rust 语言长期维护:大型项目需要长期维护和稳定性WebAssembly 优势:需要充分利用 Wasm 的能力选择 React 的场景快速原型:需要快速开发和迭代团队熟悉 JS:团队主要使用 JavaScript/TypeScript生态系统需求:需要使用丰富的第三方库开发效率:优先考虑开发速度而非运行时性能传统 Web 应用:典型的 CRUD 应用迁移考虑从 React 迁移到 Yew挑战:需要学习 Rust 语言重写所有组件和逻辑适配不同的 API 和模式生态系统差异收益:更好的性能类型安全更少的运行时错误从 Yew 迁移到 React挑战:失去类型安全(除非使用 TypeScript)性能可能下降需要适应不同的开发模式收益:更快的开发速度更丰富的生态系统更好的开发工具未来展望Yew:WebAssembly 生态系统持续发展Rust 前端社区增长工具链和调试工具改进React:Server Components 和 RSC更好的性能优化继续主导前端市场结论选择 Yew 还是 React 取决于项目需求、团队技能和长期目标。Yew 提供了更好的性能和类型安全,但需要更高的学习成本;React 提供了更好的开发体验和生态系统,但可能在性能和类型安全方面有所妥协。
阅读 0·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:[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:<!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 函数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 函数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 中使用:// 调用简单函数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()); // 153. 复杂数据类型传递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. 减少序列化开销// 不好的做法:频繁序列化#[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 Workersuse 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.jsself.importScripts('wasm/yew_app.js');wasm.run_worker();3. 优化 Wasm 包大小# Cargo.toml[profile.release]opt-level = "z" # 优化大小lto = true # 链接时优化codegen-units = 1panic = "abort" # 减少恐慌处理代码[dependencies]wee_alloc = { version = "0.4", optional = true }[features]default = ["wee_alloc"]// lib.rs#[cfg(feature = "wee_alloc")]#[global_allocator]static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;调试 Wasm 代码1. 使用浏览器开发工具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 测试# 运行测试wasm-pack test --firefox --headless# 运行特定测试wasm-pack test --firefox --headless test_name实际应用场景1. 图像处理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. 数据加密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. 复杂计算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}最佳实践最小化 Wasm-JS 边界:减少跨边界调用,批量处理数据使用类型安全的绑定:利用 wasm-bindgen 的类型系统优化包大小:使用 wee_alloc、LTO 和适当的优化级别错误处理:使用 Result 类型正确处理错误内存管理:注意 Wasm 的线性内存限制测试:使用 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 的类型系统,可以构建高性能、安全的前端应用。
阅读 0·2月19日 16:24

Yew 中如何使用 Hooks,有哪些常用的 Hook 函数?

Yew 中的 Hooks 使用详解Yew 从 0.19 版本开始支持 Hooks API,提供了类似 React Hooks 的功能,使组件编写更加简洁和函数式。基础 Hooks1. use_stateuse_state 用于在函数组件中管理状态,类似于 React 的 useState。#[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_reduceruse_reducer 用于复杂的状态逻辑,类似于 React 的 useReducer。#[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_effectuse_effect 用于处理副作用,类似于 React 的 useEffect。#[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_refuse_ref 用于存储可变值,类似于 React 的 useRef。#[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> }}高级 Hooks5. use_contextuse_context 用于消费 Context,类似于 React 的 useContext。#[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_memouse_memo 用于记忆化计算结果,类似于 React 的 useMemo。#[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_callbackuse_callback 用于记忆化回调函数,类似于 React 的 useCallback。#[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 是复用逻辑的强大方式。// 自定义 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 时必须遵循以下规则:只在函数组件或自定义 Hook 中调用 Hooks // ✅ 正确 #[function_component(MyComponent)] fn my_component() -> Html { let count = use_state(|| 0); html! { <div>{ *count }</div> } } // ❌ 错误 fn regular_function() { let count = use_state(|| 0); // 不能在普通函数中调用 }只在顶层调用 Hooks // ✅ 正确 #[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> } }保持 Hooks 调用顺序一致 // ✅ 正确:每次渲染都按相同顺序调用 #[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 避免重复计算let expensive_result = use_memo( (*input1, *input2), |(a, b)| { // 只在 input1 或 input2 变化时重新计算 complex_calculation(a, b) });2. 使用 use_callback 稳定回调引用let handle_click = use_callback( dependency.clone(), |dep, _| { // 只在 dependency 变化时创建新的回调 do_something(dep) });3. 合理使用 use_ref 避免不必要的状态更新let ref_count = use_ref(|| 0);// 不会触发重新渲染*ref_count.borrow_mut() += 1;
阅读 0·2月19日 16:24

Yew 中如何处理事件,支持哪些类型的事件?

Yew 中的事件处理机制Yew 提供了强大的事件处理机制,类似于 React,但使用 Rust 的类型系统来确保类型安全。基本事件处理1. 点击事件#[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. 输入事件#[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. 表单提交事件#[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. 事件冒泡和捕获#[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. 自定义事件#[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. 防抖和节流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 简化代码// 简单事件处理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. 避免不必要的克隆// 不好的做法:每次渲染都克隆let onclick = Callback::from({ let value = value.clone(); move |_| { // 使用 value }});// 好的做法:使用 Rc 或引用let onclick = Callback::from({ let value = Rc::clone(&value); move |_| { // 使用 value }});3. 类型安全的事件处理// 使用具体的事件类型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. 如何阻止默认行为?let onsubmit = Callback::from(|e: SubmitEvent| { e.prevent_default(); // 处理表单提交});2. 如何阻止事件冒泡?let onclick = Callback::from(|e: MouseEvent| { e.stop_propagation(); // 处理点击});3. 如何获取事件目标?let onclick = Callback::from(|e: MouseEvent| { let target = e.target().unwrap(); let element: HtmlElement = target.unchecked_into(); // 使用元素});性能优化1. 使用 use_callback 避免重复创建#[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. 事件委托#[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> }}
阅读 0·2月19日 16:24

Yew 中如何处理异步数据,有哪些常用的异步处理模式?

Yew 中的异步数据处理Yew 提供了强大的异步数据处理能力,通过 wasm-bindgen-futures 和 Rust 的 async/await 语法,可以优雅地处理异步操作。基础异步操作1. 使用 send_future 处理异步任务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 Hookuse 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 请求[dependencies]reqwest = { version = "0.11", features = ["json"] }serde = { version = "1.0", features = ["derive"] }serde_json = "1.0"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 请求[dependencies]gloo-net = { version = "0.4", features = ["http", "fetch"] }serde = { version = "1.0", features = ["derive"] }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[dependencies]gloo-net = { version = "0.4", features = ["websocket"] }serde = { version = "1.0", features = ["derive"] }serde_json = "1.0"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. 实现重试逻辑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 Hookuse 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. 实现简单的数据缓存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> }}最佳实践错误处理:始终处理异步操作中的错误加载状态:提供清晰的加载反馈取消操作:实现取消未完成请求的机制数据缓存:合理使用缓存减少不必要的请求重试机制:对关键操作实现重试逻辑类型安全:使用 Rust 的类型系统确保数据安全
阅读 0·2月19日 16:23

Yew 中如何进行状态管理,有哪些不同的状态管理方案?

Yew 中的状态管理方案Yew 提供了多种状态管理方案,从简单的组件内部状态到复杂的应用级状态管理。1. 组件内部状态组件内部状态是最简单的状态管理方式,适用于单个组件的本地状态。#[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 用于从父组件向子组件传递数据,是单向数据流的一部分。#[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 APIContext API 允许在组件树中共享状态,避免 props drilling。// 定义 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 示例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 处理异步数据获取。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 | 中 |最佳实践从简单开始:优先使用组件内部状态,只在必要时升级到更复杂的方案避免过度使用 Context:Context 适合全局状态,不适合频繁变化的局部状态合理划分状态:将状态放在逻辑上最接近使用它的组件中使用不可变数据:利用 Rust 的类型系统确保状态更新的安全性
阅读 0·2月19日 16:23

Yew 中有哪些性能优化技巧,如何提升应用性能?

Yew 性能优化技巧Yew 应用虽然基于 WebAssembly 具有天然的性能优势,但通过合理的优化策略可以进一步提升应用性能。组件渲染优化1. 使用 should_render 控制渲染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 记忆化计算#[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 稳定回调引用#[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 属性#[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. 虚拟滚动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. 减少不必要的状态更新#[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 优化复杂状态#[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 包大小# 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"]// lib.rs#[cfg(feature = "wee_alloc")]#[global_allocator]static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;2. 减少序列化开销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. 请求去重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. 批量请求#[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 避免不必要的状态更新#[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. 延迟加载组件#[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 APIuse 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> }}最佳实践总结避免不必要的渲染:使用 should_render、use_memo、use_callback优化列表渲染:使用 key 属性、虚拟滚动减少状态更新:只在必要时更新状态优化 Wasm 包大小:使用适当的编译选项减少序列化开销:直接操作内存而非序列化批量处理请求:合并多个请求为批量请求使用 ref 避免渲染:对于不需要触发渲染的数据使用 use_ref延迟加载:只在需要时加载和渲染组件监控性能:使用 Performance API 监控应用性能代码分割:将代码分割成更小的块以提高加载速度
阅读 0·2月19日 16:22

Yew 中如何进行测试,有哪些测试策略和工具?

Yew 测试策略与实践Yew 应用测试需要考虑 WebAssembly 环境和 Rust 的测试框架,本文介绍完整的测试策略。单元测试1. 组件单元测试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 单元测试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. 组件交互测试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. 异步组件测试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 测试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 和 Stub1. Mock HTTP 请求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 WebSocketuse 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 测试覆盖率# 安装 tarpaulincargo install cargo-tarpaulin# 运行覆盖率测试cargo tarpaulin --out Html --output-dir ./coverage# 针对 Wasm 测试wasm-pack test --firefox --headless2. 配置测试覆盖率# 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. 组件渲染性能测试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. 测试组织// tests/common/mod.rspub mod common { use yew::prelude::*; pub fn setup_test_environment() { // 设置测试环境 } pub fn cleanup_test_environment() { // 清理测试环境 }}// tests/components/button_test.rsmod 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. 测试数据管理// tests/fixtures/mod.rspub 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 配置# .github/workflows/test.ymlname: Teston: 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 Xml2. GitLab CI 配置# .gitlab-ci.ymlstages: - testtest: 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. 使用浏览器开发者工具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_hookuse 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"));}测试工具推荐wasm-bindgen-test: Wasm 环境测试框架yew-app: Yew 应用测试工具gloo: Web API 绑定和工具cargo-tarpaulin: 代码覆盖率工具web-sys: Web API 绑定总结Yew 测试需要结合 Rust 的测试框架和 WebAssembly 的特性。通过合理的测试策略、Mock 技术和持续集成配置,可以构建高质量的 Yew 应用。
阅读 0·2月19日 16:22

Yew 组件生命周期包含哪些方法,每个方法的作用是什么?

Yew 组件生命周期详解Yew 组件的生命周期与 React 类似,但使用 Rust 的 trait 系统来实现。理解组件生命周期对于构建高效的应用至关重要。主要生命周期方法1. create()时机:组件首次创建时调用用途:初始化组件状态返回值:返回组件的初始状态示例: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 类型示例: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,表示是否需要重新渲染示例: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,表示是否需要重新渲染示例:fn changed(&mut self, ctx: &Context<Self>, _old_props: &Self::Properties) -> bool { // 只有当重要属性变化时才重新渲染 true}5. rendered()时机:组件渲染到 DOM 后调用用途:执行 DOM 操作、初始化第三方库等示例:fn rendered(&mut self, ctx: &Context<Self>, first_render: bool) { if first_render { // 首次渲染后的初始化操作 }}6. destroy()时机:组件从 DOM 中移除时调用用途:清理资源、取消订阅等示例:fn destroy(&mut self, ctx: &Context<Self>) { // 清理定时器、取消网络请求等}}生命周期流程图创建组件 ↓create() → 初始化状态 ↓view() → 渲染 UI ↓rendered() → DOM 操作 ↓[接收消息/props 变化] ↓update() / changed() → 更新状态 ↓view() → 重新渲染 ↓rendered() → DOM 更新 ↓[组件卸载] ↓destroy() → 清理资源最佳实践避免在 view() 中执行复杂计算:将计算逻辑放在 update() 或单独的方法中合理使用 changed():避免不必要的重新渲染及时清理资源:在 destroy() 中清理定时器、事件监听器等使用 rendered() 处理 DOM 操作:确保 DOM 已经渲染完成
阅读 0·2月19日 16:22

Yew 中如何实现路由管理,支持哪些路由功能?

Yew 路由管理详解Yew 提供了强大的路由管理功能,支持客户端路由、嵌套路由、路由守卫等高级特性。基础路由配置1. 安装依赖[dependencies]yew = { version = "0.21", features = ["csr"] }yew-router = { version = "0.18" }2. 定义路由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. 创建路由组件#[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. 设置路由切换#[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. 创建应用组件#[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. 嵌套路由#[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. 路由参数#[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. 查询参数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. 路由守卫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. 编程式导航#[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> }}路由过渡动画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> }}路由懒加载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. 路由组织// 将路由定义在单独的模块中// src/routes.rs#[derive(Clone, Routable, PartialEq)]pub enum Route { #[at("/")] Home, #[at("/about")] About, // ... 其他路由}// 将组件定义在单独的模块中// src/components/home.rs// src/components/about.rs// ...2. 路由守卫复用// src/guards/auth_guard.rspub 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. 路由错误处理#[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. 路由预加载#[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. 路由缓存#[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}
阅读 0·2月19日 16:21