RustとDioxusで投稿アプリのSPAを作ってみようWebアプリ実装で学ぶ、現場で役立つRust入門(7)

第7回からは、第6回で作成した投稿アプリのREST APIを利用して、UI作成フレームワークDioxusでアプリのフロントエンド部分をSPAとして開発していきます。第7回では、API呼び出し関数の実装と、その呼び出し結果をページに反映するコードを通じて、Dioxusの基本動作を理解します。

» 2024年04月19日 05時00分 公開

この記事は会員限定です。会員登録(無料)すると全てご覧いただけます。

「Webアプリ実装で学ぶ、現場で役立つRust入門」のインデックス

連載:Webアプリ実装で学ぶ、現場で役立つRust入門

 本連載のサンプルコードをGitHubで公開しています。こちらからダウンロードしてみてください。


Dioxusの概要

 Dioxus(ディオクサス)は、Dioxus Labsによるクロスプラットフォーム対応のアプリを構築できるRustライブラリです。移植性が高く(portable)、高性能で(performant)、人間工学に基づいた(ergonomic)設計が特徴とされています。Dioxusでは、単一のコードで以下のプラットフォームで動作するアプリを開発可能です。

(1)Web

 WebAssembly技術を用いたSPA(Single Page Application)を開発できます。WebAssemblyについては@ITの連載「いろんな言語で試す、WebAssembly入門」で紹介しています。その第5回「RustでWebAssembly――「Rust and WebAssembly」を体験する」でRust and WebAssemblyを紹介しています。本記事の理解にWebAssemblyの知識は必須ではありませんが、興味があれば一読することをお勧めします。

(2)デスクトップ(macOS、Windows、Linux)

 ネイティブ環境でスタンドアロンで動作するデスクトップアプリを開発できます。このアプリは、システムのWebView(アプリ内でHTMLを表示する機能)でページをレンダリングし、プラットフォーム間で見た目が同じになります。WebViewを使用しますが、ブラウザAPIにはアクセスできません。その替わり、ファイルシステムなどのシステムのAPIを利用できます。

(3)モバイル(iOS、Android OS)

 他のプラットフォームに比べるとサポートが遅れていますが、WebViewを使ったモバイルアプリを開発できます。アニメーションやネイティブウイジェットを使えませんが、シンプルなCRUDスタイルのアプリの迅速な開発に向いています。

(4)TUI(Text User Interface)アプリ

 TUI形式のアプリを開発できます。TUIとは、CUIにGUIのような操作性を持ち込んだインタフェースを指します。CUIでは、例えばターミナル画面でコマンドを入力して操作しますが、同じくターミナル画面にGUIを模してテキストで配置したボタンや入力フィールドを、キーボードで操作できるようにしたものがTUIです。ターミナル画面でありながら、GUIのような直感的な操作が可能です。

(5)フルスタックWebアプリ

 サーバサイドレンダリングとハイドレーションに対応したフルスタックのWebアプリを開発できます。サーバサイドレンダリング(SSR)とは、ReactやVueなどで本来はクライアントサイドで実施していたレンダリング処理を、サーバサイドで処理することを指します。サーバサイドで実施することでクライアントサイドの負担は大幅に減りますが、インタラクティブ性が失われるという問題があります。ハイドレーションとは、SSRでレンダリング済みのコンテンツにイベントハンドラをアタッチし直すことで、インタラクティブ性を回復させる仕組みです。

