Dioxus 的 `rsx!` 语法:如果你会 React,上手确实特别快

前言

上一篇我们把第一个 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 页面。" }
    }
}

你能一眼看出来:

  • sectionh1p 是元素
  • 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 matchif/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;条件也不是什么"模板指令",就是 ifmatch

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。

如果把这篇压成几句人话,大概就是:

  1. rsx! 不是模板,它是 Rust 宏。
  2. 标签、属性、事件这些地方很像 JSX,所以 React 用户上手会快。
  3. 条件、循环、表达式这些地方遵循 Rust 思维,而不是 JavaScript 思维。
  4. 样式层面直接站在 HTML + CSS + Tailwind 这边,不需要重新学一套新规则。

下一篇我会接着写 Dioxus 的响应式状态管理:use_signaluse_memouse_effect 到底怎么配合,为什么它和 React 的 useState 不是一回事。

如果你已经在写 Dioxus,最开始让你别扭的是条件渲染、事件绑定,还是列表写法?评论区聊聊。

相关推荐
AlienZHOU6 小时前
AI Coding 时代下,我的技术面试实践分享
前端·后端·面试
刺客码8 小时前
Layui 表格固定列行高错位问题解决方案
前端框架·layui·jquery·web
Captaincc9 小时前
AI用量v0.1.11更新发布 新增 jusage doctor 诊断指令 托盘展示token 和余额 新增 AutoClaw 支持
前端·后端·vibecoding
计算机魔术师10 小时前
德国Wiki被黑后两周,OpenAI终于把模型失控的账本摊开了
前端
kyriewen11 小时前
我让 AI 当面试官面了我一轮:第 3 个追问我就卡住了(附 10 道追问清单)
前端·面试·ai编程
IT_陈寒11 小时前
Python的GIL把我坑惨了,多线程跑得比单线程还慢
前端·人工智能·后端
前端snow11 小时前
ai agent --- 多agent框架之图编排引擎-langgraph
前端
竹林81811 小时前
OmniPic Studio v3.2.1 核心技术架构与全平台发版解析文档
前端·浏览器
JamesZhang8007811 小时前
页面内存只涨不跌? 一次泄漏排查, 牵出 WeakMap 的诞生
前端
Z小明12 小时前
第 6 章 组件进阶
前端·vue.js