前言
上一篇我们把第一个 Dioxus 项目跑起来了。
这一篇先不讲 CLI,也不碰状态管理,只聊一件事:rsx! 到底该怎么理解。
我一开始对它的判断也很简单:这不就是 Rust 版 JSX 吗。
但真写了输入框、按钮、列表、条件分支之后,我发现这个理解只能对一半。
它确实像 JSX,但更准确一点说,它是一套长得像 HTML 的 Rust UI 语法。
这个认知早点拧过来,后面学组件、路由、Signals 都会轻松不少。要是没拧过来,你就会一直下意识去找三元表达式、event.target.value,越写越别扭。
1. rsx! 到底是什么
先说结论:rsx! 不是模板引擎,它就是一个 Rust 宏。
官方文档在 Dioxus 0.7 里说得很直接:RSX 是用来构建 Dioxus UI 的语法,底层会被过程宏展开成 Rust 代码,不是单独的模板文件。
所以你看到的:
rust
rsx! {
h1 { "Welcome to Dioxus!" }
p { "Hello, {name}" }
}
它不是"在 Rust 里塞了一段 HTML",更像是"用更短的方式描述一棵 UI 树"。
这也是它和传统模板系统差别最大的地方:
- 模板系统通常有自己的一套语法规则
rsx!直接活在 Rust 语法环境里- 花括号里的内容不是特殊模板语法,而是普通 Rust 表达式
举个例子:
rust
use dioxus::prelude::*;
fn main() {
dioxus::launch(App);
}
#[component]
fn App() -> Element {
let name = "Dioxus";
rsx! {
div {
h1 { "你好,{name}" }
p { "这段 UI 不是模板文件,而是 Rust 代码的一部分。" }
}
}
}
这段代码真正重要的,不是"标签看起来像 HTML",而是你已经在一个 Rust 函数里把 UI 写完了。
2. 为什么 React 开发者会觉得它很眼熟
如果你写过 React,第一眼看到 rsx!,大概率会觉得挺顺。
因为它和 JSX 有几个很像的地方。
2.1 都是声明式 UI
你不需要一行行去创建节点、设置文本、挂事件。你只描述"页面应该长什么样",状态变化后框架帮你更新。
2.2 都是标签结构 + 属性 + 插值
举个例子,下面这段 Dioxus 代码基本不用翻译:
rust
rsx! {
section {
class: "hero",
h1 { "欢迎回来,{user_name}" }
p { "今天继续改你的 Dioxus 页面。" }
}
}
你能一眼看出来:
section、h1、p是元素class是属性{user_name}是动态插值
2.3 事件也是贴在元素上
rust
button {
onclick: move |_| println!("clicked"),
"点我"
}
这和 React 的心智很接近:事件写在元素旁边,逻辑也跟着组件走。
所以如果你是从 React 过来的,rsx! 最舒服的地方,不是它和 JSX 一模一样,而是你不用重新适应一套很陌生的 UI DSL。
3. 它和 JSX 最关键的差异:花括号里是 Rust,不是 JavaScript
这句话我建议直接记住:
rsx! 像 JSX,但花括号里跑的是 Rust 表达式。
这会直接影响你写条件、写列表、写事件、写字符串拼接的方式。
3.1 字符串插值走 Rust 的格式化规则
rust
let world = "earth";
rsx! {
h1 { "Hello {world}!" }
}
这个感觉更接近 Rust 的 format!,不是 JavaScript 模板字符串那套。
3.2 复杂表达式可以直接塞进去
举个例子,如果你想把字符串大写后再渲染:
rust
rsx! {
span {
{
format!("当前用户: {}", current_user_name()).to_uppercase()
}
}
}
只要这个表达式最后能变成 Dioxus 能渲染的东西,就能塞进来。
3.3 match、if/else 都是正经 Rust 写法
这个地方往往是 React 开发者第一次明显觉得"哦,这里已经不是 JSX 了"。
在 React 里,大家太习惯三元表达式了:
tsx
const screen = authenticated ? <Dashboard /> : <Login />;
到了 Dioxus,这里就老老实实按 Rust 来写:
rust
let screen = if authenticated() {
rsx! { Dashboard {} }
} else {
rsx! { Login {} }
};
rsx! {
main {
{screen}
}
}
如果分支再多一点,match 往往比 JSX 还顺手:
rust
let badge = match status.as_str() {
"success" => rsx! { span { class: "ok", "已完成" } },
"pending" => rsx! { span { class: "pending", "进行中" } },
_ => rsx! { span { class: "draft", "草稿" } },
};
rsx! {
div { {badge} }
}
说白了,Dioxus 没有打算把 Rust 伪装成 JavaScript。它只是给 Rust UI 写法套了一层更像前端的外观。
4. 条件渲染怎么写:别找三元,也别硬套 &&
这一段是 rsx! 里最值得单独适应的地方。
官方文档给了两种思路。
4.1 先算出一个 Element,再插进去
rust
let panel = if logged_in() {
rsx! { UserPanel {} }
} else {
rsx! { GuestPanel {} }
};
rsx! {
div {
{panel}
}
}
这个写法很稳,逻辑稍微复杂一点时尤其好用。
4.2 直接在 rsx! 里写内联 if
Dioxus 也支持直接在 rsx! 里写内联 if:
rust
rsx! {
div {
if logged_in() {
"你已经登录了"
} else {
"你还没有登录"
}
}
}
再举个更接近日常页面的例子:
rust
rsx! {
section {
h2 { "发布设置" }
if is_saving() {
p { class: "tips", "正在保存..." }
}
if save_error().is_some() {
p { class: "error", "保存失败,请稍后重试" }
}
}
}
这里有两个细节可以顺手记一下:
if的分支体是 RSX,不是普通 Rust 语句块- 即使没有
else,Dioxus 也能正常渲染,缺省分支会变成一个占位节点
所以别再下意识往 React 那套上靠:
tsx
{loading && <Spinner />}
在 Dioxus 里,直接写 if loading() { Spinner {} } 通常更干脆。
5. 列表渲染怎么写:map 能用,内联 for 也能用
如果说条件渲染最容易让 React 用户卡一下,那列表渲染反而是最容易上手的部分。
因为 Dioxus 两种都支持。
5.1 直接用迭代器
rust
let todos = vec!["读文档", "改按钮", "接路由"];
rsx! {
ul {
{todos.iter().map(|todo| rsx! {
li { "{todo}" }
})}
}
}
这很像 JSX 里的 array.map(...)。
5.2 用 Dioxus 提供的内联 for
官方文档还给了一个更顺手的写法:
rust
let todos = vec!["读文档", "改按钮", "接路由"];
rsx! {
ul {
for todo in todos.iter() {
li { "{todo}" }
}
}
}
这段我个人挺喜欢。原因很简单:它比 {items.iter().map(...)} 更像在读结构,不像在读一串链式调用。
5.3 循环里要临时算东西怎么办
也可以,直接包一层表达式:
rust
rsx! {
ul {
for user in users.iter() {
{
let label = format!("{} ({})", user.name, user.role);
rsx! {
li { "{label}" }
}
}
}
}
}
这一点也很能体现 rsx! 的脾气:你不是在写模板循环,你是在 RSX 里继续写 Rust。
6. 属性绑定怎么写:语义像 HTML,写法像 Rust
属性这一块,Dioxus 的规则其实很统一:
属性名后面跟冒号,值写 Rust 表达式。
最基础的是这种:
rust
rsx! {
input {
class: "search-input",
id: "keyword",
placeholder: "搜索文章"
}
}
6.1 动态值直接写表达式
rust
#[component]
fn SearchBox() -> Element {
let mut keyword = use_signal(String::new);
rsx! {
input {
value: "{keyword}",
placeholder: "输入关键字",
oninput: move |evt| keyword.set(evt.value())
}
}
}
这段代码里最容易让人停一下的,通常是这一句:
rust
oninput: move |evt| keyword.set(evt.value())
因为很多人脑子里会先冒出 event.target.value。
但别忘了,你现在已经不在 JS 世界里了。事件参数是 Dioxus 自己的 Rust 类型,取值方式自然也和浏览器原生事件对象不一样。
6.2 布尔属性和条件属性也可以直接算
rust
button {
disabled: is_saving(),
onclick: move |_| save(),
"保存"
}
只要最终能算出属性需要的值,就可以直接写进去。
6.3 class 可以写多次,条件拼样式会更自然
这一点挺实用。
rust
button {
class: "btn",
class: if is_active() { "btn-active" },
class: if is_large() { "btn-large" },
"切换状态"
}
比起手动拼一长串 class 字符串,这种写法干净很多,尤其是状态一多的时候。
7. 常见 HTML 元素和属性,在 Dioxus 里怎么落地
如果你现在脑子里想的是"那我以前那段 HTML 到底该怎么抄过来",可以先看这张最常用的对照表。
| 场景 | HTML / JSX 习惯 | Dioxus 写法 |
|-------|-------------------------------------|--------------------------------------------------------|---|-------|
| class | class="card" / className="card" | class: "card" |
| id | id="hero" | id: "hero" |
| 文本插值 | {name} | "你好,{name}" 或 {name} |
| 事件 | onClick={...} | `onclick: move | _ | ...` |
| 输入框取值 | event.target.value | evt.value() |
| 条件渲染 | cond ? A : B | if cond { rsx!{ A {} } } else { rsx!{ B {} } } |
| 列表渲染 | items.map(...) | {items.iter().map(...)} 或 for item in items.iter() |
| 内联样式 | style="color:red" | style: "color: red;" |
这张表当然覆盖不了全部,但把大部分静态 HTML 和基础交互页面迁过来,已经够用了。
8. 样式怎么写:内联、CSS 文件、Tailwind 都能接
这一块是 Dioxus 很讨喜的地方。
它没有自己再发明一套样式系统,而是老老实实站在 HTML + CSS 这边。
8.1 最直接的是内联 style
rust
rsx! {
div {
style: "background-color: #1d4ed8; color: white; padding: 16px; border-radius: 12px;",
"这是一个带内联样式的卡片"
}
}
8.2 也可以直接写单个 CSS 属性
这点很多人第一次看到时会有点惊喜:
rust
rsx! {
div {
background_color: "#1d4ed8",
color: "white",
padding: "16px",
border_radius: "12px",
"这也是合法写法"
}
}
也就是说,CSS 属性名可以改成 snake_case,直接写进 RSX。
8.3 真正做项目,还是建议样式表分出去
官方文档在 0.7 里推荐用 asset!() 和 document::Stylesheet 引入样式文件:
rust
use dioxus::prelude::*;
static MAIN_CSS: Asset = asset!("/assets/main.css");
#[component]
fn App() -> Element {
rsx! {
document::Stylesheet { href: MAIN_CSS }
div {
class: "page",
h1 { "Hello Dioxus" }
}
}
}
对应的 assets/main.css 就是你熟悉的 CSS:
css
.page {
width: min(720px, calc(100vw - 32px));
margin: 48px auto;
padding: 32px;
border-radius: 20px;
background: white;
}
8.4 Tailwind 也能接,而且官方就是这么支持的
如果你已经是 Tailwind 用户,Dioxus 这边基本没有额外心智成本。
类名照写:
rust
rsx! {
div {
class: "flex flex-col gap-4 rounded-2xl bg-white p-6 shadow-lg",
h1 { class: "text-2xl font-bold", "Rust + Dioxus" }
p { class: "text-slate-600", "这一段就是标准的 Tailwind class。" }
}
}
按 Dioxus 0.7 官方文档的做法,你在项目根目录放一个 tailwind.css:
css
@import "tailwindcss";
@source "./src/**/*.{rs,html,css}";
然后在应用里引入生成后的样式:
rust
rsx! {
document::Stylesheet { href: asset!("/assets/tailwind.css") }
}
这条路为什么舒服?因为你没有被迫去学什么"Rust 专属样式系统"。你原来会的 CSS、Tailwind、选择器、布局思路,大部分都还能接着用。
9. 从 HTML / React 迁过来时,最容易卡住的 4 个点
9.1 不要把 rsx! 当成模板语言
它长得像模板,但你应该把它看成"Rust 里的声明式 UI 宏"。
一旦这么看,很多写法就顺了。比如循环不是"模板循环标签",而是迭代器或者 for;条件也不是什么"模板指令",就是 if 和 match。
9.2 少找 event.target.value
输入事件最容易暴露思维惯性。
看到下面这句别慌:
rust
oninput: move |evt| name.set(evt.value())
它不是奇怪,就是更 Rust 一点。
9.3 复杂逻辑先在外面算,再往 rsx! 里塞
很多人 JSX 写久了,习惯把一大坨条件和拼接都塞进标记里。到 Dioxus 这边,我反而建议你更 Rust 一点:
rust
let header = if is_editing() { "编辑文章" } else { "发布文章" };
let submit_text = if is_saving() { "保存中..." } else { "保存" };
然后再渲染:
rust
rsx! {
h1 { "{header}" }
button { "{submit_text}" }
}
这样读起来通常会更轻松。
9.4 有现成 HTML 时,可以用 dx translate
官方文档还提到一个挺实用的工具:dx translate。
如果你手里已经有一段 HTML,可以先自动翻成 RSX,再手动整理。迁移现有页面,或者先拿设计稿生成的 HTML 搭个架子,这个命令都能省你不少时间。
总结
rsx! 最容易让人误解的地方,是它看起来像"Rust 版 JSX",但真正决定手感的,其实是后半句:它本质上还是 Rust。
如果把这篇压成几句人话,大概就是:
rsx!不是模板,它是 Rust 宏。- 标签、属性、事件这些地方很像 JSX,所以 React 用户上手会快。
- 条件、循环、表达式这些地方遵循 Rust 思维,而不是 JavaScript 思维。
- 样式层面直接站在 HTML + CSS + Tailwind 这边,不需要重新学一套新规则。
下一篇我会接着写 Dioxus 的响应式状态管理:use_signal、use_memo、use_effect 到底怎么配合,为什么它和 React 的 useState 不是一回事。
如果你已经在写 Dioxus,最开始让你别扭的是条件渲染、事件绑定,还是列表写法?评论区聊聊。