Rust跨平台UI框架Dioxus的使用:构建高性能Web、桌面和移动应用的React式组件库

Dioxus是一个用于在Rust中构建跨平台应用程序的框架。通过一个代码库,您可以使用全栈服务器功能构建Web、桌面和移动应用程序。Dioxus的设计旨在让熟悉HTML、CSS和JavaScript等Web技术的开发人员易于学习。

概览

Dioxus是一个跨平台应用程序框架,使开发人员能够使用Rust构建美观、快速、类型安全的应用程序。默认情况下,Dioxus应用程序使用HTML和CSS声明。Dioxus包含许多有用的功能:

  • RSX标记和资源的热重载
  • 具有日志记录、项目模板、linting等的交互式CLI
  • 用于部署到Web、macOS、Linux和Windows的集成捆绑器
  • 支持现代Web功能,如SSR、Hydration和HTML流
  • 通过JNI(Android)、CoreFoundation(Apple)和web-sys(Web)直接访问系统API
  • 类型安全的应用程序路由和服务器功能

快速开始

要开始使用Dioxus,您需要获取dioxus-cli工具:dx。我们使用cargo-binstall分发dx - 如果您已经安装了binstall,请跳过此步骤。

# 如果您已经安装了cargo-binstall,请跳过
cargo install cargo-binstall

# 安装预编译的`dx`工具
cargo binstall dioxus-cli

# 创建一个新应用程序,遵循模板
dx new my-app && cd my-app

# 然后启动!
dx serve --platform desktop

您的第一个应用程序

所有Dioxus应用程序都是通过组合返回Element的函数来构建的。

要启动应用程序,我们使用launch方法。在启动函数中,我们传递应用程序的根Component

use dioxus::prelude::*;

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

// #[component]属性简化了组件创建。
// 它不是必需的,但强烈推荐。它会检查不正确的组件定义并帮助您创建props结构体。
#[component]
fn App() -> Element {
    rsx! { "hello world!" }
}

元素和您的第一个组件

您可以使用rsx!宏以类似jsx的语法创建元素。 rsx!中的任何元素都可以有属性、监听器和子元素。为了 一致性,我们强制所有属性和监听器在子元素之前列出。

# use dioxus::prelude::*;
let value = "123";

rsx! {
    div {
        class: "my-class {value}",                  // <--- 属性
        onclick: move |_| println!("clicked!"),   // <--- 监听器
        h1 { "hello world" }                       // <--- 子元素
    }
};

rsx!宏接受"结构体形式"的属性。任何包含在花括号中并实现IntoDynNode的Rust表达式都将被解析为子元素。我们有两个例外:for循环和if语句都被解析,它们的体被解析为rsx节点。

# use dioxus::prelude::*;
rsx! {
    div {
        for _ in 0..10 {
            span { "hello world" }
        }
    }
};

将所有内容放在一起,我们可以编写一个简单的组件来渲染元素列表:

# use dioxus::prelude::*;
#[component]
fn App() -> Element {
    let name = "dave";
    rsx! {
        h1 { "Hello, {name}!" }
        div { class: "my-class", id: "my-id",
            for i in 0..5 {
                div { "FizzBuzz: {i}" }
            }
        }
    }
}

组件

我们可以组合这些函数组件来构建复杂的应用程序。每个新组件都接受一些属性。对于没有显式属性的组件,我们可以完全省略类型。

在Dioxus中,所有属性默认都使用ClonePartialEq进行记忆化。对于无法克隆的props,只需将字段包装在ReadOnlySignal中,Dioxus将为您处理类型转换。

# use dioxus::prelude::*;
# #[component] fn Header(title: String, color: String) -> Element { todo!() }
#[component]
fn App() -> Element {
    rsx! {
        Header {
            title: "My App",
            color: "red",
        }
    }
}

#[component]宏将帮助我们自动为组件创建props结构体:

