Rust/DioxusでWebフロントエンド入門してみる

こんにちは、エンジニアの渡辺(@mochi_neko_7)です。

先週の記事

synamon.hatenablog.com

では Rust の Web バックエンドのフレームワークを紹介してもらいました。

本記事ではそれに続く形で、Rust で使用できる Web フロントエンドのフレームワークで、React 風の API が使用できる Dioxus に入門してみたいと思います。

  • Rust v1.75.0
  • Dioxus v0.4.3

Dioxus Hello, world

Dioxus は Rust 向けの GUI ライブラリで、主な特徴は下記になります。

  • Web / Desktop / Mobile / Terminal のクロスプラットフォーム対応
  • React 風の宣言的な記述で UI を構築できる
  • 非同期処理をサポートしている
  • Hot Reload の機能がある

dioxuslabs.com

現在は v0.x 系でベータバージョンな点にご注意ください。

Web 向けには3種類の実装が提供されています。

  1. dioxus-web : クライアント側で WASM(WebAssembly)でレンダリングする
  2. dioxus-liveview : サーバー側でレンダリングした結果を WebSocket でクライアント側に送信して表示する
  3. dioxus-fullstack : 最初はサーバー側でレンダリングして、それ以降はクライアント側で更新する

dioxuslabs.com

今回は一番シンプルそうな dioxus-web を選んで触ってみます。

公式のインストール手順に従ってセットアップをします。

1. ツールのセットアップ

開発時のローカルサーバーを立てたりするために使用する CLI ツールをインストールします。

$ cargo install dioxus-cli

dioxus-web では WASM でビルドをするため、rustup に WASM のターゲットを追加します。

$ rustup target add wasm32-unknown-unknown

もしくは rust-toolchain.toml を作成して、toolchain.target を下記のように指定する形でも構いません。

[toolchain]
targets = ["wasm32-unknown-unknown"]

2. プロジェクトのセットアップ

cargo で作成したプロジェクトに、dioxusdioxus-web の2つの crate の依存関係を追加します。

$ cargo add dioxus
$ cargo add dioxus-web

3. Hello, world

src/main.rs にアプリケーションを起動するコードを書きます。

use dioxus::prelude::*;

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

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

dioxus-cli の下記のコマンドでビルドと起動を行い、http://127.0.0.1:8080 をブラウザで開いて「Hello, world!」が表示されたら成功です。

$ dx serve

ちなみに Hot Reload を使いたい場合は代わりに下記コマンドで起動します。

$ dx serve --hot-reload

Dioxus 入門

基本的な解説は公式の Guide

dioxuslabs.com

を参照していただくとして、ここではいくつかの取っ掛かりのポイントに絞って紹介します。

  1. Rendering
  2. Hooks
  3. Async
  4. Routing
  5. Design

1. Rendering

Hello world でも書いているように、Dioxus のレンダリングは render! マクロ*1を使用して記述をします。

dioxuslabs.com

HTML タグ風の記述もできます。

use dioxus::prelude::*;

fn app(cx: Scope) -> Element {
    render! {
        h1 { "Title" }

        div {
            "Hello, world!"
        }

         br {}    
    }
}

app 関数自身もそうですが、別で #[component] マクロを使って Component に切り出してレンダリングの要素を定義をすることもできます。

dioxuslabs.com

2. Hooks

Dioxus には React の Hooks 風の状態管理の仕組みが実装されています。

  • use_state
  • use_ref
  • use_future
  • use_coroutine
  • use_callback
  • etc...

dioxuslabs.com

docs.rs

名前も基本的には似ていますし、Rules of hooks も同様に存在します。

dioxuslabs.com

use_state を使ったシンプルな例が下記です。

use dioxus::prelude::*;

fn app(cx: Scope) -> Element {
    let mut count = use_state(cx, || 0);

    render! {
        h1 { "Counter: {count}" }

        button {
            onclick: move |_| {
                count += 1
            },
            "+"
        }

        button {
            onclick: move |_| {
                count -= 1
            },
            "-"
        }
    }
}

3. Async

Dioxus の Components の API は同期処理で書かれていますが、非同期処理を利用したい場合には下記を使用します。

  1. use_future : データの Fetch 系の処理など
  2. use_coroutine : 無限ループ系のタスク処理など
  3. cx.spawn() : 単発の非同期処理の実行など

それぞれ振る舞いが異なりますので場面に応じて使い分けます。

ちなみに tokiort-multi-thread は WASM をサポートしていないため使用できず、Mutex<T> などを使用したい場合は async-std などを使用することになります。

Rust で非同期処理中で共有するオブジェクトを実装する際にお馴染みの Arc<Mutex<T>> パターンを使用するときにはご注意ください。

doc.rust-jp.rs

4. Routing

Routing の機能は dioxus-router という dioxus 本体とは別の crate で提供されています。

dioxuslabs.com

dioxuslabs.com

リファレンスの

// All of our routes will be a variant of this Route enum
enum Route {
    // if the current location is "/home", render the Home component
    #[route("/home")]
    Home {},
    // if the current location is "/blog", render the Blog component
    #[route("/blog")]
    Blog {},
}

のように、enum で Route を定義して、それぞれの Component (例だと HomeBlog)を定義してレンダリングを切り替えます。

#[component]
fn Home(cx: Scope) -> Element {
    render! {
        h1 { "Welcome to the Dioxus Blog!" }
    }
}

main.rsapp 関数の render! { } マクロ内で Router::<Route> {} のように呼び出して利用します。

Route を明示的に切り替える方法には二種類あります。

  1. dioxus_router::components::Link を使ってリンクを埋め込む
  2. use dioxus_router::hooks::use_navigator を使って取得した navigatornavigator.push(Route::XXX {}) のように明示的に遷移する