(6)LiveViewアプリ

 LiveViewアプリは、WebSocket(Webにおけるリアルタイム通信のための標準)を使用したサーバとクライアント間のリアルタイム通信をサポートするアプリです。アプリは基本的にサーバサイドで動作し、クライアントはページのレンダリングを受け持ちます。その性質上、チャットやゲームなどのリアルタイム性を要求されるアプリに向いています。

 今回紹介するのは、Webプラットフォームにおけるアプリ開発です。Webプラットフォームでは、「Rust and WebAssembly」を使用したWebAssemblyバイナリを生成し、ブラウザ上でSPAとしてアプリを実行します。WebAssemblyバイナリによる実行となるので、メモリの利用効率が高く、JavaScriptと比較して高速に動作します。

 Dioxusは、その設計思想においてReact(JavaScriptによるコンポーネント指向のUI構築ライブラリ)の影響を強く受けています。コンポーネント、フック、propsなどの考え方は、ほとんどReactと共通であり、Reactの経験者ならDioxusの理解は比較的容易と思われます。ただし本記事では、それぞれについて都度言及します。

 なお、本稿作成時点でDioxusはβ版(v0.4)であり、安定版リリースには達していません。ただし、Webベースの機能の変更はほとんどないであろうと公式ドキュメントでは言及されているので、導入を前提とした検証には問題のない状況と言えます。

Dioxusを使用する準備

 Dioxusを使用するために、幾つかの準備作業を済ませておきます。

 Dioxusには、専用のCLIツール(dioxus-cli)が用意されているので、それをインストールします。このツールのdxコマンドによって、アプリケーションのビルドの他、WebAssemblyバイナリの生成、サーバの起動などを一括して実行してくれるので便利です。

% cargo install dioxus-cli -force

 続けて、WebAssemblyのビルドターゲットであるwasm32-unknown-unknownを有効にします。すでに有効になっていれば、実行例のようにup to dateと出力されます。

% rustup target add wasm32-unknown-unknown
info: component 'rust-std' for target 'wasm32-unknown-unknown' is up to date

アプリの作成

 アプリの作成は、通常通りcargo newコマンドで実行します。ここでは、dioxus-postsとしました。作成後、カレントフォルダをアプリケーションのルートに移動しておきます。

% cargo new --bin dioxus-posts
% cd dioxus-posts

 クレートdioxusとクレートdioxus-webをプロジェクトに追加します。これらは、Webプラットフォームの使用に最低限必要なクレートです。その他、必要となるクレートは都度追加していきます。

% cargo add dioxus dioxus-web

「Hello, World!!」アプリの実装

 Dioxusが正しく動作するかどうかの確認と、アプリの基本構造を理解するために、ブラウザ画面に「Hello, World!!」と表示するだけの機能をアプリに実装します。src/main.rsファイルの既定の内容は削除し、リスト1の内容を記述します。

use dioxus::prelude::*;		(1)
fn main() {
    dioxus_web::launch(app);	(2)
}
fn app(cx: Scope) -> Element {	(3)
    cx.render(			(4)
     rsx! {			(5)
        div { "Hello, World!!" }
    })
}
リスト1:src/main.rs

 コードの説明は後にして、まずはビルドして実行してみましょう。ビルドと実行は、DioxusのCLIツールであるdxコマンドで行えます。なお、serveサブコマンドはビルドとウォッチ付きの実行を意味します。buildサブコマンドではビルドのみとなりますので、使い分けられます。

% dx serve

 初回のビルドにはかなり時間がかかります。ビルド終了後、ターミナルに以下のように出力されれば、サーバが待機状態となっています。「Local」あるいは「Network」に表示されているURLにブラウザでアクセスして、図1のように表示されれば成功です。

Dioxus @ v0.4.1 [09:18:42] 
        > Local : http://localhost:8080/
        > Network : http://192.168.108.49:8080/
        > HTTPS : Disabled
