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,最开始让你别扭的是条件渲染、事件绑定,还是列表写法?评论区聊聊。

相关推荐
Momo__1 小时前
TypeScript NoInfer<T>——精准控制泛型推断的工具类型
前端·typescript
lichenyang4532 小时前
从 Web 容器开始,理解 ASCF 元服务开发
前端
用户059540174462 小时前
把待办应用从Electron换成Tauri,内存占用狂降90%,打包体积仅5MB
前端·css
假如让我当三天老蒯2 小时前
回归基本功!前端的解构赋值、扩展运算符、剩余参数
前端·面试
bonechips3 小时前
JS 数组指南:从内存原理到二维矩阵
前端·javascript
亿元程序员3 小时前
美术妹子让我给模型加个描边,我差点把Cocos卸了
前端
IT_陈寒3 小时前
React的useEffect依赖数组把我坑惨了,真相其实很简单
前端·人工智能·后端