Rust前端开发库Dioxus-html的使用:构建高效、类型安全的Web与桌面应用UI组件

Rust前端开发库Dioxus-html的使用:构建高效、类型安全的Web与桌面应用UI组件

Crates.io MIT licensed Build Status Discord chat

概述

Dioxus的rsx!宏可以接受任何编译时正确的命名空间,这些命名空间位于NodeFactory之上。这个crate提供了HTML(和SVG)命名空间,它们被导入到Dioxus的prelude中。

然而,这种抽象使您能够添加任何元素命名空间,只要它们在调用rsx!时在作用域内。例如,为增强现实设计的UI可能使用与HTML不同的原语:

use ar_namespace::*;

rsx! {
    magic_div {
        magic_header {}
        magic_paragraph {
            on_magic_click: move |event| {
                //
            }
        }
    }
}

这是Dioxus目前尚未深入探索的部分。然而,命名空间系统确实使得提供语法高亮、文档、"转到定义"和编译时正确性成为可能,因此值得将其抽象化。

工作原理

Dioxus的元素必须实现(简单的)DioxusElement trait才能在rsx!宏中使用。

struct div;
impl DioxusElement for div {
    const TAG_NAME: &'static str = "div";
    const NAME_SPACE: Option<&'static str> = None;
}

所有元素都应定义为零大小结构体(也称为单元结构体)。这些结构体是零成本的,只是为Rust提供类型级别的技巧,以实现编译时正确的模板。

属性然后作为常量在这些单元结构体上实现。

HTML命名空间主要使用宏定义。然而,展开后的形式大致如下:

struct base;
impl DioxusElement for base {
    const TAG_NAME: &'static str = "base";
    const NAME_SPACE: Option<&'static str> = None;
}
impl base {
    const href: (&'static str, Option<'static str>, bool) = ("href", None, false);
    const target: (&'static str, Option<'static str>, bool) = ("target", None, false);
}

由于属性被定义为单元结构体上的方法,它们将属性创建保护在编译时正确的接口后面。

如何扩展

每当调用rsx!宏时,它依赖于作用域内的dioxus_elements模块。当您在dioxus中启用html功能时,此模块会被导入到prelude中。但是,您可以通过创建自己的dioxus_elements模块并重新导出html命名空间来扩展它。

mod dioxus_elements {
    use dioxus::prelude::dioxus_elements::*;
    struct my_element;
    impl DioxusElement for my_element {
        const TAG_NAME: &'static str = "base";
        const NAME_SPACE: Option<&'static str> = None;
    }
}

完整示例代码

use dioxus::prelude::*;

fn main() {
    dioxus::desktop::launch(app);
}

fn app(cx: Scope) -> Element {
    cx.render(rsx! {
        div {
            h1 { "欢迎使用Dioxus-html" }
            p { "这是一个使用Dioxus构建的高效、类型安全的UI组件示例" }
            
            // 按钮组件示例
            button {
                onclick: move |_| println!("按钮被点击了!"),
                "点击我"
            }
            
            // 输入框组件示例
            input {
                r#type: "text",
                placeholder: "输入一些文本...",
                oninput: move |event| println!("输入内容: {}", event.value)
            }
            
            // 列表组件示例
            ul {
                li { "列表项 1" }
                li { "列表项 2" }
                li { "列表项 3" }
            }
        }
    })
}

贡献

  • 在我们的问题跟踪器上报告问题。
  • 加入discord并提问!

许可证

此项目根据MIT许可证授权。

除非您明确说明,否则您为包含在Dioxus中而有意提交的任何贡献都应被许可为MIT,没有任何附加条款或条件。


完整示例demo

以下是一个更完整的Dioxus-html使用示例,包含状态管理和更复杂的UI组件:

use dioxus::prelude::*;

fn main() {
    // 启动桌面应用
    dioxus::desktop::launch(app);
}

