Yew
Yew 是一个用 Rust 编写的现代 Rust 框架,用于创建多线程前端 web 应用。它使用 WebAssembly (Wasm) 运行在浏览器中,使得开发者能够利用 Rust 的性能和安全性来构建高效的网页应用。Yew 类似于 JavaScript 的 React 框架,采用组件化的开发模式,支持 JSX-like 模板语法(称为 "macro HTML")。

查看更多相关内容
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