Rust + Dioxus 试水:被读者安利之后,我发现它和 Yew 根本不在一个赛道
上一篇写完 Yew 之后,有个读者留言说:"试试 Dioxus 吧,跨平台能力很香。"
说实话我一开始是抵触的。Rust 前端生态本来就小,再分散精力学第二个框架,值吗?
但试了一个周末之后,我理解了为什么有人死磕 Dioxus——它和 Yew 解决的问题完全不同。
先放结论:Yew 是 React,Dioxus 是 Flutter
这个比喻不太严谨,但能帮你快速定位。
| Yew | Dioxus | |
|---|---|---|
| 对标谁 | React | Flutter |
| 核心目标 | 做好 Web 前端 | 一套代码跑 Web + 桌面 + 移动端 |
| 渲染方式 | WASM → DOM | Web: WASM → DOM / 桌面: WGPU 原生渲染 |
| 状态模型 | Hooks(类 React) | Signals(类 Solid.js) |
| 最新版本 | 0.23 | 0.7 |
如果你只想要一个 Rust 写的单页 Web 应用,Yew 更纯粹。
但如果你想要"用 Rust 写一个应用,然后决定它跑在哪"——Dioxus 是目前唯一的选择。
第一个 Dioxus 项目:五分钟跑起来
Dioxus 官方给了 CLI 工具,体验比 Yew 的 Trunk 更"现代":
# 安装 Dioxus CLI
cargo install dioxus-cli
# 创建项目
dx new dioxus-playground
# 选择 Web 模板
cd dioxus-playground
dx serve浏览器打开 http://localhost:8080,看到 Hello World——整个过程和 Yew 差不多,但 dx serve 的热重载比 trunk serve 快不少。
Cargo.toml 的依赖长这样(Dioxus 0.7):
[package]
name = "dioxus-playground"
version = "0.1.0"
edition = "2024"
[dependencies]
dioxus = { version = "0.7", features = ["web"] }和 Yew 最大的不同是 features = ["web"] ——Dioxus 通过 feature 控制渲染目标。同一个 crate,换一套 features 就能跑桌面端。
# 桌面端只需要改这一行
dioxus = { version = "0.7", features = ["desktop"] }这就是 Dioxus 的核心卖点:平台是配置,不是重写。
组件写法:RSX 比 Yew 的 html! 更自然
Dioxus 用 RSX(Rust JSX)写模板,语法比 Yew 的 html! 宏更像真正的 JSX:
use dioxus::prelude::*;
#[component]
fn App() -> Element {
rsx! {
div {
h1 { "Hello, Dioxus!" }
p { "This looks more like JSX than Yew's html! macro." }
}
}
}注意几个细节:
#[component]宏 ——比 Yew 的#[function_component]简洁,功能一样。- RSX 不需要逗号分隔 ——Yew 的
html!里元素之间要用空格或换行,RSX 更接近 JSX 的语法直觉。 Element返回类型 ——Dioxus 的统一节点类型,比 Yew 的Html更通用。
Counter 组件对比:
// Dioxus 写法
use dioxus::prelude::*;
#[component]
fn Counter() -> Element {
let mut count = use_signal(|| 0);
rsx! {
div {
button {
onclick: move |_| count -= 1,
"-"
}
span { "{count}" }
button {
onclick: move |_| count += 1,
"+"
}
}
}
}和 Yew 的对比:
// Yew 写法(回顾)
#[function_component(Counter)]
fn counter() -> Html {
let value = use_state(|| 0);
let increment = {
let value = value.clone();
Callback::from(move |_| value.set(*value + 1))
};
html! {
<div>
<button onclick={decrement}>{"-"}</button>
<span>{ *value }</span>
<button onclick={increment}>{"+"}</button>
</div>
}
}最大的差异:状态管理。
Yew 用 use_state 返回一个智能指针,修改状态要调用 .set(),而且闭包里要手动 clone。Dioxus 用 use_signal 返回一个可读写信号,直接 count += 1 就行。
// Dioxus — 直接读写,像普通变量
let mut count = use_signal(|| 0);
count += 1; // 触发重新渲染
// Yew — 通过 setter,需要 clone 进闭包
let value = use_state(|| 0);
value.set(*value + 1); // 同样触发渲染,但更啰嗦Dioxus 的 Signal 设计更像 Solid.js 的信号系统——细粒度响应式,只有真正用到的部分会重新渲染,而不是整个组件。对于复杂 UI,这个差异在性能上是真实的。
状态管理:Signals 让 Rust 前端更像 Rust
写 Yew 的时候最烦的就是 Callback::clone() 满天飞。Dioxus 是怎么解决的?
use dioxus::prelude::*;
#[component]
fn TodoApp() -> Element {
let mut todos = use_signal(|| vec![
Todo { id: 1, title: "Learn Dioxus".to_string(), completed: false },
]);
let mut filter = use_signal(|| Filter::All);
let filtered = use_memo(move || {
match *filter.read() {
Filter::All => todos.read().clone(),
Filter::Active => todos.read().iter().filter(|t| !t.completed).cloned().collect(),
Filter::Completed => todos.read().iter().filter(|t| t.completed).cloned().collect(),
}
});
rsx! {
div { class: "todo-app",
h1 { "Dioxus TODO" }
// ...
div { class: "todo-list",
for todo in filtered.read().iter() {
TodoItem {
key: "{todo.id}",
todo: todo.clone(),
on_toggle: move |id: u32| {
let mut ts = todos.write();
if let Some(t) = ts.iter_mut().find(|t| t.id == id) {
t.completed = !t.completed;
}
}
}
}
}
}
}
}
#[component]
fn TodoItem(todo: Todo, on_toggle: EventHandler<u32>) -> Element {
rsx! {
div { class: "todo-item",
input {
r#type: "checkbox",
checked: todo.completed,
onchange: move |_| on_toggle.call(todo.id)
}
span { class: if todo.completed { "completed" } else { "" },
"{todo.title}"
}
}
}
}注意几个让 Rust 开发者舒服的细节:
todos.write()返回写锁 ——Signal 内部用类似RwLock的机制,读写分离。你不需要手动 clone 状态进闭包,Signal 自己处理引用计数。EventHandler<T>替代Callback<T>——语义更清晰,调用用.call()而不是.emit()。use_memo缓存派生状态 ——只有依赖变化时才重新计算,Yew 里没有直接等价物。
但说实话,todos.read() 和 todos.write() 的显式调用是一把双刃剑。 好处是你清楚知道自己在读还是写,坏处是代码里到处都是 .read(),有点噪音。
最让我惊喜的:桌面端真的能用
前面说的跨平台不是画饼,我试了一下桌面端。
改一行 Cargo.toml:
[dependencies]
dioxus = { version = "0.7", features = ["desktop"] }入口代码改一下:
use dioxus::prelude::*;
fn main() {
dioxus::desktop::launch(App);
}
#[component]
fn App() -> Element {
rsx! {
div {
h1 { "This is a desktop app" }
p { "Same code, different target." }
}
}
}cargo run一个原生窗口弹出来,里面跑的是同一个 RSX 组件。没有 Electron,没有嵌入式 Chromium,WGPU 直接渲染。
包体大小对比(简单应用):
| 方案 | 产物大小 |
|---|---|
| Dioxus 桌面端 | ~15MB |
| Tauri | ~5MB(但依赖系统 WebView) |
| Electron | ~150MB+ |
Dioxus 桌面端比 Electron 小一个数量级,比 Tauri 大一些但不依赖系统 WebView——这在 Linux 上尤其重要,因为各发行版的 WebView 版本差异很大。
但我要泼个冷水:桌面端的生态还很早期。 没有现成的文件选择对话框、没有系统托盘 API、没有原生菜单。这些你都得自己用 raw-window-handle 对接平台 API。
最让我头疼的:Web 端体验不如 Yew 纯粹
Dioxus 的 Web 端是基于 WASM 的,和 Yew 一样。但有几个让我不适应的地方:
1. 浏览器 API 的对接更麻烦
Yew 有 gloo 系列 crate,操作 localStorage、fetch、timer 都很自然。Dioxus 的 Web API 封装还在完善中:
// Yew — gloo-storage 很成熟
use gloo_storage::Storage;
gloo_storage::LocalStorage::set("key", &data).unwrap();
// Dioxus — 目前主要依赖 web-sys 直接调用
use wasm_bindgen::JsCast;
use web_sys::{window, Storage};
let storage = window().unwrap().local_storage().unwrap().unwrap();
storage.set_item("key", &serde_json::to_string(&data).unwrap()).unwrap();Dioxus 的 Web 生态比 Yew 单薄。 毕竟 Yew 专注 Web 多年,社区积累更深。
2. 服务端渲染(SSR)支持
Dioxus 有全栈框架 Dioxus-Fullstack,支持 SSR 和 server functions。理念很好——前后端共享 Rust 代码。但我实际试下来,文档和示例还不够完整,遇到问题时 StackOverflow 上几乎搜不到答案。
Yew 的 SSR 也不成熟,但至少有 yew-ssr crate 和若干博客文章可以参考。
3. 编译速度和调试
和 Yew 一样,WASM 编译慢、DevTools 调试困难。这是 Rust 前端的通用问题,不是 Dioxus 独有的。
Dioxus vs Yew:怎么选?
试完两个框架之后,我的判断:
| 场景 | 推荐 | 原因 |
|---|---|---|
| 纯 Web 应用 | Yew | 生态更成熟,文档更全,社区更大 |
| Web + 桌面端 | Dioxus | 跨平台是原生能力,不是移植 |
| Web + 移动端 | Dioxus | 移动端支持虽然早期,但 Yew 完全没有 |
| 内部工具/Dashboard | 都可以 | 两者都能胜任 |
| 需要 SSR/全栈 | Leptos | Dioxus Fullstack 和 Yew SSR 都不够成熟 |
如果你现在必须选一个 Rust 前端框架,问自己一个问题:你的应用未来会不会需要桌面端?
如果答案是"可能",选 Dioxus。跨平台能力是架构层面的,不是后期能加的。
如果答案是"绝对不会",选 Yew。Web 生态的成熟度是真实的优势。
结论
试 Dioxus 的这个周末,我对 Rust 前端的看法变了一些。
之前我觉得 Rust 前端是在和 React/Vue 竞争——同样做 Web,凭什么用你的 WASM?
但 Dioxus 让我意识到,Rust 前端的真正价值可能不在 Web,而在"跨平台"。
JavaScript 生态里,跨平台是 React Native、Electron、Tauri 各自为战。Rust 生态里,Dioxus 试图用同一套组件模型覆盖所有平台。这个野心很大,但方向是对的。
说实话,Dioxus 现在还不完美。Web 生态不如 Yew,桌面端功能不如 Tauri,移动端我没实测过不敢下结论,但从文档来看支持还在很早期的阶段。但它给出了一个愿景:以后写 Rust 应用,平台只是一个配置项。
Yew 让我相信 Rust 能写前端。Dioxus 让我相信 Rust 可能重新定义"前端"的边界。
如果你已经试过 Yew,我强烈建议花一个下午跑一下 Dioxus 的桌面端 demo。那种"同一套代码弹出原生窗口"的感觉,会让你对 Rust 的跨平台能力有新的认识。
如果你决定深入,这份资源清单可以帮到你:
- Dioxus 官方文档 —— 入门首选
- Dioxus GitHub —— 关注 issues 了解当前限制
- Dioxus Discord —— 社区很活跃,提问能得到回复
- Awesome Dioxus —— 生态资源汇总