😺

Dioxus (Rustでfrontend) 入門1

2022/10/14に公開

Dioxus入門1

FrontendをRustで書く意味?

  • 趣味...
  • WASM Interface Typesが標準化されれば、JSの呼び出し等が高速になる
  • RuntimeエラーがReact/Angularに比べて殆どない
    • ただし .unwrap()とかexpect()とか不用意に使うとすぐpanicする
  • Rust Backend APIのStructを使い回せる
  • DioxusならSSR対応(ただしbotがアクセスする用として割り切るべき)

YewではなくDioxusな理由

  • SSR対応
  • Dioxusのほうが Runtimeエラーがでづらい
  • use_futureが便利
  • Yewよりいろいろなものがシンプル
  • formに対する inputイベントが取れる
    • Yewも取れるかもしれませんが ちょっと触った限りではできませんでした。。。

Setup

  • trunkをインストール
  • rustc webassembly対応に

Cargo.toml

[package]
name = "dixious-test"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
dioxus = { version = "0.2.4", features = ["web"] }
dioxus-router = "0.2.3"
web-sys = {"version" = "0.3.44", features=['console']}
gloo = "0.8"
gloo-net = "0.2"
serde = {version = "1", features = ["derive"]}
serde_json = "1"
anyhow="1"  

[profile.release]
lto = true
opt-level = 's'
[profile.release]
lto = true
opt-level = 's'

この設定を入れることで release build時のwasmのサイズを小さくできる。

開発時の実行は trunk serve でweb serverも起動します。
Release Buildは trunk build --release

サンプルコード

Routerをつかってみる

pub fn Route(cx: Scope) -> Element {
    cx.render(rsx! {
        div {
            Router {
                dioxus_router::Route {
                    to: "/login", Login{}
                },
                dioxus_router::Route {
                    to: "/", root()
                }
            }
        }
    })
}

このようなコードを書けば pathとコンポーネントを紐付けることができる。

Dioxusでは Componentを呼ぶ際、大文字の自作コンポーネントはRSXの中で呼び出すとき、 App{"aaa"}  と呼ぶことができる。 {}で囲って呼び出せる。
小文字の場合は app()で呼出すため、使い勝手が異なる。 一般的には頭が大文字の関数を作成することになる。 Warningがでると面倒なので下記をコードに入れることで、コンパイル時の警告をなくすことができる。

#[allow(non_snake_case)]

また、 onclickなど コードで画面遷移したい場合

    use_router(&cx).replace_route("/login", None, None);

などとすれば /loginに遷移することができる。 location.href等で飛ばすとjsやwasmが再度ロードされてしまうので注意。

Navigation サイドバーなどでリンクで飛ばしたい場合は下記のようなRSXを書く。

fn app(cx: Scope) -> Element {
    cx.render(rsx! {
        Router {
            Route { to: "/", "Home" }
            Route { to: "/games", "Games" }
            Route { to: "/play", "Play" }
            Route { to: "/settings", "Settings" }

            p {
                "----"
            }
            nav {
                ul {
                    Link { to: "/", li { "Home" } }
                    Link { to: "/games", li { "Games" } }
                    Link { to: "/play", li { "Play" } }
                    Link { to: "/settings", li { "Settings" } }
                }
            }
        }
    })
    

Local State

Dioxusでは Reactと同様にuse_stateが利用できる。

https://github.com/DioxusLabs/dioxus/blob/master/docs/guide/examples/hooks_counter.rs

このサンプルはDesktop Appを作成するためだが、
mainの中身を下記に入れ替えれば webで利用できる。

dioxus::web::launch(App);

use_future

async fn get_customer_names() -> Result<Vec<String>, anyhow::Error> {
    let ls = gloo::storage::LocalStorage::raw();
    let token = ls.get_item("token");
    if token.is_err() {
        return Err(anyhow::anyhow!("token not found"));
    }
    let token = token.unwrap();
    if token.is_none() {
        return Err(anyhow::anyhow!("token not found"));
    }
    let token = token.unwrap();

    const URL: &str = "https://graphqlsvr/v1/api/test/graphql";
    let token = format!("Bearer {}", token.replace("\"", ""));
    let json = Request::post(URL)
    .header("authorization", token.as_str())
        .body(r#"{"operationName":null,"variables":{},"query":"{\n  customers @mysql {\n    name\n  }\n}\n"}"#.to_string())
        .send()
        .await?
        .text()
        .await?;

    gloo::console::log!("", &json);
    let data: Value = serde_json::from_str(json.as_str())?;
    let datas = data
        .get("data")
        .ok_or(anyhow::anyhow!("data not found"))?
        .get("customers")
        .ok_or(anyhow::anyhow!("customers not found"))?;
    gloo::console::log!("", &datas.to_string());
    let names: Vec<String> = datas
        .as_array()
        .unwrap()
        .iter()
        .flat_map(|v| v.get("name"))
        .map(|v| v.as_str().unwrap_or("").to_string())
        .collect();

    Ok(names)
}

というgraphqlで顧客名を取得する非同期のコードがあったとき、

pub fn CustomerSelector(cx: Scope) -> Element {
    let s = use_state(&cx, || vec!["".to_string()]);
    let selected_name = use_state(&cx, || "test".to_string());
    {
        let s = s.clone();
        use_future(&cx, (), |()| async move {
            let datas = get_customer_names().await.unwrap_or(vec!["".to_string()]);
            gloo::console::log!(format!("{:?}", &datas));
            s.set(datas);
        });
    }
    let names = s.get();
    let selected_name_string = selected_name.get().to_string();
    cx.render(rsx! {
        select {
            class: "pf-c-form-control",
            onchange: move |e| {
                let selected_name = selected_name.clone();

                gloo::console::log!(e.value.as_str());
                selected_name.set(e.value.clone());
            },

        names.iter().map(|name| rsx!{
            option {
               "{name}"
            }
        })
    }
    CustomerInfo {
        name: selected_name_string,
    }
    })
}

この様に呼び出すことができる。
use_futureはReactのuse_effectのFutureを扱える版である。
Yewとは異なり、DioxusだけでFutureを解決することができる。

Discussion