前言
上一篇我们把第一个 Dioxus 项目跑起来了。
这一篇先不讲 CLI,也不碰状态管理,只聊一件事:rsx!到底该怎么理解。
我一开始对它的判断也很简单:这不就是 Rust 版 JSX 吗。
但真写了输入框、按钮、列表、条件分支之后,我发现这个理解只能对一半。
它确实像 JSX,但更准确一点说,它是一套长得像 HTML 的 Rust UI 语法。
这个认知早点拧过来,后面学组件、路由、Signals 都会轻松不少。要是没拧过来,你就会一直下意识去找三元表达式、event.target.value,越写越别扭。
1.rsx!到底是什么
先说结论:rsx!不是模板引擎,它就是一个 Rust 宏。
官方文档在 Dioxus 0.7 里说得很直接:RSX 是用来构建 Dioxus UI 的语法,底层会被过程宏展开成 Rust 代码,不是单独的模板文件。
所以你看到的:
rsx!{h1{"Welcome to Dioxus!"}p{"Hello, {name}"}}它不是“在 Rust 里塞了一段 HTML”,更像是“用更短的方式描述一棵 UI 树”。
这也是它和传统模板系统差别最大的地方:
- 模板系统通常有自己的一套语法规则
rsx!直接活在 Rust 语法环境里- 花括号里的内容不是特殊模板语法,而是普通 Rust 表达式
举个例子:
usedioxus::prelude::*;fnmain(){dioxus::launch(App);}#[component]fnApp()->Element{letname="Dioxus";rsx!{div{h1{"你好,{name}"}p{"这段 UI 不是模板文件,而是 Rust 代码的一部分。"}}}}这段代码真正重要的,不是“标签看起来像 HTML”,而是你已经在一个 Rust 函数里把 UI 写完了。
2. 为什么 React 开发者会觉得它很眼熟
如果你写过 React,第一眼看到rsx!,大概率会觉得挺顺。
因为它和 JSX 有几个很像的地方。
2.1 都是声明式 UI
你不需要一行行去创建节点、设置文本、挂事件。你只描述“页面应该长什么样”,状态变化后框架帮你更新。
2.2 都是标签结构 + 属性 + 插值
举个例子,下面这段 Dioxus 代码基本不用翻译:
rsx!{section{class:"hero",h1{"欢迎回来,{user_name}"}p{"今天继续改你的 Dioxus 页面。"}}}你能一眼看出来:
section、h1、p是元素class是属性{user_name}是动态插值
2.3 事件也是贴在元素上
button{onclick:move|_|println!("clicked"),"点我"}这和 React 的心智很接近:事件写在元素旁边,逻辑也跟着组件走。
所以如果你是从 React 过来的,rsx!最舒服的地方,不是它和 JSX 一模一样,而是你不用重新适应一套很陌生的 UI DSL。
3. 它和 JSX 最关键的差异:花括号里是 Rust,不是 JavaScript
这句话我建议直接记住:
rsx!像 JSX,但花括号里跑的是 Rust 表达式。
这会直接影响你写条件、写列表、写事件、写字符串拼接的方式。
3.1 字符串插值走 Rust 的格式化规则
letworld="earth";rsx!{h1{"Hello {world}!"}}这个感觉更接近 Rust 的format!,不是 JavaScript 模板字符串那套。
3.2 复杂表达式可以直接塞进去
举个例子,如果你想把字符串大写后再渲染:
rsx!{span{{format!("当前用户: {}",current_user_name()).to_uppercase()}}}只要这个表达式最后能变成 Dioxus 能渲染的东西,就能塞进来。
3.3match、if/else都是正经 Rust 写法
这个地方往往是 React 开发者第一次明显觉得“哦,这里已经不是 JSX 了”。
在 React 里,大家太习惯三元表达式了:
const screen = authenticated ? <Dashboard /> : <Login />;到了 Dioxus,这里就老老实实按 Rust 来写:
letscreen=ifauthenticated(){rsx!{Dashboard{}}}else{rsx!{Login{}}};rsx!{main{{screen}}}如果分支再多一点,match往往比 JSX 还顺手:
letbadge=matchstatus.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,再插进去
letpanel=iflogged_in(){rsx!{UserPanel{}}}else{rsx!{GuestPanel{}}};rsx!{div{{panel}}}这个写法很稳,逻辑稍微复杂一点时尤其好用。
4.2 直接在rsx!里写内联if
Dioxus 也支持直接在rsx!里写内联if:
rsx!{div{iflogged_in(){"你已经登录了"}else{"你还没有登录"}}}再举个更接近日常页面的例子:
rsx!{section{h2{"发布设置"}ifis_saving(){p{class:"tips","正在保存..."}}ifsave_error().is_some(){p{class:"error","保存失败,请稍后重试"}}}}这里有两个细节可以顺手记一下:
if的分支体是 RSX,不是普通 Rust 语句块- 即使没有
else,Dioxus 也能正常渲染,缺省分支会变成一个占位节点
所以别再下意识往 React 那套上靠:
{loading && <Spinner />}在 Dioxus 里,直接写if loading() { Spinner {} }通常更干脆。
5. 列表渲染怎么写:map能用,内联for也能用
如果说条件渲染最容易让 React 用户卡一下,那列表渲染反而是最容易上手的部分。
因为 Dioxus 两种都支持。
5.1 直接用迭代器
lettodos=vec!["读文档","改按钮","接路由"];rsx!{ul{{todos.iter().map(|todo|rsx!{li{"{todo}"}})}}}这很像 JSX 里的array.map(...)。
5.2 用 Dioxus 提供的内联for
官方文档还给了一个更顺手的写法:
lettodos=vec!["读文档","改按钮","接路由"];rsx!{ul{fortodointodos.iter(){li{"{todo}"}}}}这段我个人挺喜欢。原因很简单:它比{items.iter().map(...)}更像在读结构,不像在读一串链式调用。
5.3 循环里要临时算东西怎么办
也可以,直接包一层表达式:
rsx!{ul{foruserinusers.iter(){{letlabel=format!("{} ({})",user.name,user.role);rsx!{li{"{label}"}}}}}}这一点也很能体现rsx!的脾气:你不是在写模板循环,你是在 RSX 里继续写 Rust。
6. 属性绑定怎么写:语义像 HTML,写法像 Rust
属性这一块,Dioxus 的规则其实很统一:
属性名后面跟冒号,值写 Rust 表达式。
最基础的是这种:
rsx!{input{class:"search-input",id:"keyword",placeholder:"搜索文章"}}6.1 动态值直接写表达式
#[component]fnSearchBox()->Element{letmutkeyword=use_signal(String::new);rsx!{input{value:"{keyword}",placeholder:"输入关键字",oninput:move|evt|keyword.set(evt.value())}}}这段代码里最容易让人停一下的,通常是这一句:
oninput:move|evt|keyword.set(evt.value())因为很多人脑子里会先冒出event.target.value。
但别忘了,你现在已经不在 JS 世界里了。事件参数是 Dioxus 自己的 Rust 类型,取值方式自然也和浏览器原生事件对象不一样。
6.2 布尔属性和条件属性也可以直接算
button{disabled:is_saving(),onclick:move|_|save(),"保存"}只要最终能算出属性需要的值,就可以直接写进去。
6.3class可以写多次,条件拼样式会更自然
这一点挺实用。
button{class:"btn",class:ifis_active(){"btn-active"},class:ifis_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
rsx!{div{style:"background-color: #1d4ed8; color: white; padding: 16px; border-radius: 12px;","这是一个带内联样式的卡片"}}8.2 也可以直接写单个 CSS 属性
这点很多人第一次看到时会有点惊喜:
rsx!{div{background_color:"#1d4ed8",color:"white",padding:"16px",border_radius:"12px","这也是合法写法"}}也就是说,CSS 属性名可以改成snake_case,直接写进 RSX。
8.3 真正做项目,还是建议样式表分出去
官方文档在 0.7 里推荐用asset!()和document::Stylesheet引入样式文件:
usedioxus::prelude::*;staticMAIN_CSS:Asset=asset!("/assets/main.css");#[component]fnApp()->Element{rsx!{document::Stylesheet{href:MAIN_CSS}div{class:"page",h1{"Hello Dioxus"}}}}对应的assets/main.css就是你熟悉的 CSS:
.page{width:min(720px,calc(100vw - 32px));margin:48px auto;padding:32px;border-radius:20px;background:white;}8.4 Tailwind 也能接,而且官方就是这么支持的
如果你已经是 Tailwind 用户,Dioxus 这边基本没有额外心智成本。
类名照写:
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:
@import"tailwindcss";@source"./src/**/*.{rs,html,css}";然后在应用里引入生成后的样式:
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
输入事件最容易暴露思维惯性。
看到下面这句别慌:
oninput:move|evt|name.set(evt.value())它不是奇怪,就是更 Rust 一点。
9.3 复杂逻辑先在外面算,再往rsx!里塞
很多人 JSX 写久了,习惯把一大坨条件和拼接都塞进标记里。到 Dioxus 这边,我反而建议你更 Rust 一点:
letheader=ifis_editing(){"编辑文章"}else{"发布文章"};letsubmit_text=ifis_saving(){"保存中..."}else{"保存"};然后再渲染:
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,最开始让你别扭的是条件渲染、事件绑定,还是列表写法?评论区聊聊。