知识点:RSX 是什么
定义:
RSX(Rust Syntax Extension)是 Dioxus 框架的核心宏
作用:
用类 JSX 的语法在 Rust 中描述 UI 结构
它在编译期被转换为虚拟 DOM(VDOM)节点构建代码
与 HTML 模板的区别:
RSX 直接活在 Rust 语法环境中,花括号里是普通 Rust 表达式
编译期类型检查(属性类型、事件处理器签名)
无需额外模板引擎或构建工具
依赖Cargo.toml:
[dependencies]
dioxus = "0.7"
注意:
Dioxus 目前处于 0.7版本,API 可能在后续版本中有所变化
use dioxus::prelude::*;
fn main() {
dioxus::launch(App);
}
// #[component] 宏将函数标记为 Dioxus 组件
// 它会将函数参数转换为自动派生的 Props 结构体,
// 并确保组件名称使用 PascalCase
#[component]
fn App() -> Element {
let name = "Dioxus";
rsx! {
div {
h1 { "你好,{name}" }
p { "这段 UI 不是模板文件,而是 Rust 代码的一部分。" }
}
}
}
rsx! 宏在编译期的转换过程(简化版):
编译前:
rsx! {
div {
class: "container",
h1 { "Hello {name}" }
}
}
编译后(简化):
VNode::new([
VElement::new("div",
[Attribute::new("class", "container")],
[VNode::new([
VElement::new("h1", [],
[VText::new(format!("Hello {}", name))]
)
])
])
])
知识点:元素与嵌套
use dioxus::prelude::*;
#[component]
fn ElementsDemo() -> Element {
rsx! {
// 基本元素:元素名 + 花括号
div {
h1 { "标题" }
p { "段落文本" }
}
// 嵌套元素
section {
header {
h2 { "文章标题" }
span { "2026-01-01" }
}
article {
p { "正文内容" }
footer {
span { "作者:张三" }
}
}
}
// Web Components(含连字符的元素名自动识别为非类型化 Web 组件)
my_custom_element {
"自定义组件内容"
}
}
}
知识点:属性系统
use dioxus::prelude::*;
#[component]
fn AttributesDemo() -> Element {
let width = 100;
let is_active = true;
let is_disabled = false;
rsx! {
// === 静态属性:直接写值 ===
div {
class: "container",
id: "main",
"静态属性"
}
// === 动态属性:用 Rust 表达式 ===
div {
width: "{width}px",
// 字符串中 {width} 会被自动格式化(类似 format! 宏)
"动态属性"
}
// === 可选属性:用 if 表达式 ===
div {
class: if is_active { "active" },
// is_active 为 true → class="active"
// is_active 为 false → 不设置 class 属性
disabled: if is_disabled { "true" },
"可选属性"
}
// === 原始属性:用引号包裹属性名 ===
// 用于不在 HTML 规范中的自定义属性
div {
"data-count": "42",
"aria-label": "计数标签",
"自定义属性"
}
// === 样式属性 ===
div {
style: "color: red; font-size: 16px;",
"内联样式"
}
}
}
知识点:文本与表达式
use dioxus::prelude::*;
#[component]
fn TextDemo() -> Element {
let name = "World";
let count = 42;
rsx! {
// === 静态文本:字符串字面量 ===
p { "这是静态文本" }
// === 格式化文本:在字符串中嵌入变量 ===
p { "你好,{name}!" }
p { "计数:{count}" }
// === Rust 表达式:用花括号包裹 ===
p { {name.to_uppercase()} }
// === 表达式结果实现 IntoDynNode 即可渲染 ===
// 数字、字符串、Element 等都实现了该 trait
p { {format!("计算结果: {}", count * 2)} }
// === 迭代器也可以直接渲染 ===
div {
{(0..5).map(|n| rsx! { span { "{n} " } })}
}
}
}
知识点:组件调用
use dioxus::prelude::*;
// 组件名必须以大写字母开头或包含下划线
#[component]
fn UserCard(name: String, age: u32) -> Element {
rsx! {
div {
class: "user-card",
h3 { "{name}" }
p { "年龄:{age}" }
}
}
}
#[component]
fn Badge(label: String, color: String) -> Element {
rsx! {
span {
style: "background: {color}; padding: 2px 8px; border-radius: 4px;",
"{label}"
}
}
}
#[component]
fn ComponentDemo() -> Element {
let users = vec![
("Alice".to_string(), 25),
("Bob".to_string(), 30),
("Charlie".to_string(), 35),
];
rsx! {
div {
h2 { "用户列表" }
// 调用组件:组件名 + 花括号 + 属性
for (name, age) in users {
UserCard {
name: name.clone(),
age: age,
}
}
// 组件属性可以是任意 Rust 表达式
Badge {
label: "Rust".to_string(),
color: "orange".to_string(),
}
Badge {
label: "Dioxus".to_string(),
color: "blue".to_string(),
}
}
}
}
知识点:条件渲染 — if / if else
use dioxus::prelude::*;
#[component]
fn ConditionalDemo() -> Element {
let logged_in = use_signal(|| false);
let role = use_signal(|| "user");
rsx! {
div {
// === 简单 if:条件为真时渲染 ===
if logged_in() {
div {
class: "welcome",
"欢迎回来!"
}
}
// === if / else:二选一 ===
if logged_in() {
div { "你已登录" }
} else {
div { "请先登录" }
}
// === if / else if / else:多分支 ===
if role() == "admin" {
div { "管理员面板" }
} else if role() == "editor" {
div { "编辑面板" }
} else {
div { "普通用户面板" }
}
// === 提前计算 Element 再插入 ===
// 适合逻辑较复杂的场景
{
let panel = if logged_in() {
rsx! { div { "已登录面板" } }
} else {
rsx! { div { "未登录面板" } }
};
panel
}
}
}
}
知识点:列表渲染 — for 循环
use dioxus::prelude::*;
#[component]
fn ListDemo() -> Element {
let items = use_signal(|| vec!["苹果", "香蕉", "橙子"]);
let numbers = vec![1, 2, 3, 4, 5];
rsx! {
// === 基本 for 循环 ===
ul {
for item in items.iter() {
// key 属性帮助 diff 算法识别元素身份
// 在列表重排时保持状态不丢失
li {
key: "{item}",
"{item}"
}
}
}
// === for 循环中渲染组件 ===
div {
for n in &numbers {
div {
key: "{n}",
"数字:{n},平方:{n * n}"
}
}
}
// === for 循环中做复杂计算 ===
ul {
for item in items.iter() {
{
let label = format!("🍎 {}", item.to_uppercase());
rsx! {
li {
key: "{item}",
"{label}"
}
}
}
}
}
// === 迭代器直接渲染 ===
div {
{(0..10).map(|i| rsx! {
span {
key: "{i}",
" {i} "
}
})}
}
}
}
知识点:事件处理
use dioxus::prelude::*;
#[component]
fn EventDemo() -> Element {
let mut count = use_signal(|| 0);
let mut text = use_signal(|| String::from(""));
rsx! {
div {
// === 点击事件 ===
button {
onclick: move |_| count += 1,
"点击次数:{count}"
}
// === 输入事件(双向绑定)===
input {
value: "{text}",
oninput: move |event| text.set(event.value()),
}
p { "你输入了:{text}" }
// === 事件对象 ===
button {
onclick: move |event| {
// event 包含鼠标位置、按键等信息
println!("点击坐标: {:?}", event.data.client_coordinates());
count += 1;
},
"带详情的按钮"
}
// === 表单提交 ===
form {
onsubmit: move |event| {
println!("提交的数据: {:?}", event.data.values());
},
input {
r#type: "text",
name: "username",
}
button {
r#type: "submit",
"提交"
}
}
}
}
}
知识点:信号(Signal)与响应式
use dioxus::prelude::*;
#[component]
fn SignalDemo() -> Element {
// use_signal 创建响应式状态
// 当信号值变化时,依赖它的 UI 自动更新
let mut count = use_signal(|| 0);
let mut name = use_signal(|| String::from("Rust"));
// 读取信号值:直接调用 ()
let current_count = count();
let current_name = name();
rsx! {
div {
// 在格式化字符串中直接使用信号
h1 { "计数器:{count}" }
h2 { "名字:{name}" }
// 修改信号值
button {
onclick: move |_| count += 1,
"+1"
}
button {
onclick: move |_| count -= 1,
"-1"
}
button {
onclick: move |_| count.set(0),
"重置"
}
// 修改字符串信号
input {
value: "{name}",
oninput: move |event| name.set(event.value()),
}
// 在子组件中传递信号(自动响应式)
ChildDisplay {
count: count,
}
}
}
}
#[component]
fn ChildDisplay(count: Signal<i32>) -> Element {
// 子组件接收 Signal 类型
// 当父组件的 count 变化时,这里也会自动更新
rsx! {
p { "子组件看到的计数:{count}" }
}
}
知识点:Fragment 与多根节点
use dioxus::prelude::*;
#[component]
fn FragmentDemo() -> Element {
// rsx! 可以返回多个根节点(Fragment)
// 不需要包裹在一个父元素中
rsx! {
h1 { "标题一" }
h2 { "标题二" }
p { "段落" }
}
}
等价于 React 中的 <>…</>
编译后生成一个 Fragment 节点,包含多个子节点
知识点:编译期优化原理
rsx! 宏在编译期完成以下工作:
这意味着:
- 属性类型错误在编译期就能发现(不是运行时)
- 静态文本和结构不会在每次渲染时重新分配
- 动态部分被标记,diff 算法只更新变化的节点对比其他方案:
| rsx! (Dioxus) | ✅ | ✅ | ✅ |
| html! (框架) | 部分 | 部分 | 部分 |
| 手写 VNode | ✅ | ✅ | ❌ |
| 模板文件 | ❌ | ❌ | 部分 |
核心规则
概念 说明
rsx! 声明式 UI 宏,类 JSX 语法,编译期转换为 VDOM
元素 元素名 { 属性, 子元素 }
属性 属性名: 值,支持静态、动态、可选、原始属性
文本 字符串字面量,支持 format! 风格的变量插值
表达式 {rust_expression},需实现 IntoDynNode
组件 大写开头的函数名,#[component] 标记
if / else 条件渲染,直接在 rsx 中写 Rust 条件分支
for 列表渲染,循环体解析为 rsx 标记
key 列表项唯一标识,帮助 diff 算法正确识别元素
事件 on 前缀属性,值为闭包(如 onclick: move \\ _\\ {})
Signal use_signal 创建响应式状态,变化自动触发 UI 更新
动手试试
补全下面的代码:
=== 题目1:个人资料卡片 ===
use dioxus::prelude::*;
// 补全:UserProfile 组件
// 接收以下属性:
// – name: String(用户名)
// – age: u32(年龄)
// – is_online: bool(是否在线)
// – hobbies: Vec<String>(爱好列表)
//
// 渲染要求:
// – 外层 div,class 为 "profile-card"
// – h2 显示用户名
// – p 显示年龄
// – 如果 is_online 为 true,显示绿色圆点 + "在线";否则显示灰色圆点 + "离线"
// – ul 列表显示所有爱好,每个 li 需要 key 属性
#[component]
fn UserProfile(name: String, age: u32, is_online: bool, hobbies: Vec<String>) -> Element {
// 补全
todo!()
}
=== 题目2:待办事项 ===
补全:TodoApp 组件
功能要求:
- 用 use_signal 管理待办列表 Vec 和输入框文本 String
- 输入框 + 添加按钮
- 输入框用 oninput 双向绑定到输入文本信号
- 添加按钮用 onclick 将输入文本加入列表(非空时),然后清空输入
- 用 for 循环渲染每个待办项(li),带 key 属性
- 每个待办项旁边有删除按钮,点击后从列表中移除
#[component]
fn TodoApp() -> Element {
// 补全
// 提示:
// let mut todos = use_signal(|| Vec::<String>::new());
// let mut input = use_signal(|| String::new());
todo!()
}
=== 题目3:选项卡组件 ===
补全:TabControl 组件
功能要求:
- 用 use_signal 管理当前选中的标签索引(usize)
- tabs: Vec 是标签名列表
- 顶部渲染一排按钮,每个按钮对应一个标签
- 选中的按钮 class 为 “tab active”,未选中为 “tab”
- 点击按钮切换选中标签
- 下方显示当前标签的内容(用 if/else if 或 match)
- 索引 0 → “首页内容”
- 索引 1 → “关于内容”
- 索引 2 → “设置内容”
- 其他 → “未知标签”
#[component]
fn TabControl(tabs: Vec<String>) -> Element {
// 补全
todo!()
}
=== 题目4:购物车 ===
//补全:CartItem 结构体和 CartApp 组件
struct CartItem {
name: String,
price: f64,
quantity: u32,
}
#[component]
fn CartApp() -> Element {
// 补全:
// – 用 use_signal 管理购物车 Vec<CartItem>
// – 初始化 3 个商品:
// ("Rust 编程书", 89.0, 1)
// ("机械键盘", 399.0, 1)
// ("鼠标垫", 49.0, 2)
// – 渲染商品列表,每个商品显示名称、单价、数量
// – 每个商品有 + 和 – 按钮修改数量(数量不能低于 0,为 0 时从列表移除)
// – 底部显示总价(用表达式计算)
todo!()
}
fn main() {
dioxus::launch(App);
}
#[component]
fn App() -> Element {
rsx! {
h1 { "第34课练习" }
// 测试 UserProfile
UserProfile {
name: "张三".to_string(),
age: 28,
is_online: true,
hobbies: vec!["编程".to_string(), "阅读".to_string(), "跑步".to_string()],
}
// 测试 TodoApp
TodoApp {}
// 测试 TabControl
TabControl {
tabs: vec!["首页".to_string(), "关于".to_string(), "设置".to_string()],
}
// 测试 CartApp
CartApp {}
}
}
期望效果:
- UserProfile:显示用户名、年龄、在线状态(绿色/灰色圆点)、爱好列表
- TodoApp:可输入待办事项并添加,每个事项可删除
- TabControl:点击标签切换内容,选中标签高亮
- CartApp:显示购物车商品列表,可增减数量,底部显示总价
贴代码
Dioxus 全套练习题完整参考答案(Dioxus‑0.7 稳定版语法)
Cargo.toml
[package]
name = "dioxus-exercise"
version = "0.1.0"
edition = "2021"
[dependencies]
dioxus = "0.7"
use dioxus::prelude::*;
// === 题目1:个人资料卡片 ===
#[component]
fn UserProfile(name: String, age: u32, is_online: bool, hobbies: Vec<String>) -> Element {
rsx! {
div { class: "profile-card",
h2 { "{name}" }
p { "年龄:{age}" }
div {
span {
style: if is_online { "color:green" } else { "color:gray" },
"● "
}
span { if is_online { "在线" } else { "离线" } }
}
ul {
for hobby in hobbies {
li { key: "{hobby}", "{hobby}" }
}
}
}
}
}
// === 题目2:待办事项 ===
#[component]
fn TodoApp() -> Element {
let mut todos = use_signal(|| Vec::<String>::new());
let mut input = use_signal(|| String::new());
rsx! {
div {
h3 { "待办事项" }
input {
value: "{input}",
oninput: move |e| input.set(e.value()),
placeholder: "请输入待办…"
}
button {
onclick: move |_| {
let text = input().trim().to_string();
if !text.is_empty() {
todos.push(text);
input.set(String::new());
}
},
"添加"
}
ul {
for (idx, item) in todos().iter().enumerate() {
li { key: "{idx}",
"{item}"
button {
onclick: move |_| {
todos.remove(idx);
},
style: "margin-left:8px;color:red",
"删除"
}
}
}
}
}
}
}
// === 题目3:选项卡组件 ===
#[component]
fn TabControl(tabs: Vec<String>) -> Element {
let mut active = use_signal(|| 0usize);
rsx! {
div {
div { class: "tab-bar",
for (i, tab_name) in tabs.iter().enumerate() {
button {
class: if active() == i { "tab active" } else { "tab" },
onclick: move |_| active.set(i),
"{tab_name}"
}
}
}
div { class: "tab-content",
match active() {
0 => "首页内容",
1 => "关于内容",
2 => "设置内容",
_ => "未知标签"
}
}
}
}
}
// === 题目4:购物车 ===
#[derive(Clone)]
struct CartItem {
name: String,
price: f64,
quantity: u32,
}
#[component]
fn CartApp() -> Element {
let mut cart = use_signal(|| {
vec![
CartItem { name: "Rust 编程书".into(), price: 89.0, quantity: 1 },
CartItem { name: "机械键盘".into(), price: 399.0, quantity: 1 },
CartItem { name: "鼠标垫".into(), price: 49.0, quantity: 2 },
]
});
let total: f64 = cart().iter().map(|item| item.price * (item.quantity as f64)).sum();
rsx! {
div {
h3 { "购物车" }
for (idx, item) in cart().iter().enumerate() {
div { key: "{idx}", style: "margin:6px 0",
span { "{item.name} 单价:{item.price} 数量:{item.quantity}" }
button {
onclick: move |_| {
cart.modify(|items| {
items[idx].quantity += 1;
});
},
style: "margin:0 4px",
"+"
}
button {
onclick: move |_| {
cart.modify(|items| {
items[idx].quantity -= 1;
if items[idx].quantity == 0 {
items.remove(idx);
}
});
},
"-"
}
}
}
div { style: "font-weight:bold;margin-top:10px",
"总价:{total:.2f}"
}
}
}
}
fn main() {
dioxus::launch(App);
}
#[component]
fn App() -> Element {
rsx! {
h1 { "第34课练习" }
// 测试 UserProfile
UserProfile {
name: "张三".to_string(),
age: 28,
is_online: true,
hobbies: vec!["编程".to_string(), "阅读".to_string(), "跑步".to_string()],
}
// 测试 TodoApp
TodoApp {}
// 测试 TabControl
TabControl {
tabs: vec!["首页".to_string(), "关于".to_string(), "设置".to_string()],
}
// 测试 CartApp
CartApp {}
}
}
关键知识点说明
可选样式补充(放到App,美化界面)
style { r#"
.profile-card { border:1px solid #ccc; padding:12px; border-radius:8px; margin:8px 0; }
.tab-bar button.tab { padding:6px 12px; border:none; background:#eee;margin-right:4px; }
.tab-bar button.tab.active { background:#2563eb;color:white; }
.tab-content { padding:10px;border:1px solid #ccc;margin-top:6px; }
"# }


