Skip to content
On this page

Dioxus

1、简介

Dioxus是一个Rust库,用于构建在桌面Web移动设备等上运行的应用程序。

2、官网

官网:https://dioxuslabs.com GitHub:https://github.com/dioxuslabs/dioxus 文档:https://dioxuslabs.com/docs

3、特点

  • 基于Rust语言开发,性能优异
  • 跨平台:支持WebAssembly桌面移动设备
  • 组件化:支持组件路由状态管理
  • 高效:支持SSRSSG预渲染
  • 易用:支持TypeScriptRustJSX

4、使用

1. 安装

bash
cargo install dioxus-cli

# 没有安装wasm32-unknown-unknown工具链,则需要先安装
rustup target add wasm32-unknown-unknown

2. 创建项目

2.1 创建项目

bash
cargo new --bin dioxus-app
cd dioxus-app

2.2 添加依赖

bash
cargo add dioxus
cargo add dioxus-web

Cargo.toml

js
[dependencies]
console_error_panic_hook = "0.1.6"
dioxus = "0.4.0"
dioxus-web = "0.4.0"
tracing = "0.1"
tracing-wasm = "0.2"
wasm-bindgen = "0.2"
chrono = { version = "0.4", features = ["serde"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
reqwest = { version = "0.11", features = ["json"] }
futures = "0.3"

2.3、 编写代码

rust
// main.rs
use dioxus::prelude::*;

fn main() {
    console_error_panic_hook::set_once();
    tracing_wasm::set_as_global_default();
    dioxus_web::launch(app)
}

fn app(cx: Scope) -> Element {
    cx.render(rsx! {
        div {
            "Hello, world!"
        }
    })
}

2.4、 运行项目

bash
dx serve

2.5、 构建项目

bash
dx build

3、dioxus-cli

3.1、热更新

bash
dx serve --hot-reload

3.2、启动服务

bash
dx serve

3.3、指定平台特性

bash
dx serve --hot-reload --platform=web --no-default-features --features=web

3.4、构建项目

bash

4、dioxus模版语法

4.1、cx.render render! rsx! 渲染模板

js
fn app(cx: Scope) -> Element {
    cx.render(rsx! {
        div {
            "Hello, world!"
        }
    })
}
js
fn app(cx: Scope) -> Element {
    render!(
        div {
            "Hello, world!!!"
        }
    )
    render!{
        div {
            "Hello, world!!!"
        }
    }
}
js
fn app(cx: Scope) -> Element {
    rsx!{
        title_component {}
    }
    rsx!(title_component {})
}

4.2、模版参数

js
fn app(cx: Scope) -> Element {
    let title = "title";
    let by = "author";
    let score = 0;
    let comments = "comments";
    info
    render!(
        div {
          "{title} by {by} ({score}) {comments}"
        }
    )
}

4.3、样式attribute

js
fn app(cx: Scope) -> Element {
    let title = "title";
    let by = "author";
    let score = 0;
    let comments = "comments";
  
    render!(
        div {
          padding: "0.5rem", // style="padding: 0.5rem;"
          position: "relative",
          "{title} by {by} ({score}) {comments}"
        }
    )
}

class

js
fn app(cx: Scope) -> Element {
    let title = "title";
    let by = "author";
    let score = 0;
    let comments = "comments";
  
    render!(
        div {
          class="item-wrapper"
          "{title} by {by} ({score}) {comments}"
        }
    )
}

style

js
fn app(cx: Scope) -> Element {
    let item_wrapper = r#"
        padding: 0.5rem;
        position: relative;
    "
  
    render!(
        div {
          style="{item_wrapper}"
        }
    )
}


fn app(cx: Scope) -> Element {
    render! {
        div {
            style: "height: 20px; background-color: red;",
            "hello world" 
        }
    }
}

4.4、创建组件

js
use dioxus::prelude::*;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use tracing::info;

#[derive(Debug, Serialize, Deserialize)]
struct StoryPageData {
    #[serde(flatten)]
    pub item: StoryItem,
    #[serde(default)]
    pub comments: Vec<Comment>,
}

#[derive(Debug, Deserialize, Serialize, PartialEq)]
struct StoryItem {
    pub id: i64,
    pub title: String,
    pub url: Option<String>,
    pub text: Option<String>,
    #[serde(default)]
    pub by: String,
    #[serde(default)]
    pub score: i64,
    #[serde(default)]
    pub descendants: i64,
    #[serde(with = "chrono::serde::ts_seconds")]
    pub time: DateTime<Utc>,
    #[serde(default)]
    pub kids: Vec<i64>,
    pub r#type: String,
}

#[derive(Debug, Deserialize, Serialize)]
struct Comment {
    pub id: i64,
    pub title: String,
    pub url: Option<String>,
    pub text: Option<String>,
    #[serde(default)]
    pub by: String,
    #[serde(default)]
    pub score: i64,
    #[serde(default)]
    pub descendants: i64,
    #[serde(with = "chrono::serde::ts_seconds")]
    pub time: DateTime<Utc>,
    #[serde(default)]
    pub kids: Vec<i64>,
    pub r#type: String,
}

fn main() {
    console_error_panic_hook::set_once();
    tracing_wasm::set_as_global_default();
    dioxus_web::launch(app);
}

fn app(cx: Scope) -> Element {
    info!("hello world");
    render! {
        story_listing {
            story: StoryItem {
                id: 0,
                title: "hello hackernews".to_string(),
                url: None,
                text: None,
                by: "Author".to_string(),
                score: 0,
                descendants: 0,
                time: Utc::now(),
                kids: vec![],
                r#type: "".to_string(),
            }
        }
        title_component {}
        rsx!{
            title_component {}
        }
        rsx!(title_component {})
    }
}

fn title_component(cx: Scope) -> Element {
    render! {
        h1 {
            "title"
        }
    }
}

#[inline_props]
fn story_listing(cx: Scope, story: StoryItem) -> Element {
    let StoryItem {
        title,
        url,
        by,
        score,
        time,
        kids,
        id,
        ..
    } = story;
    let url = url.as_deref().unwrap_or_default();
    let hostname = url.trim_start_matches("https://")
        .trim_start_matches("http://")
        .trim_start_matches("www.");
    let score = format!("{score} {}", if *score == 1 { "point" } else { "points" });
    let comments = format!("{} {}", kids.len(), if kids.len() == 1 { "comment" } else { "comments" });
    let time = time.format("%Y-%m-%d %H:%M:%S");
    
    let caption_container_style = r#"
        font_size: "1.25rem",
        color: "gray",
    "#;

    render! {
        div {
          padding: "0.5rem",
          position: "relative",
          div {
              font_size: "1.5rem",
              a {
                  href: url,
                  "{title}"
              }
              a {
                  color: "gray",
                  href: "https://news.ycombinator.com/from?site={hostname}",
                  text_decoration: "none",
                  " {hostname}"
              }
          }
          div {
              class: "cl",
              style: "{caption_container_style}",
              div {
                  "{score}"
              }
              div {
                  padding_left: "0.5rem",
                  "by {by}"
              }
              div {
                  padding_left: "0.5rem",
                  "{time}"
              }
              div {
                  padding_left: "0.5rem",
                  "{comments}"
              }
          }
        }
    }
}

4.5、状态展示

js
fn preview_component(cx: Scope) -> Element {
    let preview_state = PreviewState::Loaded(StoryPageData {
        item: StoryItem {
            id: 0,
            title: "hello hackernews".to_string(),
            url: None,
            text: Some("hello world".into()),
            by: "Author".to_string(),
            score: 0,
            descendants: 0,
            time: chrono::Utc::now(),
            kids: vec![],
            r#type: "".to_string(),
          },
        comments: vec![],
    });
    match preview_state {
      PreviewState::Unset => render! {
          "Hover over a story to preview it here"
      },
      PreviewState::Loading => render! {
          "Loading..."
      },
      PreviewState::Loaded(story) => {
          let title = &story.item.title;
          let url = story.item.url.as_deref().unwrap_or_default();
          let text = story.item.text.as_deref().unwrap_or_default();
          render! {
              div {
                  padding: "0.5rem",
                  div {
                      font_size: "1.5rem",
                      a {
                          href: "{url}",
                          "{title}"
                      }
                  }
                  div {
                      dangerous_inner_html: "{text}",
                  }
                  for comment in &story.comments {
                      comment_component { comment: comment.clone() }
                  }
              }
          }
      }
  }
}

4.6、事件

js
render! {
    div {
        padding: "0.5rem",
        position: "relative",
        onmouseenter: move |_| {
            info!("div onMouseEnter");
        },
        div {
            font_size: "1.5rem",
            a {
                href: url,
                onfocus: move |e| {
                    info!("focus {e:?}");
                },
                "{title}"
            }
        }
    }
}

4.7、state状态共享

js
use dioxus::prelude::*;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use tracing::info;

#[derive(Debug, Serialize, Deserialize, Clone)]
struct StoryPageData {
    #[serde(flatten)]
    pub item: StoryItem,
    #[serde(default)]
    pub comments: Vec<Comment>,
}

#[derive(Debug, Deserialize, Serialize, PartialEq, Clone)]
struct StoryItem {
    pub id: i64,
    pub title: String,
    pub url: Option<String>,
    pub text: Option<String>,
    #[serde(default)]
    pub by: String,
    #[serde(default)]
    pub score: i64,
    #[serde(default)]
    pub descendants: i64,
    #[serde(with = "chrono::serde::ts_seconds")]
    pub time: DateTime<Utc>,
    #[serde(default)]
    pub kids: Vec<i64>,
    pub r#type: String,
}

#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
struct Comment {
    pub id: i64,
    pub title: String,
    pub url: Option<String>,
    pub text: Option<String>,
    #[serde(default)]
    pub by: String,
    #[serde(default)]
    pub score: i64,
    #[serde(default)]
    pub descendants: i64,
    #[serde(with = "chrono::serde::ts_seconds")]
    pub time: DateTime<Utc>,
    #[serde(default)]
    pub kids: Vec<i64>,
    pub r#type: String,
}

// New
#[derive(Clone, Debug)]
enum PreviewState {
    Unset,
    Loading,
    Loaded(StoryPageData),
}


fn main() {
    console_error_panic_hook::set_once();
    tracing_wasm::set_as_global_default();
    dioxus_web::launch(app);
}

fn app(cx: Scope) -> Element {
    use_shared_state_provider(cx, || PreviewState::Unset);

    render! {
        div {
          display: "flex",
          flex_direction: "row",
          width: "100%",
          div {
              width: "50%",
              stories_component {}
          }
          div {
              width: "50%",
              preview_component {}
          }
      }
    }
}

// New
fn stories_component(cx: Scope) -> Element {
    render! {
        story_listing {
            story: StoryItem {
                id: 0,
                title: "hello hackernews".to_string(),
                url: None,
                text: None,
                by: "Author".to_string(),
                score: 0,
                descendants: 0,
                time: chrono::Utc::now(),
                kids: vec![],
                r#type: "".to_string(),
            }
        }
    }
}

fn preview_component(cx: Scope) -> Element {
    let preview_state = use_shared_state::<PreviewState>(cx)?;
    match &*preview_state.read() {
      PreviewState::Unset => render! {
          "Hover over a story to preview it here"
      },
      PreviewState::Loading => render! {
          "Loading..."
      },
      PreviewState::Loaded(story) => {
          let title = &story.item.title;
          let url = story.item.url.as_deref().unwrap_or_default();
          let text = story.item.text.as_deref().unwrap_or_default();
          render! {
              div {
                  padding: "0.5rem",
                  div {
                      font_size: "1.5rem",
                      a {
                          href: "{url}",
                          "{title}"
                      }
                  }
                  div {
                      dangerous_inner_html: "{text}",
                  }
                  for comment in &story.comments {
                      comment_component { comment: comment.clone() }
                  }
              }
          }
      }
  }
}

#[inline_props]
fn comment_component(cx: Scope, comment: Comment) -> Element<'a> {
    let text = comment.text.as_deref().unwrap_or_default();
    render! {
        div {
            padding: "0.5rem",
            div {
                color: "gray",
                "by {comment.by}"
            }
            div {
                dangerous_inner_html: "{text}"
            }
        }
    }
}

#[inline_props]
fn story_listing(cx: Scope, story: StoryItem) -> Element {
    let preview_state = use_shared_state::<PreviewState>(cx)?;
    let StoryItem {
        title,
        url,
        by,
        score,
        time,
        kids,
        id,
        ..
    } = story;
    let url = url.as_deref().unwrap_or_default();
    let hostname = url.trim_start_matches("https://")
        .trim_start_matches("http://")
        .trim_start_matches("www.");
    let score = format!("{score} {}", if *score == 1 { "point" } else { "points" });
    let comments = format!("{} {}", kids.len(), if kids.len() == 1 { "comment" } else { "comments" });
    let time = time.format("%Y-%m-%d %H:%M:%S");
    
    let caption_container_style = r#"
        font_size: "1.25rem",
        color: "gray",
    "#;

    render! {
        div {
          padding: "0.5rem",
          position: "relative",
          onmouseenter: move |_| {
              info!("div onMouseEnter");
              *preview_state.write() = PreviewState::Loaded(StoryPageData {
                  item: story.clone(),
                  comments: vec![],
            });
          },
          div {
              font_size: "1.5rem",
              a {
                  href: url,
                  onfocus: move |e| {
                      info!("focus {e:?}");
                      *preview_state.write() = PreviewState::Loaded(StoryPageData {
                          item: story.clone(),
                          comments: vec![],
                      });
                  },
                  "{title}"
              }
              a {
                  color: "gray",
                  href: "https://news.ycombinator.com/from?site={hostname}",
                  text_decoration: "none",
                  " {hostname}"
              }
          }
          div {
              class: "cl",
              style: "{caption_container_style}",
              div {
                  "{score}"
              }
              div {
                  padding_left: "0.5rem",
                  "by {by}"
              }
              div {
                  padding_left: "0.5rem",
                  "{time}"
              }
              div {
                  padding_left: "0.5rem",
                  "{comments}"
              }
          }
        }
    }
}

4.8、异步请求

js
use tracing::{info, warn};
use futures::future::join_all;

async fn get_story_preview(id: i64) -> Result<StoryItem, reqwest::Error> {
    let url = format!("{}item/{}.json", BASE_API_URL, id);
    reqwest::get(&url)
       .await?
       .json()
       .await
}

async fn get_stories(count: usize) -> Result<Vec<StoryItem>, reqwest::Error> {
    let url = format!("{}topstories.json", BASE_API_URL);
    let stories_ids = &reqwest::get(&url)
        .await?
        .json::<Vec<i64>>()
        .await?[..count];

    let story_futures = stories_ids[..usize::min(stories_ids.len(), count)]
        .iter()
        .map(|&story_id| get_story_preview(story_id));

    let stories = join_all(story_futures)
        .await
        .into_iter()
        .filter_map(|story| story.ok())
        .collect();
    Ok(stories)
}

fn stories_component(cx: Scope) -> Element {
    let stories: &UseFuture<Result<Vec<StoryItem>, reqwest::Error>> = use_future(cx, (), |_e|get_stories(10));

    match stories.value() {
        Some(Ok(list)) => {
            render! {
                div {
                    for story in list {
                        story_listing {
                            story: story.clone()
                        }
                    }
                }              
            }
        },
        Some(Err(e)) => {
            warn!("An error occurred while fetching stories {e}");
            render! {
                "An error occurred while fetching stories"
            }
        },
        None => {
            info!("loading stories");
            render! {
                "Loading stories..."
            }
        }
    }
}

4.9、渲染html

js
fn app(cx: Scope) -> Element {
    render! {
        div {
            style: "height: 20px; background-color: red;",
            dangerous_inner_html: "<div>hello world!!!</div>" 
        }
    }
}
js
fn app(cx: Scope) -> Element {
    let contents = "live <b>dangerously</b>";
    cx.render(rsx! {
        div {
            dangerous_inner_html: "{contents}",
        }
    })
}

4.10、{}表达式

js
fn app(cx: Scope) -> Element {
    let name = "mz";
    render! {
        div {
            "hello world!!!! {name.to_uppercase()}" 
        }
        div {
            "{7*6}"
        }
    }
}
js
fn app(cx: Scope) -> Element {
    let name = "mz";
    render! {
        span {
            name.to_uppercase(),
            // create a list of text from 0 to 9
            (0..10).map(|i| rsx!{ i.to_string() })
        }
    }
}
js
fn app(cx: Scope) -> Element {
    render! {
        div {
            for i in 0..3 {
                div {
                   "div-{i}"
                }
            }
        }
        div {
            (0..3).map(|i| rsx! {
                div {
                    "div-{i}"
                }
              }
            )
            if true {
                rsx!{div { "true" }}
            }
        }
    }
}

4.11、行样式处理

js
fn app(cx: Scope) -> Element {
    render! {
        p {
            b {"Dioxus Labs"}
            " An Open Source project dedicated to making Rust UI wonderful."
        }
    }
}

4.12、props

4.12.1、props
js
#[derive(Props, PartialEq)]
struct LikesProps {
    score: i32,
}

fn likes_component(cx: Scope<LikesProps>) -> Element {
    render! {
      div {
          "This post has ",
          b { "{cx.props.score}" },
          " likes"
      }
    }  
}
4.12.2、props option
js
#[derive(Props)]
struct LikesProps<'a> {
    score: i32,
    #[props(!optional)]
    title: Option<&'a str>,
}