…略…
図1 「Hello,World!!」アプリの表示 図1 「Hello,World!!」アプリの表示

 ここで、リスト1の「Hello, World!!」アプリのコードを説明します。これは、Webアプリの基本形となります。Reactの経験者なら、構造が非常に似ていることに気付くでしょう。

 (1)は、Dioxusの基本機能であるprelude(利用頻度の高い名前空間をまとめたもの)を使うためのuse文です。

 (2)は、アプリのルートとなるappコンポーネントをlaunchメソッドでマウントしています。この部分は、どのようなアプリでもあまり変わりません。また、コンポーネントとは無関係の処理(ロガーの有効化など)は、この前で実施しておきます。

 (3)は、appコンポーネントの関数定義です。このように、コンポーネントはScope型の引数を受け取り、Element型を返すのが基本です。Scope型とはコンテキストを意味しますが、詳しくは後述します。Element型は、文字通りHTML要素を指します。つまり、コンポーネントの関数はページ上に現れるHTMLを生成(レンダリング)して返すのが基本的な役割となります。

 (4)は、コンポーネントのレンダリングを担当するrenderメソッドの呼び出しです。コンポーネントのレンダリングが必要なときには、renderメソッドが実行されることを覚えておいてください。renderメソッドの引数はレンダリングのためのコードです。コードを実行した結果をElement型として返すので、このメソッドの処理結果がapp関数の戻り値となるわけです。

 (5)は、rsx!マクロです。このマクロのブロック内にはHTMLをRSX構文というもので記述します。ReactにおけるJSX構文(JavaScriptコード中にHTMLを直接記述できる仕組み)のようなイメージです。

 実際のアプリはこのように単純ではないですが、これまでの回でActix Webで作成してきたアプリをDioxusで置き換えるサンプルを通じて、Dioxusの持つ豊富な機能の一部を紹介します。

【補足】コンポーネント

 コンポーネント(Component)とは、Webページに配置する特定の機能を持った部品を意味します。appコンポーネントは、その名の通りアプリの全体を表すコンポーネントとなります。コンポーネントには階層構造を持たせることができ、コンポーネント内に別のコンポーネントを配置しながら、アプリを構築します。このような考え方はコンポーネント指向と呼ばれます。

投稿アプリ実装の準備

 投稿アプリに機能を実装する前に、基本的な部分を準備しておきます。

ロガーを準備する

 アプリで発生する問題を解決しやすくするために、ロガーを準備しておきます。ロガーとして、標準のlogクレートに加えて、Dioxusのロガーであるdioxus-loggerクレートを追加します。このクレートを使うと、ログの出力先をJavaScriptのconsolo.log()呼び出しとすることができます。

% cargo add log dioxus-logger

 ロガーを有効にするために、src/main.rsファイルに、リスト2のようにロガーのuse文(1)と、dioxus-loggerの初期化コード(2)を追記します。最低ログレベルはinfoとしています。この初期化処理により、通常のinfo!マクロなどによるログの出力ができるようになります(3)。ログの出力は前述のようにconsole.log()によるので、ここでは特に例は示しませんが、確認はブラウザの開発者ツールなどを活用します。

