こんにちは、エンジニアの渡辺(@mochi_neko_7)です。
先週の記事
では Rust の Web バックエンドのフレームワークを紹介してもらいました。
本記事ではそれに続く形で、Rust で使用できる Web フロントエンドのフレームワークで、React 風の API が使用できる Dioxus に入門してみたいと思います。
- Rust v1.75.0
- Dioxus v0.4.3
- Dioxus Hello, world
- Dioxus 入門
- Dioxus × WASM のハマりどころ
- Dioxus × Rust のハマりどころ
- Rust × Web フロントエンドの良いところ
- まとめ
- サンプル
- おわりに
Dioxus Hello, world
Dioxus は Rust 向けの GUI ライブラリで、主な特徴は下記になります。
- Web / Desktop / Mobile / Terminal のクロスプラットフォーム対応
- React 風の宣言的な記述で UI を構築できる
- 非同期処理をサポートしている
- Hot Reload の機能がある
現在は v0.x 系でベータバージョンな点にご注意ください。
Web 向けには3種類の実装が提供されています。
- dioxus-web : クライアント側で WASM(WebAssembly)でレンダリングする
- dioxus-liveview : サーバー側でレンダリングした結果を WebSocket でクライアント側に送信して表示する
- dioxus-fullstack : 最初はサーバー側でレンダリングして、それ以降はクライアント側で更新する
今回は一番シンプルそうな 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 で作成したプロジェクトに、dioxus
と dioxus-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
を参照していただくとして、ここではいくつかの取っ掛かりのポイントに絞って紹介します。
- Rendering
- Hooks
- Async
- Routing
- Design
1. Rendering
Hello world でも書いているように、Dioxus のレンダリングは render!
マクロ*1を使用して記述をします。
HTML タグ風の記述もできます。
use dioxus::prelude::*; fn app(cx: Scope) -> Element { render! { h1 { "Title" } div { "Hello, world!" } br {} } }
app
関数自身もそうですが、別で #[component]
マクロを使って Component に切り出してレンダリングの要素を定義をすることもできます。
2. Hooks
Dioxus には React の Hooks 風の状態管理の仕組みが実装されています。
- use_state
- use_ref
- use_future
- use_coroutine
- use_callback
- etc...
名前も基本的には似ていますし、Rules of hooks も同様に存在します。
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 は同期処理で書かれていますが、非同期処理を利用したい場合には下記を使用します。
- use_future : データの Fetch 系の処理など
- use_coroutine : 無限ループ系のタスク処理など
- cx.spawn() : 単発の非同期処理の実行など
それぞれ振る舞いが異なりますので場面に応じて使い分けます。
ちなみに tokio の rt-multi-thread
は WASM をサポートしていないため使用できず、Mutex<T>
などを使用したい場合は async-std などを使用することになります。
Rust で非同期処理中で共有するオブジェクトを実装する際にお馴染みの Arc<Mutex<T>>
パターンを使用するときにはご注意ください。
4. Routing
Routing の機能は dioxus-router
という dioxus
本体とは別の crate で提供されています。
リファレンスの
// 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 (例だと Home
と Blog
)を定義してレンダリングを切り替えます。
#[component] fn Home(cx: Scope) -> Element { render! { h1 { "Welcome to the Dioxus Blog!" } } }
main.rs
の app
関数の render! { }
マクロ内で Router::<Route> {}
のように呼び出して利用します。
Route を明示的に切り替える方法には二種類あります。
dioxus_router::components::Link
を使ってリンクを埋め込むuse dioxus_router::hooks::use_navigator
を使って取得したnavigator
でnavigator.push(Route::XXX {})
のように明示的に遷移する
上記の「sign up」「reset password」は Link で、「BACK TO HOME」 は Navigator で画面遷移を実装しています。
5. Design
公式では Tailwind をサポートしています。
OSS で Material UI を Dioxus 向けに実装しているものも利用できます。
Dioxus × WASM のハマりどころ
dioxus-web を触っていると WASM 由来と思しきハマりポイントが見えてきましたのでいくつか紹介します。
- 環境変数や I/O に制限がある
style.css
の読み込みがうまく動かない
1. 環境変数や I/O に制限がある
WASM はサンドボックス環境で動作するため、デフォルトでは環境変数や OS の I/O は使用できず、通常は WASI を通して限定的に許可したものにアクセスし利用します。
dioxus-web ではその辺りをどう扱うのかドキュメントに記載がなく、デフォルトでは環境変数もファイル I/O も使用できませんでした。
上記のドキュメントを見ながら asset_dir
を設定してもファイルはコピーされてもブラウザ側の Resources では確認できず。
環境変数は直接アプリに埋め込んでも問題ないなら .env
などから build.rs
でソースコードを自動生成して利用するという力業もありますが...
2. style.css
の読み込みがうまく動かない
自分のプロジェクトではリファレンスのプロジェクトと同様に style.css
を配置したり、Dioxus.toml
の web.resources.style
で設定してみても反映されませんでした。
こちらも力業ですが
render! { style { dangerous_inner_html: r#"body { ... } " } }
のようにソースコードに文字列として埋め込んだ CSS を直接指定するという方法ではうまく動きました。
Dioxus × Rust のハマりどころ
同様に Rust との兼ね合いでハマりやすいポイントも紹介します。
- 借用やライフタイムよるコンパイルエラーが出やすい
- 実行時 panic が発生する場合がある
- マクロが多いので馴れないとソースコードが読みづらい
1. 借用やライフタイムよるコンパイルエラーが出やすい
Context (cx: Scope
)や UseState
などの Hooks、非同期処理関連などで借用やライフタイム関係で意図せずコンパイルエラーを出してしまう、rust-analyzer を使いながらでも慣れないと修正に時間がかかるということが起こりがちでした。
この手のコンパイルエラーはエラーメッセージを見ても解決方法が分かりづらいですが、Hooks のインスタンスは適度に .clone()
を挟むと解決できる場合があります。
これは Rust の仕様上避けられない部分なので仕方ありませんが、Rust に慣れていない方だとハマる可能性は高いので注意が必要です。
2. 実行時 panic が発生する場合がある
Hooks の制約 の違反はコンパイルエラーにはならないため、実行時に panic を出すケースがあります。
また、UseSharedState
も write の操作を同時に行うと panic を出しますので、Arc<Mutex<T>>
パターンを使用するなどの工夫をした方が良いです。
Result
ベースの API にはなっていないこともありエラーハンドリング自体も少し複雑になりやすい印象です。
仕様の問題ではあるものの、Rust の「コンパイルが通れば基本的に動く」性質を弱めてしまいデバッグのコストが少し高いのが少しもったいないポイントです。
3. マクロが多いので馴れないとソースコードが読みづらい
render!
、#[component]
などマクロがコアな API になっていることもあり、ぱっと見で挙動が分かりづらかったり、エディターの補完が効きづらかったりして少し扱いが難しい印象を受けました。
ただしこれは React 風の宣言的 UI の実装のための副作用のようなものなので一定仕方がないかとは思います。
ひょっとしたらより Rust フレンドリーな実装方法も別であるのかもしれません。
Rust × Web フロントエンドの良いところ
これまでデメリットの側面ばかり挙げていますが、もちろん良い側面も多いです。
- cargo、rustfmt、clippy をはじめとした周辺ツールが充実していて使いやすい
- Rust の強い型制約や trait/enum/Option/Result/macro などの柔軟な表現を Web フロントエンドでも活かせる
- バイナリサイズやメモリ使用量を抑えやすい
私に本家 React の知見がないためちゃんとした比較はできませんが、やはり Rust の恩恵を受けつつ快適な環境で開発ができるメリットは大きいと感じます。
まとめ
- Dioxus の導入や使用するにあたってのコアな機能を簡単に紹介しました
- dioxus-web を使用する際にハマりやすいポイントをいくつか紹介しました
- Rust × Web フロントエンドの良い側面にも少し触れました
全体の所感としては実戦投入するには WASM 周辺の事情をちゃんと把握しないと難しそうかなという印象です。
Rust もしくは React に慣れている方がもう一方の技術の勉強がでら試しに触ってみる分には面白いのではないかと思います。
サンプル
今回紹介した内容を勉強する目的で作成した Dioxus 上で Firebase Auth を触る Repository を公開しています。
Rust / Dioxus の dioxus-web をベースに、Firebase Auth の API を利用したログインフローと Material UI によるデザインの実装をしています。
Web Frontend 初心者で拙い部分も多いことを前提に参考にしていただければと思います。
おわりに
個人的にはついでに React の勉強にもなったのは良かったですし、Rust で Web フロントエンドが書けるのは楽しかったのですが、 慣れが必要な点も多く初心者のハードルは少し高めかなと思いました。
Dioxus は公式のドキュメントも充実しているので情報は探しやすいですが、逆にドキュメントに書かれていない落とし穴もありましたので補足しました。
Rust × Web フロントエンドはまだまだ未成熟な分野ではありますが、Rust ユーザーが増えてエコシステムが発達していくとより快適になっていくことを期待したいです。
*1:render! {...} と csx.render( rsx!(...) ) はほぼ同じのようです。