fn likes_component<'a>(cx: Scope<'a, LikesProps<'a>>) -> Element {
    cx.render(rsx! {
        div {
            "This post has ",
            b { "{cx.props.score}" },
            " likes"
        }
    })
}

likes_component { 
    score: 100,
    title: Some("hello"),
}
4.12.3、props handler
js
#[derive(Props)]
struct ClickComponentProps<'a> {
    on_click: EventHandler<'a, MouseEvent>,
}

fn click_component<'a>(cx: Scope<'a, ClickComponentProps<'a>>) -> Element {
    cx.render(rsx! {
        div {
            onclick: move |_| {
                info!("div clicked");
            },
            "This is a clickable component"
            button {
                onclick: move |event| {
                    info!("button clicked");
                    event.stop_propagation();
                    cx.props.on_click.call(event);
                },
                "Click me"
            }
        }
    })
}

click_component {
    on_click: move |_| {
        info!("======== div clicked");
    },
}
4.12.4、props handler 自定义数据
js
#[derive(Debug)]
struct CustomData(i32);

#[derive(Props)]
struct ClickComponentProps<'a> {
    on_click: EventHandler<'a, CustomData>,
}

fn click_component<'a>(cx: Scope<'a, ClickComponentProps<'a>>) -> Element {
    cx.render(rsx! {
        div {
            onclick: move |_| {
                info!("div clicked");
            },
            "This is a clickable component"
            button {
                onclick: move |event| {
                    info!("button clicked");
                    event.stop_propagation();
                    cx.props.on_click.call(CustomData(10));
                },
                "Click me"
            }
        }
    })
}