上記の「sign up」「reset password」は Link で、「BACK TO HOME」 は Navigator で画面遷移を実装しています。

5. Design

公式では Tailwind をサポートしています。

dioxuslabs.com

OSS で Material UI を Dioxus 向けに実装しているものも利用できます。

github.com

Dioxus × WASM のハマりどころ

dioxus-web を触っていると WASM 由来と思しきハマりポイントが見えてきましたのでいくつか紹介します。

  1. 環境変数や I/O に制限がある
  2. style.css の読み込みがうまく動かない

1. 環境変数や I/O に制限がある

WASM はサンドボックス環境で動作するため、デフォルトでは環境変数や OS の I/O は使用できず、通常は WASI を通して限定的に許可したものにアクセスし利用します。

dioxus-web ではその辺りをどう扱うのかドキュメントに記載がなく、デフォルトでは環境変数もファイル I/O も使用できませんでした。

dioxuslabs.com

上記のドキュメントを見ながら asset_dir を設定してもファイルはコピーされてもブラウザ側の Resources では確認できず。

環境変数は直接アプリに埋め込んでも問題ないなら .env などから build.rs でソースコードを自動生成して利用するという力業もありますが...

2. style.css の読み込みがうまく動かない

自分のプロジェクトではリファレンスのプロジェクトと同様に style.css を配置したり、Dioxus.tomlweb.resources.style で設定してみても反映されませんでした。

こちらも力業ですが

render! {
    style {
        dangerous_inner_html: r#"body { ... } "
    }
}

のようにソースコードに文字列として埋め込んだ CSS を直接指定するという方法ではうまく動きました。

Dioxus × Rust のハマりどころ

同様に Rust との兼ね合いでハマりやすいポイントも紹介します。

  1. 借用やライフタイムよるコンパイルエラーが出やすい
  2. 実行時 panic が発生する場合がある
  3. マクロが多いので馴れないとソースコードが読みづらい

1. 借用やライフタイムよるコンパイルエラーが出やすい

Context (cx: Scope)や UseState などの Hooks、非同期処理関連などで借用やライフタイム関係で意図せずコンパイルエラーを出してしまう、rust-analyzer を使いながらでも慣れないと修正に時間がかかるということが起こりがちでした。

この手のコンパイルエラーはエラーメッセージを見ても解決方法が分かりづらいですが、Hooks のインスタンスは適度に .clone() を挟むと解決できる場合があります。

これは Rust の仕様上避けられない部分なので仕方ありませんが、Rust に慣れていない方だとハマる可能性は高いので注意が必要です。

2. 実行時 panic が発生する場合がある

Hooks の制約 の違反はコンパイルエラーにはならないため、実行時に panic を出すケースがあります。

また、UseSharedState も write の操作を同時に行うと panic を出しますので、Arc<Mutex<T>> パターンを使用するなどの工夫をした方が良いです。

doc.rust-jp.rs

Result ベースの API にはなっていないこともありエラーハンドリング自体も少し複雑になりやすい印象です。

dioxuslabs.com

仕様の問題ではあるものの、Rust の「コンパイルが通れば基本的に動く」性質を弱めてしまいデバッグのコストが少し高いのが少しもったいないポイントです。

3. マクロが多いので馴れないとソースコードが読みづらい

render!#[component] などマクロがコアな API になっていることもあり、ぱっと見で挙動が分かりづらかったり、エディターの補完が効きづらかったりして少し扱いが難しい印象を受けました。

ただしこれは React 風の宣言的 UI の実装のための副作用のようなものなので一定仕方がないかとは思います。

ひょっとしたらより Rust フレンドリーな実装方法も別であるのかもしれません。

Rust × Web フロントエンドの良いところ

これまでデメリットの側面ばかり挙げていますが、もちろん良い側面も多いです。

  • cargo、rustfmt、clippy をはじめとした周辺ツールが充実していて使いやすい
  • Rust の強い型制約や trait/enum/Option/Result/macro などの柔軟な表現を Web フロントエンドでも活かせる
  • バイナリサイズやメモリ使用量を抑えやすい

私に本家 React の知見がないためちゃんとした比較はできませんが、やはり Rust の恩恵を受けつつ快適な環境で開発ができるメリットは大きいと感じます。

まとめ

  1. Dioxus の導入や使用するにあたってのコアな機能を簡単に紹介しました
  2. dioxus-web を使用する際にハマりやすいポイントをいくつか紹介しました
  3. Rust × Web フロントエンドの良い側面にも少し触れました

全体の所感としては実戦投入するには WASM 周辺の事情をちゃんと把握しないと難しそうかなという印象です。

Rust もしくは React に慣れている方がもう一方の技術の勉強がでら試しに触ってみる分には面白いのではないかと思います。

サンプル

今回紹介した内容を勉強する目的で作成した Dioxus 上で Firebase Auth を触る Repository を公開しています。

github.com

Rust / Dioxus の dioxus-web をベースに、Firebase Auth の API を利用したログインフローと Material UI によるデザインの実装をしています。

Web Frontend 初心者で拙い部分も多いことを前提に参考にしていただければと思います。

おわりに

個人的にはついでに React の勉強にもなったのは良かったですし、Rust で Web フロントエンドが書けるのは楽しかったのですが、 慣れが必要な点も多く初心者のハードルは少し高めかなと思いました。

Dioxus は公式のドキュメントも充実しているので情報は探しやすいですが、逆にドキュメントに書かれていない落とし穴もありましたので補足しました。

Rust × Web フロントエンドはまだまだ未成熟な分野ではありますが、Rust ユーザーが増えてエコシステムが発達していくとより快適になっていくことを期待したいです。

*1:render! {...} と csx.render( rsx!(...) ) はほぼ同じのようです。