fn app(cx: Scope) -> Element {
    // 使用use_state钩子管理状态
    let mut count = use_state(cx, || 0);
    let mut input_text = use_state(cx, || String::new());
    
    cx.render(rsx! {
        div {
            style: "font-family: Arial, sans-serif; max-width: 800px; margin: 0 auto; padding: 20px;",
            
            // 标题区域
            header {
                h1 { 
                    style: "color: #333; text-align: center;",
                    "Dioxus-html 示例应用"
                }
                p {
                    style: "text-align: center; color: #666;",
                    "构建高效、类型安全的Web与桌面应用UI组件"
                }
            }
            
            // 计数器组件
            section {
                style: "background: #f5f5f5; padding: 20px; border-radius: 8px; margin: 20px 0;",
                
                h2 { "计数器示例" }
                p { "当前计数: {count}" }
                
                div {
                    style: "display: flex; gap: 10px; margin-top: 10px;",
                    
                    // 增加按钮
                    button {
                        style: "padding: 10px 20px; background: #007bff; color: white; border: none; border-radius: 4px; cursor: pointer;",
                        onclick: move |_| count += 1,
                        "+ 增加"
                    }
                    
                    // 减少按钮
                    button {
                        style: "padding: 10px 20px; background: #dc3545; color: white; border: none; border-radius: 4px; cursor: pointer;",
                        onclick: move |_| count -= 1,
                        "- 减少"
                    }
                    
                    // 重置按钮
                    button {
                        style: "padding: 10px 20px; background: #6c757d; color: white; border: none; border-radius: 4px; cursor: pointer;",
                        onclick: move |_| count.set(0),
                        "重置"
                    }
                }
            }
            
            // 输入表单组件
            section {
                style: "background: #f8f9fa; padding: 20px; border-radius: 8px; margin: 20px 0;",
                
                h2 { "表单输入示例" }
                p { "您输入的内容: {input_text}" }
                
                // 文本输入框
                input {
                    style: "width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 4px; margin-bottom: 10px;",
                    r#type: "text",
                    placeholder: "请输入文本...",
                    value: "{input_text}",
                    oninput: move |event| input_text.set(event.value.clone())
                }
                
                // 提交按钮
                button {
                    style: "padding: 10px 20px; background: #28a745; color: white; border: none; border-radius: 4px; cursor: pointer;",
                    onclick: move |_| {
                        println!("提交的文本: {}", input_text);
                        input_text.set(String::new());
                    },
                    "提交"
                }
            }
            
            // 列表组件
            section {
                style: "background: #fff; padding: 20px; border-radius: 8px; margin: 20px 0; border: 1px solid #eee;",
                
                h2 { "动态列表示例" }
                
                ul {
                    style: "list-style: none; padding: 0;",
                    
                    // 动态生成列表项
                    (0..5).map(|i| rsx! {
                        li {
                            key: "{i}",
                            style: "padding: 10px; border-bottom: 1px solid #eee; display: flex; justify-content: space-between; align-items: center;",
                            
                            span { "列表项 {i + 1}" }
                            button {
                                style: "padding: 5px 10px; background: #ffc107; color: #333; border: none; border-radius: 3px; cursor: pointer;",
                                onclick: move |_| println!("点击了列表项 {}", i + 1),
                                "操作"
                            }
                        }
                    })
                }
            }
            
            // 页脚
            footer {
                style: "text-align: center; margin-top: 40px; padding: 20px; color: #6c757d; border-top: 1px solid #eee;",
                
                p { "使用 Dioxus-html 构建 © 2023" }
            }
        }
    })
}

这个完整示例展示了Dioxus-html的主要特性:

  1. 状态管理:使用use_state钩子管理组件状态
  2. 事件处理:处理按钮点击和输入事件
  3. 样式设置:通过style属性设置内联样式
  4. 条件渲染:根据状态动态渲染内容
  5. 列表渲染:使用map方法动态生成列表项
  6. 组件组合:将多个HTML元素组合成复杂的UI组件

要运行此示例,需要在Cargo.toml中添加dioxus依赖:

[dependencies]
dioxus = { version = "0.3", features = ["desktop"] }

1 回复

Dioxus-HTML:构建高效、类型安全的Web与桌面应用UI组件

介绍

Dioxus-HTML是Rust生态中一个强大的前端开发库,专门用于构建高性能、类型安全的用户界面。它支持Web、桌面(通过TAURI)和移动端应用开发,提供类似React的声明式组件模型,同时充分利用Rust的强类型系统和内存安全特性。

核心特性

  • 声明式UI:采用类似JSX的RSX语法
  • 跨平台支持:一套代码可编译为Web、桌面和移动应用
  • 类型安全:编译时检查所有属性和事件处理程序
  • 高性能:基于虚拟DOM的高效差分算法
  • 组件化:可复用的组件架构

使用方法

基本安装

首先在Cargo.toml中添加依赖:

[dependencies]
dioxus = { version = "0.4", features = ["web"] }
dioxus-html = "0.4"

基础示例

use dioxus::prelude::*;

fn main() {
    dioxus_web::launch(App);
}

fn App(cx: Scope) -> Element {
    cx.render(rsx! {
        div {
            h1 { "欢迎使用Dioxus!" }
            Counter {}
        }
    })
}

fn Counter(cx: Scope) -> Element {
    let mut count = use_state(cx, || 0);
    
    cx.render(rsx! {
        div {
            p { "当前计数: {count}" }
            button {
                onclick: move |_| count += 1,
                "点击增加"
            }
            button {
                onclick: move |_| count -= 1,
                "点击减少"
            }
        }
    })
}

组件属性示例

#[derive(Props, PartialEq)]
struct ButtonProps {
    text: String,
    on_click: EventHandler,
}