click_component {
    on_click: move |data| {
        info!("======== div clicked== {data:?}");
    },
}

4.13、数据结构借用

js
#[derive(Props, PartialEq)]
struct LikesProps<'a> {
    score: i32,
    title: Option<&'a str>,
}

fn likes_component<'a>(cx: Scope<'a, LikesProps<'a>>) -> Element {
    render! {
      div {
          "This post has ",
          b { "{cx.props.score}" },
          " likes"
      }
    }  
}
js
struct LikesProps<'a> {
    score: i32,
    title: Option<&'a str>,
}

likes_component { 
    props: LikesProps { score: 10, title: None }
}

#[inline_props]
fn likes_component<'a>(cx: Scope, props: LikesProps<'a>) -> Element {
    cx.render(rsx! {
        div {
            "This post has ",
            b { "{props.score}" },
            " likes"
        }
    })
}

4.14、children

js
#[derive(Debug, Props)]
struct ClickableProps<'a> {
    href: &'a str,
    children: Element<'a>,
}

fn clickable_component<'a>(cx: Scope<'a, ClickableProps<'a>>) -> Element {
    cx.render(rsx! {
        a {
            href: "{cx.props.href}",
            &cx.props.children
        }
    })
}

clickable_component { 
    href: "https://github.com/athifr/dioxus", 
    div {
      p {
        "Dioxus is a React-like Rust framework, but with a different API"
      }
      button {
        "Click me"
      }
    }
}

4.15、use_state

js
fn app(cx: Scope) -> Element {
    let count = use_state(cx, || 0);
    render! {
         h1 {
             "counter: {count}"
         }
         button {
             onclick: move |_| count.set(*count.current() + 1),
             "+"
         }
         button {
             onclick: move |_| count.set(*count.current() - 1),
             "-"
         }
    }
    // let count = use_state(cx, || 0);
    // render! {
    //      h1 {
    //          "counter: {count}"
    //      }
    //      button {
    //          onclick: move |_| count.set(**count + 1),
    //          "+"
    //      }
    //      button {
    //          onclick: move |_| count.set(**count - 1),
    //          "-"
    //      }
    // }
    
    // let mut count = use_state(cx, || 0);
    // render! {
    //     h1 {
    //         "counter: {count}"
    //     }
    //     button {
    //         onclick: move |_| count += 1,
    //         "+"
    //     }
    //     button {
    //         onclick: move |_| count -= 1,
    //         "-"
    //     }
    // }
}

Released under the MIT License.