# use dioxus::prelude::*;
// 组件宏将我们函数的参数转换为可以在rsx中传递给组件的命名字段
#[component]
fn Header(title: String, color: String) -> Element {
    rsx! {
        div {
            background_color: "{color}",
            h1 { "{title}" }
        }
    }
}

Hooks

虽然组件是UI元素的可重用形式,但hooks是逻辑的可重用形式。Hooks提供了一种从Dioxus内部的Scope检索状态并使用它来渲染UI元素的方法。

按照约定,所有hooks都是以use_开头的函数。我们可以使用hooks来定义状态并从监听器内部修改它。

# use dioxus::prelude::*;
#[component]
fn App() -> Element {
    // use signal hook在组件创建时运行一次,然后在第一次运行后返回当前值
    let name = use_signal(|| "world");

    rsx! { "hello {name}!" }
}

Hooks对它们的使用方式很敏感。要使用hooks,您必须遵守"hooks规则":

  • Hooks只能在组件或另一个hook的主体中调用。不能在另一个表达式(如循环、条件或函数调用)内部调用。
  • Hooks应以"use_"开头

Hooks让我们可以向组件添加状态字段,而无需声明显式状态结构体。然而,这意味着我们需要以正确的顺序"加载"结构体。如果顺序错误,hook将选择错误的状态并panic。

Dioxus包含许多内置hooks,您可以在组件中使用。如果这些hooks不符合您的用例,您还可以使用自定义hooks扩展Dioxus。

将所有内容放在一起

使用组件、rsx和hooks,我们可以构建一个简单的应用程序。

use dioxus::prelude::*;

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

#[component]
fn App() -> Element {
    let mut count = use_signal(|| 0);

    rsx! {
        div { "Count: {count}" }
        button { onclick: move |_| count += 1, "Increment" }
        button { onclick: move |_| count -= 1, "Decrement" }
    }
}

结论

此概述并未涵盖所有内容。请务必查看官方网站上的教程和指南以获取更多详细信息。

除了此概述之外,Dioxus还支持:

  • 服务器端渲染
  • 并发渲染(支持异步)
  • Web/桌面/移动支持
  • 预渲染和水合
  • 片段和Suspense
  • 内联样式
  • 自定义事件处理程序
  • 自定义元素
  • 基本细粒度响应性(如SolidJS/Svelte)
  • 以及更多!

构建酷炫的东西 ✌️

完整示例代码

use dioxus::prelude::*;

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

#[component]
fn App() -> Element {
    let mut count = use_signal(|| 0);

    rsx! {
        div {
            h1 { "Dioxus Counter App" }
            p { "Current count: {count}" }
            button {
                onclick: move |_| count += 1,
                "Increment"
            }
            button {
                onclick: move |_| count -= 1,
                "Decrement"
            }
            button {
                onclick: move |_| count.set(0),
                "Reset"
            }
        }
    }
}

1 回复

Dioxus:跨平台UI框架的使用指南

框架介绍

Dioxus是一个基于Rust语言构建的声明式、React式UI框架,支持跨平台开发。它允许开发者使用相同的代码库构建高性能的Web应用、桌面应用和移动应用。Dioxus采用虚拟DOM技术,提供类似React的开发体验,同时利用Rust的内存安全性和高性能特性。

核心特性

  • 跨平台支持:一套代码可编译为Web、桌面(通过TAURI)和移动端应用
  • React式语法:使用类似JSX的RSX语法编写组件
  • 高性能:得益于Rust的零成本抽象和高效的内存管理
  • 类型安全:完整的类型系统保障应用稳定性
  • 热重载:开发时支持快速重新渲染

安装与设置

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

[dependencies]
dioxus = "0.3"
dioxus-web = "0.3"  # Web平台
# 或 dioxus-desktop = "0.3"  # 桌面平台
# 或 dioxus-mobile = "0.3"   # 移动平台

基础示例

use dioxus::prelude::*;

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

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

组件开发示例