…略…
use log::info;	(1)
fn main() {
    dioxus_logger::init(log::LevelFilter::Info).unwrap();	(2)
    dioxus_web::launch(App);
}
fn App(cx: Scope) -> Element {
    info!("Called App");	(3)
…略…
リスト2:src/main.rs

テンプレートHTMLを用意する

 「Hello, World!!」アプリでは、index.htmlに相当するHTMLファイルはDioxusが自動生成していました。自動生成させずに独自のindex.htmlファイルを用意してテンプレートとすることができますので、これまでの回に倣って用意します。テンプレートを使うと、今回のサンプルのようにBootstrapを使用したスタイル定義も容易になります。

 テンプレートHTMLとしての条件は、id属性が"main"であるdiv要素を配置するのみです。Dioxusは、この要素を見つけて、そこにWebAssemblyファイルの読み込みタグや、launchメソッドの呼び出しコードを配置します。このファイルは、プロジェクトのルートに配置すると、自動的に認識されます。リスト3では、連載第3回で紹介済みの部分は省略しているので、全体は配布サンプルを参照してください。dx serveコマンドで実行すると、図2のようにテンプレートが適用された形になります。

…略…
<body>
    <div class="container">
        <h1 style="text-align: center;">Posts</h1>
        <hr />
        <div id="main">		★
        </div>
        <hr />
        <div style="text-align: center;">Tamaplaza Digital</div>
    </div>
…略…
リスト3:index.html
図2 index.htmlファイル配置後の「Hello,World!!」アプリ 図2 index.htmlファイル配置後の「Hello,World!!」アプリ

APIの呼び出しと表示

 APIの呼び出しと表示を試します。外部リソースを利用するアプリの基本的な形となります。

API呼び出し関数を用意する

 連載第6回で作成したREST APIを呼び出す関数を用意します。プロジェクトのsrcフォルダにdata.rsファイルを作成して、前回までと同様にデータアクセスのための関数を入れていきます。

 関数の記述に先立ち、以下のクレートをプロジェクトに追加します。これらは、アプリ中でデータの取得や加工に共通して利用するクレートです。

  • serde、serde-json:JSONデータのシリアライズとデシリアライズ(連載第2回で紹介)
  • reqwest:APIにリクエストを発行(requestではないので注意)
  • im_rc:不変(immutable)なデータ構造を保持
% cargo add serde serde-json reqwest im_rc

 serdeクレートとreqwestクレートでは、構造体へのシリアライザ実装のためのderiveマクロ、JSONデコードのためのjsonメソッドを有効にするために、リスト4のようにCargo.tomlファイルを修正します。

…略…
im-rc = "15.1.0"
reqwest = { version = "0.11.26", features = ["json"] }
serde = { version = "1.0.197", features = ["derive"] }
serde_json = "1.0.114"
リスト4:Cargo.toml

 ここではまず、関数の形式を理解するために投稿一覧を取得する関数call_indexのみを実装します。リスト5のように記述してください。これまでの回で登場済みの箇所(★)は説明を省略します。API提供側と同じ構造体(enum)を使えることに注目してください。APIのURLの様式は、連載第6回を参照してください。

use serde::{Serialize, Deserialize};	★
static BASE_API_URL: &str = "http://localhost:8000/api/posts";	(1)
#[derive(Serialize, Deserialize, Debug, Clone)]	★
pub struct Message {
    …略…
}
#[derive(Serialize, Deserialize, Debug)]	★
pub enum ResponseContent {
    …略…
}
#[derive(Serialize, Deserialize, Debug)]	★
pub struct ApiResponse {
    …略…
}
pub async fn call_index() -> Result<ApiResponse, reqwest::Error> {	(2)
    let url = format!("{}", BASE_API_URL);
    reqwest::get(&url).await?.json::<ApiResponse>().await	(3)
}
リスト5:src/data.rs

 (1)は、APIリクエストURLの共通部です。ここでは自ホスト(localhost:8000)で動作するActix Webサーバとしてあるので、別ホストにAPIをサービスさせるといった場合は適宜変更してください。

 (2)は、投稿一覧取得のAPIを呼び出すcall_index関数の定義です。性質上、asyncキーワードを付加して非同期関数としています。戻り値は、reqwest::getメソッドの呼び出し結果であるResult構造体に合わせてあります。成功すればApiResponse型を返し、失敗したらErrorを返します。

 (3)は、APIの呼び出しです。メソッドチェーンによって、getメソッドによるデータ取得、awaitによる待機、jsonメソッドによるJSONデータのデシリアライズ、awaitによる待機を、順番に実施します。

【補足】API側でのCORS対応

 今回のサンプルはWebAssemblyバイナリとしてブラウザ上で実行されるので、外部リソース(API)へのアクセスは同一オリジンポリシー(Same-Origin Policy)に従います。APIサーバとDioxusアプリサーバは別に起動するので、同一のオリジンで提供できません。そのため、API側ではCORS(Cross Origin Resource Sharing)によって特定のオリジンからのアクセスを許可するようにレスポンスヘッダを追加する必要があります。今回の配布サンプルには、CORS対応を施したAPIサーバのプロジェクト(actix-posts)を含めています。このプロジェクトのbuild_response関数(header.rs内)において、レスポンスを生成する部分に以下のようにレスポンスヘッダを付加しています。

HttpResponse::Ok()
    .insert_header(("Access-Control-Allow-Origin", "*"))	追記
    .json(response)

API呼び出し結果を表示する

 appコンポーネントに、call_index関数を呼び出してその結果(JSON)をそのまま表示するコードをリスト6のように記述します。app関数中の「Hello, World!!」アプリのコードは全て削除するかコメントアウトしてください。

…略…
mod data;					(1)
use data::{ResponseContent};
…略…
fn app(cx: Scope) -> Element {
  let posts_source = use_future(cx, (), |_| data::call_index());	(2)
  cx.render(	(3)
    match posts_source.value() {	(4)
      Some(Ok(res)) => {
        match &res.result {		(5)
          ResponseContent::Items(items) => {
            rsx! {			(6)
              for item in items {
                rsx! { div { "{serde_json::to_string(&item).unwrap()}" } }	(7)
              }
            }
          },
          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 { "データを読み込んでいます..." } }
    }
  )
}
リスト5:src/main.rs

 これは、フック(下記[補足]を参照)を使用してAPIを非同期で呼び出して、結果が得られたらHTMLを生成して表示するというコードです。

 (1)は、API呼び出し関数を記述したdata.rsをモジュールとして使うことと、参照する構造体を指定しています。

 (2)では、use_futureメソッドで非同期関数の呼び出しをフックしています。3つ目の引数は実行するクロージャであり、この場合はcall_index関数を呼び出すのみです。use_futureメソッドの戻り値は非同期処理の状況を保持し、Ok()なら終了、Err()なら何らかのエラーが発生、Noneはそのどちらでもないことを示しています。

 (3)は、コンポーネント全体のレンダリングが必要になったら呼び出されるrenderメソッドです。レンダリングは、フックの変更がトリガーになります。この場合は、use_futureメソッドの戻り値が変化した(API呼び出し関数が終了したなど)時です。

 (4)は、use_futureメソッドの戻り値によるレンダリング内容の仕訳です。Ok()、Err()、Noneで、それぞれ取得データのレンダリング、エラーメッセージのレンダリング、ローディングメッセージのレンダリングを実施します。

 (5)は、取得したデータによるレンダリング内容の仕訳です。リスト1では単にHTMLを記述しましたが、(6)のようにRustのコードも記述できます。rsx!マクロについては次回で詳しく紹介しますが、(7)のようにRustの式を文字列中に埋め込むことも可能です。

 ここからはAPIにアクセスするので、別途今回のサンプルに含まれるAPIサーバを起動しておいてください。dx serveコマンドを実行してブラウザでアクセスし、図3のように取得結果がJSON文字列で表示されれば成功です。

図3 API呼び出しと結果の表示 図3 API呼び出しと結果の表示

【補足】フック

 フック(hook)とは、コンポーネント内に状態を作るための仕組みです。Dioxusでは多くの組み込みフックが用意されており、独自にカスタムフックを作成することも可能です。以下は、本連載で使うフックです。

  • use_state:単純な値の保持。プリミティブ型の保持に使う
  • use_ref:参照の保持。構造体などの保持に使う
  • use_future:Future(非同期関数の呼び出し)を使用した値の取得

まとめ

 今回は、第6回で作成した投稿アプリのREST APIを「Dioxus」から呼び出して生データのまま表示する例を紹介しました。

 次回は、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.

スポンサーからのお知らせPR

注目のテーマ

Microsoft & Windows最前線2025
AI for エンジニアリング
ローコード/ノーコード セントラル by @IT - ITエンジニアがビジネスの中心で活躍する組織へ
Cloud Native Central by @IT - スケーラブルな能力を組織に
システム開発ノウハウ 【発注ナビ】PR
あなたにおすすめの記事PR

RSSについて

アイティメディアIDについて

メールマガジン登録

@ITのメールマガジンは、 もちろん、すべて無料です。ぜひメールマガジンをご購読ください。