RustとDioxusでSPAを新バージョンに対応させよう:Webアプリ実装で学ぶ、現場で役立つRust入門(8)
第8回では、2024年3月末のDioxus 0.5リリースに合わせて、機能変更のポイントをWebプラットフォームに絞って紹介し、第7回で作成したアプリをバージョン0.5対応となるように修正します。
本連載のサンプルコードをGitHubで公開しています。こちらからダウンロードしてみてください。
Dioxus バージョン0.5の概要
第7回の執筆後に、Dioxusはバージョン0.5がリリースされました(第7回のサンプルは、1つ前のバージョン0.4に準拠しています)。バージョン0.5はわずかに0.1の違いですが、バージョン0.4とかなりの部分で互換性がなく、基本的には細かく修正しないとアプリをビルド、実行できません。
そこで今回は、サンプルの機能拡張に先立ち、バージョン0.5における変更点のポイントをWebプラットフォーム関連に絞って紹介した後、第7回のサンプルをバージョン0.5に対応させます。
バージョン0.5における多くの機能変更は、コンポーネントのコードをよりシンプルにするとともに、既存の問題を解決するためのものとなっています。公式ブログに記載された主な変更ポイントを以下に列挙します。
- dioxus-coreを完全に記述し直して、安全でないコードを全て削除
- use_state、use_ref関数を廃止してCloneフリーのSignalを使用
- 全てのライフタイムを削除し、コンテキスト引数(cx: Scope)も削除
- アプリを起動するあらゆるプラットフォームで単一のlaunch関数を提供
- TailwindおよびVanilla CSSをサポートするアセットのホットリロード
- WebAssemblyネイティブのWebSysイベントへのアクセスが可能に
- 要素のプロパティを使用したコンポーネントの拡張
ここからは、具体的なアップデート内容を解説していきます。
ロガーがtracingベースに変更された
0.4ではロガーdioxus_loggerのベースはlogクレートでしたが、0.5でtracingクレートに変更されました。tracingクレートは、構造化されたイベントベースの診断情報を収集するためのフレームワークで、特に並列実行環境で有効です。logクレートのまま使い続けると、実行時にエラーとなります。リスト1は、Infoレベルのログ出力を有効にする初期化コードの例です。
use log::info; dioxus_logger::init(log::LevelFilter::Info).expect("failed to init logger"); ↓ use tracing::Level; dioxus_logger::init(Level::INFO).expect("failed to init logger");
後述するdx newコマンドでのアプリ生成では、このロガーを使用するようにクレートの依存関係や初期化コードがあらかじめ用意されます。
ルートコンポーネントのマウント関数がシンプルになった
ルートコンポーネント(App)をマウントする関数の呼び出し方法がシンプルになりました(リスト2)。launchメソッドがdioxus::preludeに含まれたためです。これにより、あらゆるプラットフォームでコードを共通化できるようになりました。
dioxus_web::launch(App); ↓ launch(App);
コンテキスト引数(cx: Scope)が不要になった
コンポーネントにはコンテキスト引数(cx: Scope)がこれまで必要でしたが、不要になりました(リスト3)。コンポーネント内ではコンテキスト変数が使えないため、フックやマクロも変更されています。
fn App(cx: Scope) -> Element { ↓ fn App() -> Element {
主要なフックが変更された
第7回の「補足」では、フックを簡単に紹介しました。紹介した関数のうちuse_state、use_refは廃止されuse_signalに統合されました。use_futureは戻り値を返さない仕様に変更され、代わりにuse_resourceが推奨されるようになりました。
- use_signal:値(スカラー、参照)の保持
- use_resource:非同期関数の呼び出しを使用した値の取得
コンポーネントのコンテキスト引数が廃止されたので、フックの関数もそれを受け取らないように変更されました。例えば、use_future関数の呼び出しはリスト4のように変化します。
let posts_source = use_future(cx, (), |_| data::call_index()); ↓ let posts_source = use_resource(|| data::call_index());
use_signalの使用例は、次回で紹介する予定です。
renderメソッドとrender!マクロが廃止されてrsx!マクロに一本化された
コンテキスト引数(cx: Scope)が廃止されたので、そこから呼び出すrenderメソッドも廃止されました(そのマクロ版であるrender!マクロも同様です)。結果、コンポーネントのレンダリングはrsx!マクロに一本化されました(リスト5)。レンダリング結果を返すブロックは、rsx!マクロとその入れ子で記述することになります。従来は使い分けが曖昧でしたが、より簡潔になりました。
cx.render(rsx! { "Hello, world!!" }) ↓ rsx! { "Hello, world!!" }
なお、一本化とは直接関係ありませんが、rsx!マクロに式(expression)を含める場合には、それを中カッコ({〜})で囲むように変更されました(バージョン0.4では直接記述できていた)。コンパイルエラー「Expressions must be wrapped in curly braces」が発生したら、その式を中カッコで囲みましょう。
Props構造体とコンポーネント関数にライフタイム注釈が不要になった
Propsとは、親コンポーネントが子コンポーネントに渡す引数のようなものです(詳しくは次回紹介します)。Propsは構造体で定義しますが、フィールドに参照を含む場合、そのフィールドと構造体そのものにライフタイム注釈が必要でした。このライフタイム指定は、さまざまな問題を招くものでした。
例えば、コンポーネント内でイベントハンドラを記述する場合、その処理内容はクロージャとして定義します。このクロージャに求められるライフタイムと、Props構造体(と、それを受け取るコンポーネント関数)のライフタイムに不整合が起きるので、コンパイル時にエラーとなります。バージョン0.4では、これを解決する具体的な手段がありませんでした。
バージョン0.5では、値を保持するフックが参照ではなくクローンを使うことでライフタイム注釈を不要にして、上記のようなライフタイムに関する問題をクリアしています(リスト6)。
#[derive(Props, PartialEq)] struct PostEntryProps<'a> { set_posts: &'a UseRef<Posts>, id: i32, } ↓ #[derive(Props, Clone, PartialEq)] struct PostEntryProps { set_posts: Signal<Posts>, id: i32, }
クローンを使うので、#[derive]属性にCloneの指定が必要になっています。
ライフタイム注釈('a)が不要になり見た目がシンプルになる他、フィールドがuse_ref関数の戻り値(UseRef<T>)の参照からuse_signal関数の戻り値(Signal<T>)そのものに変化します。参照でなくなるので、ライフタイム注釈が不要になるというわけです。同様に、コンポーネント関数でもライフタイム注釈が不要になり、Props構造体を直接受け取る形に変わります(リスト7)。
fn PostEntry<'a>(cx: Scope<'a, PostEntryProps<'a>>) -> Element { ↓ fn PostEntry(props: PostEntryProps) -> Element {
こちらも、シグネチャがシンプルかつ直感的になります。これらの詳細は、次回に投稿表示コンポーネントを紹介する際に改めて触れます。
サンプルのバージョン0.5対応
ここからは、上記の変更を踏まえて第7回で紹介したサンプルをバージョン0.5に対応させていきます。
CLIツールを最新版にする
本連載の手順でインストールされているDioxusのCLIツールはバージョン0.4ですので、これをアップデートしておきます。これにより、dx newなどの新しいコマンドが使えるようになります。アップデートは、インストールコマンドで実行できます。
% cargo install dioxus-cli …略… Replaced package `dioxus-cli v0.4.3` with `dioxus-cli v0.5.4` (executable `dx`) % dx --version dioxus 0.5.4
アプリを新規作成する
バージョン0.4のドキュメントでは、アプリはcargo newコマンドで作成することになっていました。バージョン0.5では、dx newコマンドを利用して対話式で作成できます。アプリの基本構成をバージョン0.5に合わせるため、アプリを作成し直します。アプリを作成したいフォルダで、dx newコマンドを実行します。「?🤷」で始まる問いにはカーソルキーで「>」を移動させて選択します。「🤷」には項目を入力します。ここでは、順番に「Web」「dioxus-posts」「Vanilla」「false」を選択、入力します。以下のように、Webプラットフォーム、プロジェクト名dioxus-posts、Vanilla CSS(フレームワークに依存しない素のCSS)、Routerなしとなります。
% dx new ✔ 🤷 Which sub-template should be expanded? · Web 🤷 Project Name: dioxus-posts ✔ 🤷 How do you want to create CSS? · Vanilla ✔ 🤷 Should the application use the Dioxus router? · false
アプリを実行してみる
ここでアプリをビルド、実行してみます。第7回で紹介したdx serveコマンド(ビルドだけならdx buildコマンド)を使えます。
% cd dioxus-posts % dx serve Dioxus @ v0.5.4 [13:59:58] > Local address: http://localhost:8080/ > Network address: http://192.168.108.49:8080/ > HTTPS status: Disabled > Hot Reload Mode: RSX > Watching: [ src, assets, Cargo.toml, Dioxus.toml ] > Custom index.html: None > Serve index.html on 404: True > Build Features: [ ] > Build Profile: Debug > Build took: 32837 millis
ビルドが正常終了すると、自動的にブラウザが起動し、アプリが表示されます(ファイアウォールでブロックされた場合には許可してください)。図1のように、Dioxusの公式サイトにリンクするポータルアプリが表示されれば成功です。ウォッチが有効になっているので、不要なビルドを避けるためにアプリを終了させておきましょう。
フォルダとファイルの役割を確認する
ここで、dx newコマンドおよびdx serve(dx build)コマンドで生成されるフォルダとファイルの内容を確認しておきましょう。srcフォルダ、targetフォルダはCargoパッケージマネジャーと共通です。
- distフォルダ
配布されるコンテンツが置かれます(distribution)。置かれるファイルは、HTML、CSS、アイコン、後述するassetsフォルダの内容です。Dioxusのサーバは、ここのコンテンツをクライアントに提供します。なお、フォルダ名は後述するDioxus.tomlファイルで変更できます。
- assetsフォルダ
静的ファイル(アセット)が置かれます。このフォルダの内容が、ビルドによってdistフォルダのassetsフォルダにコピーされます。なお、フォルダ名は後述するDioxus.tomlファイルで変更できます。
- Dioxus.tomlファイル
Dioxusアプリのビルド時の動作を指定するファイルです。拡張子から分かるように、Cargo.tomlと同様のTOML(Tom's Obvious, Minimal Language)形式のファイルです。5つのセクション[application][web.app][web.watcher][web.resource][web.resource.dev]で、アプリ名やフォルダ名、ウォッチ対象や読み込むべきCSSファイル、JavaScriptファイルを指定できます。
アプリを修正する
ここから、第7回と同様の動作になるようにアプリを修正していきますが、その前に、dx newコマンドで生成されたsrc/main.rsファイルを見てみます(リスト8)。前節で紹介したバージョン0.5における変更点が反映された内容です。
#![allow(non_snake_case)] (1) use dioxus::prelude::*; use tracing::Level; fn main() { dioxus_logger::init(Level::INFO).expect("failed to init logger"); launch(App); } #[component] (2) fn App() -> Element { rsx! { …略… } }
(1)と(2)について念のため補足しておきます。
(1)は第7回では触れなかった属性で、識別子の非スネークケースでの記述を認める(警告を抑制する)指定です。Rustでは、変数や関数の名前はスネークケースが基本です。しかしながら、HTMLではタグ名はキャメルケースを使うことになっているので、タグ名に相当するコンポーネント名(関数名)にキャメルケースを使うのはルール違反で警告の対象になります。この警告を抑制するのが(1)です。
(2)は、これも第7回は触れなかった属性で、続く関数定義をコンポーネント関数と見なすための指定になります。(1)の指定がなくてもキャメルケースでの記述で警告が抑制される他、Propsの定義方法が簡略化される(構造体でなく関数のシグネチャに直接定義できる)など、便利な機能が提供されます。もちろん、関数がコンポーネントであることを明示できる意味合いもあります。以降は、コンポーネントにこの属性を積極的に指定していきます。
HTMLテンプレートとdataクレートを追加する
HTMLテンプレートであるindex.htmlファイルと、APIを呼び出す関数を集めたdataクレートをプロジェクトに追加します。index.htmlファイルとdataクレートは、第7回のサンプルから、それぞれプロジェクトルートフォルダ、srcフォルダにコピーしてください。dataクレートはserdeクレートに依存しているので、第7回同様に以降で必要となるクレートと併せて追加してください。serdeクレート、reqwestクレートについては、Cargo.tomlファイルをリスト9のように編集して特定のfeaturesを有効にしておきます。
% cargo add serde serde-json reqwest im_rc
…略… serde = { "1.0.199", features = ["derive"] } serde_json = "1.0.116" reqwest = { version = "0.12.4", features = ["json"] } im-rc = "15.1.0"
Appコンポーネントを修正する
最後に、Appコンポーネントを修正します。dataクレートの利用のためのmod文、use文を追記し、rsx!マクロ内の既定の内容は全て削除するかコメントアウトして、リスト10の内容に置き換えます。
mod data; use data::{ResponseContent}; …略… #[component] fn App() -> Element { (1) let posts_source = use_resource(|| data::call_index()); (2) rsx! { (3) match &*posts_source.read_unchecked() { (4) Some(Ok(res)) => { match &res.result { ResponseContent::Items(items) => { rsx! { for item in items { {rsx! { div { "{serde_json::to_string(&item).unwrap()}" } }} (5) } } }, ResponseContent::Item(item) => rsx! { div { "{serde_json::to_string(&item).unwrap()}" } }, ResponseContent::Reason(reason) => rsx! { div { "{reason}" } }, ResponseContent::None => rsx! { div {} }, } }, Some(Err(err)) => rsx! { div { "初期データの読み込みに失敗しました:{err}" } }, None => rsx! { div { "データを読み込んでいます..." } } } } }
前節の通りの修正となっていますが、一つずつ確認します。
(1)は、Appコンポーネントの関数ですが、コンテキスト引数(cx: Scope)がありません。
(2)は、フックを生成するために、use_future関数ではなくuse_resource関数を呼び出しています。
(3)は、renderメソッドの呼び出しでなくrsx!マクロの呼び出しに置き換えています。
(4)は、use_resource関数の戻り値から値を取り出していますが、read_uncheckedメソッドによりライフタイムのチェックを省略した呼び出しとなっています。
最後の(5)は、rsx!マクロ全体を中カッコ({〜})で囲っています。一見すると不要な記述ですが、前述の通りrsx!マクロ内に式を置く場合には、中カッコで囲むことになったためです。
アプリを改めて実行してみる
ここまでの修正が済んだら、アプリをビルド、実行してみます。図2のように、第7回と同様の画面(投稿データがJSONそのままで表示されたもの)となれば成功です。
まとめ
今回は、Dioxusのバージョン0.5リリースに合わせて、機能変更のポイントをWebプラットフォームに絞って紹介し、第7回で作成したアプリをバージョン0.5対応となるように修正しました。
次回は、APIから取得した投稿データをコンポーネントで一覧表示する例を紹介します。
筆者紹介
WINGSプロジェクト
有限会社 WINGSプロジェクトが運営する、テクニカル執筆コミュニティー(代表山田祥寛)。主にWeb開発分野の書籍/記事執筆、翻訳、講演等を幅広く手掛ける。2021年10月時点での登録メンバーは55人で、現在も執筆メンバーを募集中。興味のある方は、どしどし応募頂きたい。著書、記事多数。
・サーバーサイド技術の学び舎 - WINGS(https://wings.msn.to/)
・RSS(https://wings.msn.to/contents/rss.php)
・X: @WingsPro_info(https://x.com/WingsPro_info)
・Facebook(https://www.facebook.com/WINGSProject)
Copyright © ITmedia, Inc. All Rights Reserved.