チュートリアル
はじめに
このハンズオンチュートリアルでは、Yewを使ってWebアプリケーションを構築する方法を見ていきます。**Yew**は、Rustを使ってWebAssemblyでフロントエンドWebアプリを構築するための最新のフレームワークです。Yewは、Rustの強力な型システムを活用することで、再利用可能で、保守性が高く、よく構造化されたアーキテクチャを促進します。Rustではクレートとして知られる、コミュニティによって作成されたライブラリの大きなエコシステムは、状態管理などの一般的に使用されるパターン用のコンポーネントを提供します。RustのパッケージマネージャーであるCargoを使用すると、crates.ioで利用可能なYewなどの多数のクレートを活用できます。
何を構築するのか
Rustconfは、毎年開催されるRustコミュニティの銀河系集会です。Rustconf 2020では、豊富な情報が得られる講演が多数ありました。このハンズオンチュートリアルでは、仲間のRustaceanが講演の概要を把握し、1つのページからすべてを視聴できるWebアプリケーションを構築します。
セットアップ
前提条件
このチュートリアルでは、Rustに既に精通していることを前提としています。Rustを初めて使用する場合は、無料のRustブックが初心者にとって最適な出発点を提供し、経験豊富なRust開発者にとっても優れたリソースであり続けています。
rustup update
を実行するか、まだインストールしていない場合はrustをインストールして、最新バージョンのRustがインストールされていることを確認してください。
Rustをインストールした後、Cargoを使用して、次のコマンドを実行することで`trunk`をインストールできます。
cargo install trunk
また、次のコマンドを実行してWASMビルドターゲットを追加する必要があります。
rustup target add wasm32-unknown-unknown
プロジェクトのセットアップ
まず、新しいcargoプロジェクトを作成します
cargo new yew-app
cd yew-app
Rust環境が正しくセットアップされていることを確認するには、cargoビルドツールを使用して初期プロジェクトを実行します。ビルドプロセスに関する出力の後、期待される「Hello, world!」メッセージが表示されます。
cargo run
最初の静的ページ
この単純なコマンドラインアプリケーションを基本的なYew Webアプリケーションに変換するには、いくつかの変更が必要です。次のようにファイルを更新します
[package]
name = "yew-app"
version = "0.1.0"
edition = "2021"
[dependencies]
yew = { git = "https://github.com/yewstack/yew/", features = ["csr"] }
アプリケーションを構築する場合にのみ、機能`csr`が必要です。`Renderer`とすべてのクライアント側レンダリング関連のコードが有効になります。
ライブラリを作成する場合は、サーバー側レンダリングバンドルにクライアント側レンダリングロジックがプルインされるため、この機能を有効にしないでください。
テストまたは例にRendererが必要な場合は、代わりに`dev-dependencies`で有効にする必要があります。
use yew::prelude::*;
#[function_component(App)]
fn app() -> Html {
html! {
<h1>{ "Hello World" }</h1>
}
}
fn main() {
yew::Renderer::<App>::new().render();
}
次に、プロジェクトのルートに`index.html`を作成します。
<!doctype html>
<html lang="en">
<head></head>
<body></body>
</html>
開発サーバーの起動
次のコマンドを実行して、アプリケーションをローカルでビルドして提供します。
trunk serve --open
デフォルトのブラウザ`trunk serve`を開かない場合は、オプション'--open'を削除します。
Trunkはデフォルトのブラウザでアプリケーションを開き、プロジェクトディレクトリを監視し、ソースファイルを変更するとアプリケーションを再構築します。ソケットが別のアプリケーションで使用されている場合、これは失敗します。デフォルトでは、サーバーはアドレス '127.0.0.1'とポート '8080' => http://localhost:8080でリッスンします。変更するには、次のファイルを作成して必要に応じて編集します
[serve]
# The address to serve on LAN.
address = "127.0.0.1"
# The address to serve on WAN.
# address = "0.0.0.0"
# The port to serve on.
port = 8000
興味がある場合は、`trunk help`と`trunk help <subcommand>`を実行して、何が起こっているかについての詳細を確認できます。
おめでとうございます
これで、Yew開発環境を正常にセットアップし、最初のYew Webアプリケーションを構築しました。
HTMLの構築
YewはRustの手続き型マクロを利用し、JSX(JavaScript内でHTMLのようなコードを書くことができるJavaScriptの拡張機能)に似た構文を提供してマークアップを作成します。
従来のHTMLの変換
Webサイトがどのように見えるかについては既にかなり良い考えがあるので、メンタルドラフトを`html!`と互換性のある表現に変換するだけです。単純なHTMLを書くことに慣れている場合は、`html!`内にマーキングを書くのに問題はないはずです。マクロはHTMLとはいくつかの点で異なることに注意することが重要です
- 式は中括弧(`{ }`)で囲む必要があります
- ルートノードは1つだけである必要があります。コンテナにラップせずに複数の要素を使用する場合は、空のタグ/フラグメント(`<> ... </>`)が使用されます
- 要素は正しく閉じる必要があります。
生のHTMLで次のようになるレイアウトを構築したいと思います
<h1>RustConf Explorer</h1>
<div>
<h3>Videos to watch</h3>
<p>John Doe: Building and breaking things</p>
<p>Jane Smith: The development process</p>
<p>Matt Miller: The Web 7.0</p>
<p>Tom Jerry: Mouseless development</p>
</div>
<div>
<h3>John Doe: Building and breaking things</h3>
<img
src="https://via.placeholder.com/640x360.png?text=Video+Player+Placeholder"
alt="video thumbnail"
/>
</div>
では、このHTMLを`html!`に変換してみましょう。`html!`の値が関数によって返されるように、次のスニペットを`app`関数の本文に入力(またはコピー/貼り付け)します
html! {
<>
<h1>{ "RustConf Explorer" }</h1>
<div>
<h3>{"Videos to watch"}</h3>
<p>{ "John Doe: Building and breaking things" }</p>
<p>{ "Jane Smith: The development process" }</p>
<p>{ "Matt Miller: The Web 7.0" }</p>
<p>{ "Tom Jerry: Mouseless development" }</p>
</div>
<div>
<h3>{ "John Doe: Building and breaking things" }</h3>
<img src="https://via.placeholder.com/640x360.png?text=Video+Player+Placeholder" alt="video thumbnail" />
</div>
</>
}
ブラウザページを更新すると、次の出力が表示されます
マークアップでのRust言語構成の使用
Rustでマークアップを書くことの大きな利点は、マークアップでRustのすべてのクールさを得られることです。HTMLにビデオのリストをハードコーディングする代わりに、それらを`Vec` of `Video`構造体として定義しましょう。データを保持する単純な`struct`(`main.rs`または任意のファイル)を作成します。
struct Video {
id: usize,
title: String,
speaker: String,
url: String,
}
次に、`app`関数でこの構造体のインスタンスを作成し、データをハードコーディングする代わりにそれらを使用します
use website_test::tutorial::Video; // replace with your own path
let videos = vec![
Video {
id: 1,
title: "Building and breaking things".to_string(),
speaker: "John Doe".to_string(),
url: "https://youtu.be/PsaFVLr8t4E".to_string(),
},
Video {
id: 2,
title: "The development process".to_string(),
speaker: "Jane Smith".to_string(),
url: "https://youtu.be/PsaFVLr8t4E".to_string(),
},
Video {
id: 3,
title: "The Web 7.0".to_string(),
speaker: "Matt Miller".to_string(),
url: "https://youtu.be/PsaFVLr8t4E".to_string(),
},
Video {
id: 4,
title: "Mouseless development".to_string(),
speaker: "Tom Jerry".to_string(),
url: "https://youtu.be/PsaFVLr8t4E".to_string(),
},
];
それらを表示するには、`Vec`を`Html`に変換する必要があります。イテレータを作成し、それを`html!`にマップし、`Html`として収集することで、それを行うことができます
let videos = videos.iter().map(|video| html! {
<p key={video.id}>{format!("{}: {}", video.speaker, video.title)}</p>
}).collect::<Html>();
リストアイテムのキーは、Yewがリスト内で変更されたアイテムを追跡するのに役立ち、再レンダリングを高速化します。リストでキーを使用することを常にお勧めします。
最後に、ハードコーディングされたビデオのリストを、データから作成した`Html`に置き換える必要があります
html! {
<>
<h1>{ "RustConf Explorer" }</h1>
<div>
<h3>{ "Videos to watch" }</h3>
- <p>{ "John Doe: Building and breaking things" }</p>
- <p>{ "Jane Smith: The development process" }</p>
- <p>{ "Matt Miller: The Web 7.0" }</p>
- <p>{ "Tom Jerry: Mouseless development" }</p>
+ { videos }
</div>
// ...
</>
}
コンポーネント
コンポーネントは、Yewアプリケーションの構成要素です。他のコンポーネントで構成できるコンポーネントを組み合わせることで、アプリケーションを構築します。再利用性を考慮してコンポーネントを構築し、汎用性を維持することで、コードやロジックを複製することなく、アプリケーションの複数の部分でコンポーネントを使用できます。
これまでに使用してきた`app`関数は、`App`と呼ばれるコンポーネントです。「関数コンポーネント」です。Yewには2種類のコンポーネントがあります。
- 構造体コンポーネント
- 関数コンポーネント
このチュートリアルでは、関数コンポーネントを使用します。
では、`App`コンポーネントをより小さなコンポーネントに分割してみましょう。ビデオリストを独自のコンポーネントに抽出することから始めます。
use yew::prelude::*;
struct Video {
id: usize,
title: String,
speaker: String,
url: String,
}
#[derive(Properties, PartialEq)]
struct VideosListProps {
videos: Vec<Video>,
}
#[function_component(VideosList)]
fn videos_list(VideosListProps { videos }: &VideosListProps) -> Html {
videos.iter().map(|video| html! {
<p key={video.id}>{format!("{}: {}", video.speaker, video.title)}</p>
}).collect()
}
`VideosList`関数コンポーネントのパラメータに注目してください。関数コンポーネントは、「props」(「プロパティ」の略)を定義する引数を1つだけ取ります。Propsは、親コンポーネントから子コンポーネントにデータを渡すために使用されます。この場合、`VideosListProps`はpropsを定義する構造体です。
propsに使用される構造体は、`Properties`を派生させることによって実装する必要があります。
上記のコードをコンパイルするには、`Video`構造体を次のように変更する必要があります
#[derive(Clone, PartialEq)]
struct Video {
id: usize,
title: String,
speaker: String,
url: String,
}
これで、`VideosList`コンポーネントを使用するように`App`コンポーネントを更新できます。
#[function_component(App)]
fn app() -> Html {
// ...
- let videos = videos.iter().map(|video| html! {
- <p key={video.id}>{format!("{}: {}", video.speaker, video.title)}</p>
- }).collect::<Html>();
-
html! {
<>
<h1>{ "RustConf Explorer" }</h1>
<div>
<h3>{"Videos to watch"}</h3>
- { videos }
+ <VideosList videos={videos} />
</div>
// ...
</>
}
}
ブラウザウィンドウを見ると、リストが正しくレンダリングされていることを確認できます。リストのレンダリングロジックをコンポーネントに移動しました。これにより、`App`コンポーネントのソースコードが短縮され、読みやすく理解しやすくなります。
インタラクティブにする
ここでの最終的な目標は、選択したビデオを表示することです。そのためには、`VideosList`コンポーネントは、ビデオが選択されたときに親に「通知」する必要があります。これは`Callback`を介して行われます。この概念は「ハンドラの受け渡し」と呼ばれます。`on_click`コールバックを受け取るようにpropsを変更します
#[derive(Properties, PartialEq)]
struct VideosListProps {
videos: Vec<Video>,
+ on_click: Callback<Video>
}
次に、選択されたビデオをコールバックに「発行」するように VideosList
コンポーネントを変更します。
#[function_component(VideosList)]
-fn videos_list(VideosListProps { videos }: &VideosListProps) -> Html {
+fn videos_list(VideosListProps { videos, on_click }: &VideosListProps) -> Html {
+ let on_click = on_click.clone();
videos.iter().map(|video| {
+ let on_video_select = {
+ let on_click = on_click.clone();
+ let video = video.clone();
+ Callback::from(move |_| {
+ on_click.emit(video.clone())
+ })
+ };
html! {
- <p key={video.id}>{format!("{}: {}", video.speaker, video.title)}</p>
+ <p key={video.id} onclick={on_video_select}>{format!("{}: {}", video.speaker, video.title)}</p>
}
}).collect()
}
次に、そのコールバックを渡すように VideosList
の使用方法を変更する必要があります。しかし、その前に、ビデオがクリックされたときに表示される新しいコンポーネント VideoDetails
を作成する必要があります。
use website_test::tutorial::Video;
use yew::prelude::*;
#[derive(Properties, PartialEq)]
struct VideosDetailsProps {
video: Video,
}
#[function_component(VideoDetails)]
fn video_details(VideosDetailsProps { video }: &VideosDetailsProps) -> Html {
html! {
<div>
<h3>{ video.title.clone() }</h3>
<img src="https://via.placeholder.com/640x360.png?text=Video+Player+Placeholder" alt="video thumbnail" />
</div>
}
}
それでは、ビデオが選択されるたびに VideoDetails
コンポーネントを表示するように App
コンポーネントを変更します。
#[function_component(App)]
fn app() -> Html {
// ...
+ let selected_video = use_state(|| None);
+ let on_video_select = {
+ let selected_video = selected_video.clone();
+ Callback::from(move |video: Video| {
+ selected_video.set(Some(video))
+ })
+ };
+ let details = selected_video.as_ref().map(|video| html! {
+ <VideoDetails video={video.clone()} />
+ });
html! {
<>
<h1>{ "RustConf Explorer" }</h1>
<div>
<h3>{"Videos to watch"}</h3>
- <VideosList videos={videos} />
+ <VideosList videos={videos} on_click={on_video_select.clone()} />
</div>
+ { for details }
- <div>
- <h3>{ "John Doe: Building and breaking things" }</h3>
- <img src="https://via.placeholder.com/640x360.png?text=Video+Player+Placeholder" alt="video thumbnail" />
- </div>
</>
}
}
今は use_state
については気にしないでください。後で戻ってきます。 { for details }
で行ったトリックに注目してください。 Option<_>
は Iterator
を実装しているので、Iterator
によって返される唯一の要素を、html!
マクロでサポートされている特別な { for ... }
構文を使用して表示できます。
状態の処理
前に使用した use_state
を覚えていますか?これは「フック」と呼ばれる特別な関数です。フックは、関数コンポーネントのライフサイクルに「フック」してアクションを実行するために使用されます。このフックや他のフックの詳細については、こちらをご覧ください。
構造体コンポーネントは動作が異なります。詳細については、ドキュメントを参照してください。
データの取得(外部 REST API を使用)
実際のアプリケーションでは、データはハードコードされるのではなく、通常 API から取得されます。外部ソースからビデオリストを取得してみましょう。そのためには、次のクレートを追加する必要があります。
- フェッチ呼び出しを行うための
gloo-net
- JSON レスポンスをデシリアライズするための derive 機能付きの
serde
- Rust Future を Promise として実行するための
wasm-bindgen-futures
Cargo.toml
ファイルの依存関係を更新しましょう。
[dependencies]
gloo-net = "0.2"
serde = { version = "1.0", features = ["derive"] }
wasm-bindgen-futures = "0.4"
依存関係を選択する際は、wasm32
と互換性があることを確認してください!そうでない場合、アプリケーションを実行できません。
Deserialize
トレイトを派生するように Video
構造体を更新します。
+ use serde::Deserialize;
- #[derive(Clone, PartialEq)]
+ #[derive(Clone, PartialEq, Deserialize)]
struct Video {
id: usize,
title: String,
speaker: String,
url: String,
}
最後のステップとして、ハードコードされたデータを使用する代わりにフェッチリクエストを行うように App
コンポーネントを更新する必要があります。
+ use gloo_net::http::Request;
#[function_component(App)]
fn app() -> Html {
- let videos = vec![
- // ...
- ]
+ let videos = use_state(|| vec![]);
+ {
+ let videos = videos.clone();
+ use_effect_with((), move |_| {
+ let videos = videos.clone();
+ wasm_bindgen_futures::spawn_local(async move {
+ let fetched_videos: Vec<Video> = Request::get("https://yew.dokyumento.jp/tutorial/data.json")
+ .send()
+ .await
+ .unwrap()
+ .json()
+ .await
+ .unwrap();
+ videos.set(fetched_videos);
+ });
+ || ()
+ });
+ }
// ...
html! {
<>
<h1>{ "RustConf Explorer" }</h1>
<div>
<h3>{"Videos to watch"}</h3>
- <VideosList videos={videos} on_click={on_video_select.clone()} />
+ <VideosList videos={(*videos).clone()} on_click={on_video_select.clone()} />
</div>
{ for details }
</>
}
}
これはデモアプリケーションなので、ここでは unwrap
を使用しています。実際のアプリケーションでは、適切なエラー処理を行うことが望ましいでしょう。
それでは、ブラウザで全てが期待通りに動作していることを確認してください... CORS がなければそうだったでしょう。これを修正するには、プロキシサーバーが必要です。幸いなことに、trunk がそれを提供しています。
次の行を更新します。
// ...
- let fetched_videos: Vec<Video> = Request::get("https://yew.dokyumento.jp/tutorial/data.json")
+ let fetched_videos: Vec<Video> = Request::get("/tutorial/data.json")
// ...
それでは、次のコマンドでサーバーを再実行します。
trunk serve --proxy-backend=https://yew.dokyumento.jp/tutorial
タブを更新すると、すべてが期待通りに動作するはずです。
まとめ
おめでとうございます!外部 API からデータを取得してビデオのリストを表示する Web アプリケーションを作成しました。
次のステップ
このアプリケーションは、完璧でも便利でもありません。このチュートリアルを完了したら、これを足掛かりとして、より高度なトピックを探索できます。
スタイル
私たちのアプリは非常に醜いです。CSS やスタイルがありません。残念ながら、Yew はコンポーネントをスタイル設定するための組み込みの方法を提供していません。スタイルシートを追加する方法については、Trunk のアセットを参照してください。
その他のライブラリ
私たちのアプリは、ほんの少数の外部依存関係しか使用していません。使用できるクレートはたくさんあります。詳細については、外部ライブラリを参照してください。
Yew についてさらに学ぶ
公式ドキュメントをご覧ください。多くの概念がはるかに詳細に説明されています。Yew API の詳細については、API ドキュメントを参照してください。