fn UserCard(cx: Scope, props: UserCardProps) -> Element {
    cx.render(rsx! {
        div {
            class: "user-card",
            img { src: "{props.avatar}", alt: "用户头像" }
            h2 { "{props.name}" }
            p { "{props.bio}" }
        }
    })
}

#[derive(Props)]
struct UserCardProps {
    name: String,
    avatar: String,
    bio: String,
}

状态管理

fn TodoApp(cx: Scope) -> Element {
    let mut todos = use_ref(cx, Vec::new);
    let mut new_todo = use_state(cx, || String::new());

    cx.render(rsx! {
        div {
            h1 { "待办事项列表" }
            
            input {
                value: "{new_todo}",
                oninput: move |e| new_todo.set(e.value.clone()),
            }
            
            button {
                onclick: move |_| {
                    todos.write().push(new_todo.get().clone());
                    new_todo.set(String::new());
                },
                "添加"
            }
            
            ul {
                for todo in todos.read().iter() {
                    li { key: "{todo}", "{todo}" }
                }
            }
        }
    })
}

跨平台编译

针对不同平台的编译命令:

# Web平台
cargo dioxus serve

# 桌面平台(需要TAURI)
cargo tauri dev

# 移动平台
cargo mobile init

最佳实践

  1. 组件拆分:将UI拆分为小型可复用组件
  2. 状态提升:将状态提升到合适的父组件
  3. 错误处理:使用Result和Option正确处理可能失败的操作
  4. 性能优化:使用mem::replace避免不必要的克隆
  5. 条件渲染:利用Rust的if表达式进行条件渲染

Dioxus为Rust开发者提供了现代化的UI开发体验,结合了React的开发模式和Rust的性能优势,是构建跨平台应用的优秀选择。

完整示例demo

use dioxus::prelude::*;

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

// 主应用组件
fn App(cx: Scope) -> Element {
    // 使用状态钩子管理计数
    let mut count = use_state(cx, || 0);
    
    cx.render(rsx! {
        div {
            // 用户信息卡片组件
            UserCard {
                name: "张三".to_string(),
                avatar: "https://example.com/avatar.png".to_string(),
                bio: "Rust开发者".to_string()
            }
            
            h1 { "计数器: {count}" }
            button { 
                onclick: move |_| count += 1, 
                "增加" 
            }
            button { 
                onclick: move |_| count -= 1, 
                "减少" 
            }
            
            // 待办事项组件
            TodoApp {}
        }
    })
}

// 用户卡片组件
fn UserCard(cx: Scope, props: UserCardProps) -> Element {
    cx.render(rsx! {
        div {
            class: "user-card",
            img { 
                src: "{props.avatar}", 
                alt: "用户头像" 
            }
            h2 { "{props.name}" }
            p { "{props.bio}" }
        }
    })
}

// 用户卡片属性结构
#[derive(Props)]
struct UserCardProps {
    name: String,
    avatar: String,
    bio: String,
}

// 待办事项应用组件
fn TodoApp(cx: Scope) -> Element {
    // 使用引用钩子管理待办事项列表
    let mut todos = use_ref(cx, Vec::new);
    // 使用状态钩子管理新待办事项输入
    let mut new_todo = use_state(cx, || String::new());

    cx.render(rsx! {
        div {
            h1 { "待办事项列表" }
            
            input {
                value: "{new_todo}",
                oninput: move |e| new_todo.set(e.value.clone()),
                placeholder: "输入新的待办事项"
            }
            
            button {
                onclick: move |_| {
                    if !new_todo.get().is_empty() {
                        todos.write().push(new_todo.get().clone());
                        new_todo.set(String::new());
                    }
                },
                "添加待办"
            }
            
            ul {
                for (index, todo) in todos.read().iter().enumerate() {
                    li { 
                        key: "{index}",
                        "{todo}" 
                    }
                }
            }
        }
    })
}
回到顶部