fn CustomButton(cx: Scope<ButtonProps>) -> Element {
    cx.render(rsx! {
        button {
            class: "custom-btn",
            onclick: move |event| cx.props.on_click.call(event),
            "{cx.props.text}"
        }
    })
}

事件处理

fn InteractiveComponent(cx: Scope) -> Element {
    let mut text = use_state(cx, || String::new());
    
    cx.render(rsx! {
        div {
            input {
                r#type: "text",
                value: "{text}",
                oninput: move |e| text.set(e.value.clone())
            }
            p { "你输入了: {text}" }
        }
    })
}

条件渲染

fn ConditionalComponent(cx: Scope) -> Element {
    let is_visible = use_state(cx, || true);
    
    cx.render(rsx! {
        div {
            button {
                onclick: move |_| is_visible.set(!is_visible),
                "切换显示"
            }
            if *is_visible {
                rsx! { p { "这个内容可以显示或隐藏" } }
            }
        }
    })
}

完整示例demo

use dioxus::prelude::*;

fn main() {
    // 启动Dioxus Web应用
    dioxus_web::launch(App);
}

// 主应用组件
fn App(cx: Scope) -> Element {
    cx.render(rsx! {
        div {
            // 应用标题
            h1 { "Dioxus示例应用" }
            
            // 计数器组件
            Counter {}
            
            // 交互式输入组件
            InteractiveComponent {}
            
            // 条件渲染组件
            ConditionalComponent {}
            
            // 自定义按钮组件
            CustomButton {
                text: "自定义按钮".to_string(),
                on_click: |_| println!("按钮被点击了!")
            }
        }
    })
}

// 计数器组件
fn Counter(cx: Scope) -> Element {
    // 使用use_state hook管理计数状态
    let mut count = use_state(cx, || 0);
    
    cx.render(rsx! {
        div {
            h2 { "计数器示例" }
            p { "当前计数: {count}" }
            button {
                onclick: move |_| count += 1,  // 增加计数
                "点击增加"
            }
            button {
                onclick: move |_| count -= 1,  // 减少计数
                "点击减少"
            }
            button {
                onclick: move |_| count.set(0),  // 重置计数
                "重置"
            }
        }
    })
}

// 交互式输入组件
fn InteractiveComponent(cx: Scope) -> Element {
    // 使用use_state hook管理文本输入状态
    let mut text = use_state(cx, || String::new());
    
    cx.render(rsx! {
        div {
            h2 { "输入框示例" }
            input {
                r#type: "text",
                placeholder: "请输入文本",
                value: "{text}",
                oninput: move |e| text.set(e.value.clone())  // 更新文本状态
            }
            p { "实时预览: {text}" }
            p { 
                "字符数: {}", 
                text.len()  // 显示字符数量
            }
        }
    })
}

// 条件渲染组件
fn ConditionalComponent(cx: Scope) -> Element {
    // 使用use_state hook管理显示状态
    let is_visible = use_state(cx, || true);
    
    cx.render(rsx! {
        div {
            h2 { "条件渲染示例" }
            button {
                onclick: move |_| is_visible.set(!is_visible),  // 切换显示状态
                if *is_visible { "隐藏内容" } else { "显示内容" }
            }
            // 条件渲染段落
            if *is_visible {
                rsx! { 
                    p { 
                        class: "content",
                        "这个内容可以根据按钮点击来显示或隐藏。条件渲染是构建动态UI的重要特性。" 
                    }
                }
            }
        }
    })
}

// 自定义按钮属性结构体
#[derive(Props, PartialEq)]
struct ButtonProps {
    text: String,
    on_click: EventHandler,
}

// 自定义按钮组件
fn CustomButton(cx: Scope<ButtonProps>) -> Element {
    cx.render(rsx! {
        div {
            h2 { "自定义组件示例" }
            button {
                class: "custom-button",
                style: "padding: 10px 20px; background-color: #007bff; color: white; border: none; border-radius: 5px;",
                onclick: move |event| cx.props.on_click.call(event),  // 调用父组件传递的事件处理函数
                "{cx.props.text}"
            }
        }
    })
}

最佳实践

  1. 组件拆分:将UI拆分为小型可复用组件
  2. 状态管理:合理使用use_state和use_ref管理组件状态
  3. 性能优化:使用should_render避免不必要的重渲染
  4. 错误处理:充分利用Rust的Result类型进行错误处理

注意事项

  • 确保所有事件处理函数都正确指定类型
  • 使用derive(Props)时确保实现PartialEq trait
  • 在Web平台注意CSS样式的正确引入方式

Dioxus-HTML为Rust开发者提供了构建现代用户界面的强大工具,结合Rust的类型安全特性和高性能特点,是开发跨平台应用的优秀选择。

回到顶部