返回文章列表

Rust + Dioxus 试水:被读者安利之后,我发现它和 Yew 根本不在一个赛道

685·5 分钟阅读
RustDioxusWeb 前端跨平台

上一篇写完 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." }
        }
    }
}

注意几个细节:

  1. #[component] ——比 Yew 的 #[function_component] 简洁,功能一样。
  2. RSX 不需要逗号分隔 ——Yew 的 html! 里元素之间要用空格或换行,RSX 更接近 JSX 的语法直觉。
  3. 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 开发者舒服的细节:

  1. todos.write() 返回写锁 ——Signal 内部用类似 RwLock 的机制,读写分离。你不需要手动 clone 状态进闭包,Signal 自己处理引用计数。
  2. EventHandler<T> 替代 Callback<T> ——语义更清晰,调用用 .call() 而不是 .emit()
  3. 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 的跨平台能力有新的认识。

如果你决定深入,这份资源清单可以帮到你: