云计算百科
云计算领域专业知识百科平台

【dioxus0.7基础语法学与练】第8课:RSX 宏 — Dioxus 声明式 UI 语法

知识点: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! 宏在编译期完成以下工作:

  • 语法解析:类 HTML 语法 → Rust AST
  • 类型检查:验证属性类型、事件处理器签名
  • 代码生成:生成 VirtualDOM 节点构建代码
  • 静态节点提取:将不变的部分提取为模板,运行时复用
  • 这意味着:

    • 属性类型错误在编译期就能发现(不是运行时)
    • 静态文本和结构不会在每次渲染时重新分配
    • 动态部分被标记,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 {}
    }
    }

    关键知识点说明

  • use_signal:Dioxus 的响应式状态, signal() 读、 .set() 写、 .modify() 原地修改集合;
  • key 属性:列表渲染必须加 key,用于 diff 算法;
  • 事件回调: onclick / oninput ,闭包捕获信号;
  • modify:购物车这里用 cart.modify ,适合数组原地更新;
  • 组件属性:普通参数直接声明,无需 Props 结构体(0.7 简化写法)。
  • 可选样式补充(放到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; }
    "#
    }

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » 【dioxus0.7基础语法学与练】第8课:RSX 宏 — Dioxus 声明式 UI 语法
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!