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

知识点:RSX 是什么

定义:

RSX(Rust Syntax Extension)是 Dioxus 框架的核心宏

作用:

用类 JSX 的语法在 Rust 中描述 UI 结构

它在编译期被转换为虚拟 DOM(VDOM)节点构建代码

与 HTML 模板的区别:

RSX 直接活在 Rust 语法环境中,花括号里是普通 Rust 表达式

编译期类型检查(属性类型、事件处理器签名)

无需额外模板引擎或构建工具

依赖Cargo.toml:

toml 复制代码
 [dependencies]
dioxus = "0.7"

注意:

Dioxus 目前处于 0.7版本,API 可能在后续版本中有所变化

rust 复制代码
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! 宏在编译期的转换过程(简化版):

编译前:

rust 复制代码
rsx! {
     div {
         class: "container",
         h1 { "Hello {name}" }
     }
 }

编译后(简化):

rust 复制代码
VNode::new([
     VElement::new("div",
         [Attribute::new("class", "container")],
         [VNode::new([
             VElement::new("h1", [],
                 [VText::new(format!("Hello {}", name))]
             )
         ])
     ])
 ])

知识点:元素与嵌套

rust 复制代码
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 {
            "自定义组件内容"
        }
    }
}

知识点:属性系统

rust 复制代码
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;",
            "内联样式"
        }
    }
}

知识点:文本与表达式

rust 复制代码
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} " } })}
        }
    }
}

知识点:组件调用

rust 复制代码
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

rust 复制代码
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 循环

rust 复制代码
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} "
                }
            })}
        }
    }
}

知识点:事件处理

rust 复制代码
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)与响应式

rust 复制代码
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 与多根节点

rust 复制代码
use dioxus::prelude::*;

#[component]
fn FragmentDemo() -> Element {
    // rsx! 可以返回多个根节点(Fragment)
    // 不需要包裹在一个父元素中
    rsx! {
        h1 { "标题一" }
        h2 { "标题二" }
        p { "段落" }
    }
}

等价于 React 中的 <>...</>

编译后生成一个 Fragment 节点,包含多个子节点

知识点:编译期优化原理

rsx! 宏在编译期完成以下工作:

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

这意味着:

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

rust 复制代码
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 属性
  • 每个待办项旁边有删除按钮,点击后从列表中移除
rust 复制代码
#[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 → "设置内容"
    • 其他 → "未知标签"
rust 复制代码
#[component]
fn TabControl(tabs: Vec<String>) -> Element {
    // 补全
    todo!()
}

=== 题目4:购物车 ===

rust 复制代码
 //补全: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

toml 复制代码
  
[package]
name = "dioxus-exercise"
version = "0.1.0"
edition = "2021"

[dependencies]
dioxus = "0.7"
 
rust 复制代码
  
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 {}
    }
}
 

关键知识点说明

  1. use_signal:Dioxus 的响应式状态, signal() 读、 .set() 写、 .modify() 原地修改集合;
  2. key 属性:列表渲染必须加 key,用于 diff 算法;
  3. 事件回调: onclick / oninput ,闭包捕获信号;
  4. modify:购物车这里用 cart.modify ,适合数组原地更新;
  5. 组件属性:普通参数直接声明,无需 Props 结构体(0.7 简化写法)。

可选样式补充(放到App,美化界面)

rust 复制代码
  
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; }
"# }
 
相关推荐
柯南46681 小时前
【AI开发之Rust】第 5 课:引用与生命周期 —— 借用能活多久?
rust
wuyk5551 小时前
18.Kruskal 算法:用最短的边连成一张网
开发语言·算法·图论
一条破秋裤1 小时前
24_MPU6050结构参数与寄存器基础
stm32·嵌入式硬件·学习
Evand J1 小时前
【MATLAB例程,图像降噪7】高斯加权滑动窗口滤波,用于图像降噪,带质量评价,附代码下载链接
开发语言·图像处理·matlab·滑动窗口·滤波·降噪·图像滤波
十年一梦惊觉醒2 小时前
快手视频发布助手,全自动视频发布带货工具
开发语言·前端·javascript
Java后端的Ai之路2 小时前
一文搞懂 GitHub Actions-CICD
开发语言·大模型·github·cicd·action
码匠许师傅2 小时前
【C++三方组件】开篇总论:为什么需要以及如何选型?
开发语言·c++
cvby2 小时前
C++11--右值引用和移动语义
开发语言·c++
en.en..2 小时前
TCP/UDP 收发流程(网络字节序---函数讲解)
开发语言·php