【Rust】Yew公式サイトの日本語対応が進むらしい+チュートリアルを試す
が バージョンに変わろうとしている
そんな
日本語トップ画面(2025/03/16現在)
https://yew.rs/ja/
https://yew.rs/ja/docs/getting-started/introduction
日本語⋯?
しかし最近になり、現在最新の「
https://yew.rs/ja/docs/next/getting-started/introduction
どうやら全てのページが日本語になっているわけではないようですが、これまで日本語を探す方が大変だったのに対して、今度は英語のページを探す方が難しくなりました。それだけのことですが、なんだか嬉しく感じます。
環境構築
-
を用意する
プログラミング言語であるを用意します。但し、環境によって導入手順が異なるため、本記事は具体的な手順を省略します。順路はどうあれ、最終的に cargo
が使えるようになれば宜いです。rustc
は使いません。cargo --helpの様子
cargoコマンドPS C:\⋯> cargo --help Rust's package manager Usage: cargo [+toolchain] [OPTIONS] [COMMAND] cargo [+toolchain] [OPTIONS] -Zscript <MANIFEST_RS> [ARGS]... Options: -V, --version Print version info and exit --list List installed commands --explain <CODE> Provide a detailed explanation of a rustc error message -v, --verbose... Use verbose output (-vv very verbose/build.rs output) -q, --quiet Do not print cargo log messages --color <WHEN> Coloring: auto, always, never -C <DIRECTORY> Change to DIRECTORY before doing anything (nightly-only) --locked Assert that `Cargo.lock` will remain unchanged --offline Run without accessing the network --frozen Equivalent to specifying both --locked and --offline --config <KEY=VALUE|PATH> Override a configuration value -Z <FLAG> Unstable (nightly-only) flags to Cargo, see 'cargo -Z help' for details -h, --help Print help Commands: build, b Compile the current package check, c Analyze the current package and report errors, but don't build object files clean Remove the target directory doc, d Build this package's and its dependencies' documentation new Create a new cargo package init Create a new cargo package in an existing directory add Add dependencies to a manifest file remove Remove dependencies from a manifest file run, r Run a binary or example of the local package test, t Run the tests bench Run the benchmarks update Update dependencies listed in Cargo.lock search Search registry for crates publish Package and upload this package to the registry install Install a Rust binary uninstall Uninstall a Rust binary ... See all commands with --list See 'cargo help <command>' for more information on a specific command.
-
の「 」を取得する という語には聞き馴染がないかもしれません。私は の記事で初めて知りました。百聞は一見に如かず、 rustup
で一覧を見ることができます。rustup target listの様子
-
で実行した場合
PS C:\⋯> rustup target list aarch64-apple-darwin aarch64-apple-ios aarch64-apple-ios-macabi aarch64-apple-ios-sim aarch64-linux-android aarch64-pc-windows-gnullvm aarch64-pc-windows-msvc aarch64-unknown-fuchsia aarch64-unknown-linux-gnu aarch64-unknown-linux-musl aarch64-unknown-linux-ohos aarch64-unknown-none aarch64-unknown-none-softfloat aarch64-unknown-uefi arm-linux-androideabi arm-unknown-linux-gnueabi arm-unknown-linux-gnueabihf arm-unknown-linux-musleabi arm-unknown-linux-musleabihf arm64ec-pc-windows-msvc armebv7r-none-eabi armebv7r-none-eabihf armv5te-unknown-linux-gnueabi armv5te-unknown-linux-musleabi armv7-linux-androideabi armv7-unknown-linux-gnueabi armv7-unknown-linux-gnueabihf armv7-unknown-linux-musleabi armv7-unknown-linux-musleabihf armv7-unknown-linux-ohos armv7a-none-eabi armv7r-none-eabi armv7r-none-eabihf i586-pc-windows-msvc i586-unknown-linux-gnu i586-unknown-linux-musl i686-linux-android i686-pc-windows-gnu i686-pc-windows-gnullvm i686-pc-windows-msvc i686-unknown-freebsd i686-unknown-linux-gnu i686-unknown-linux-musl i686-unknown-uefi loongarch64-unknown-linux-gnu loongarch64-unknown-linux-musl loongarch64-unknown-none loongarch64-unknown-none-softfloat nvptx64-nvidia-cuda powerpc-unknown-linux-gnu powerpc64-unknown-linux-gnu powerpc64le-unknown-linux-gnu powerpc64le-unknown-linux-musl riscv32i-unknown-none-elf riscv32im-unknown-none-elf riscv32imac-unknown-none-elf riscv32imafc-unknown-none-elf riscv32imc-unknown-none-elf riscv64gc-unknown-linux-gnu riscv64gc-unknown-linux-musl riscv64gc-unknown-none-elf riscv64imac-unknown-none-elf s390x-unknown-linux-gnu sparc64-unknown-linux-gnu sparcv9-sun-solaris thumbv6m-none-eabi thumbv7em-none-eabi thumbv7em-none-eabihf (installed) thumbv7m-none-eabi thumbv7neon-linux-androideabi thumbv7neon-unknown-linux-gnueabihf thumbv8m.base-none-eabi thumbv8m.main-none-eabi thumbv8m.main-none-eabihf wasm32-unknown-emscripten wasm32-unknown-unknown (installed) wasm32-wasip1 wasm32-wasip1-threads wasm32-wasip2 wasm32v1-none x86_64-apple-darwin x86_64-apple-ios x86_64-apple-ios-macabi x86_64-fortanix-unknown-sgx x86_64-linux-android x86_64-pc-solaris x86_64-pc-windows-gnu x86_64-pc-windows-gnullvm x86_64-pc-windows-msvc (installed) x86_64-unknown-freebsd x86_64-unknown-fuchsia x86_64-unknown-illumos x86_64-unknown-linux-gnu x86_64-unknown-linux-gnux32 x86_64-unknown-linux-musl x86_64-unknown-linux-ohos x86_64-unknown-netbsd x86_64-unknown-none x86_64-unknown-redox x86_64-unknown-uefi
-
( )で実行した場合
⋯@cloudshell:~$ rustup target list aarch64-apple-darwin aarch64-apple-ios aarch64-apple-ios-sim aarch64-linux-android aarch64-pc-windows-gnullvm aarch64-pc-windows-msvc aarch64-unknown-fuchsia aarch64-unknown-linux-gnu aarch64-unknown-linux-musl aarch64-unknown-linux-ohos aarch64-unknown-none aarch64-unknown-none-softfloat aarch64-unknown-uefi arm-linux-androideabi arm-unknown-linux-gnueabi arm-unknown-linux-gnueabihf arm-unknown-linux-musleabi arm-unknown-linux-musleabihf armebv7r-none-eabi armebv7r-none-eabihf armv5te-unknown-linux-gnueabi armv5te-unknown-linux-musleabi armv7-linux-androideabi armv7-unknown-linux-gnueabi armv7-unknown-linux-gnueabihf armv7-unknown-linux-musleabi armv7-unknown-linux-musleabihf armv7-unknown-linux-ohos armv7a-none-eabi armv7r-none-eabi armv7r-none-eabihf i586-pc-windows-msvc i586-unknown-linux-gnu i586-unknown-linux-musl i686-linux-android i686-pc-windows-gnu i686-pc-windows-gnullvm i686-pc-windows-msvc i686-unknown-freebsd i686-unknown-linux-gnu i686-unknown-linux-musl i686-unknown-uefi loongarch64-unknown-linux-gnu loongarch64-unknown-none loongarch64-unknown-none-softfloat nvptx64-nvidia-cuda powerpc-unknown-linux-gnu powerpc64-unknown-linux-gnu powerpc64le-unknown-linux-gnu riscv32i-unknown-none-elf riscv32im-unknown-none-elf riscv32imac-unknown-none-elf riscv32imafc-unknown-none-elf riscv32imc-unknown-none-elf riscv64gc-unknown-linux-gnu riscv64gc-unknown-none-elf riscv64imac-unknown-none-elf s390x-unknown-linux-gnu sparc64-unknown-linux-gnu sparcv9-sun-solaris thumbv6m-none-eabi thumbv7em-none-eabi thumbv7em-none-eabihf (installed) thumbv7m-none-eabi thumbv7neon-linux-androideabi thumbv7neon-unknown-linux-gnueabihf thumbv8m.base-none-eabi thumbv8m.main-none-eabi thumbv8m.main-none-eabihf wasm32-unknown-emscripten wasm32-unknown-unknown wasm32-wasi wasm32-wasip1 wasm32-wasip1-threads x86_64-apple-darwin x86_64-apple-ios x86_64-fortanix-unknown-sgx x86_64-linux-android x86_64-pc-solaris x86_64-pc-windows-gnu x86_64-pc-windows-gnullvm x86_64-pc-windows-msvc x86_64-unknown-freebsd x86_64-unknown-fuchsia x86_64-unknown-illumos x86_64-unknown-linux-gnu (installed) x86_64-unknown-linux-gnux32 x86_64-unknown-linux-musl x86_64-unknown-linux-ohos x86_64-unknown-netbsd x86_64-unknown-none x86_64-unknown-redox x86_64-unknown-uefi
私の
環境で (installed)
となっているものを例に挙げましょう。-
thumbv7em-none-eabihf
これは( - )の です。所謂 を搭載しない 環境を指します。 -
wasm32-unknown-unknown
これはの です。 -
x86_64-pc-windows-msvc
インストールした覚えがないので、多分の です。
通常
では、 で動くプログラムを作ります。よって を示す x86_64-pc-windows-msvc
を使用します。しかしでは、ブラウザーで動くプログラム、即ち のプログラムを作ることになります。従って、 を示す wasm32-unknown-unknown
を使用することになります。は rustup target add
で取得します。rustup target add wasm32-unknown-unknown
-
-
--lockedは無くてもインストールできた
cargo install --locked trunk
から にコンパイルする手順は煩雑で、 正直私もよくわかりません。大雑把には、少なくとも次の手順が必要です。-
から にコンパイルする -
を で参照する -
サーバーを起動する
しかし
を使えば、これらの手順を周く全く無視することができます(私が手順を分かっていないのもそのため)。その代替として、次のコマンドを実行するだけです。 trunk serve
-
チュートリアルを試す
ところが
プログラム
このチュートリアルで作られるプログラムは、ボタンを押すと数字が増えていくものです。yew-app
という名前は紛らわしいため、count
という名前でパッケージを作ります。以降の作業は、このパッケージ内で行われます。
cargo new count
このパッケージに対してCargo.toml
の[dependencies]
下に手動で追記するように指南しています。
[package]
name = "yew-app"
version = "0.1.0"
edition = "2021"
[dependencies]
# 開発バージョンの Yew
yew = { git = "https://github.com/yewstack/yew/", features = ["csr"] }
一方、コマンドで行うこともできます。
cargo add yew --git https://github.com/yewstack/yew/ --features csr
コマンドで追記した場合、Cargo.toml
はこのようになります。
[package]
name = "count"
version = "0.1.0"
edition = "2024"
[dependencies]
yew = { git = "https://github.com/yewstack/yew/", version = "0.21.0", features = ["csr"] }
プログラムはチュートリアルのものをそのまま使います。
use yew::prelude::*;
#[function_component]
fn App() -> Html {
let counter = use_state(|| 0);
let onclick = {
let counter = counter.clone();
move |_| {
let value = *counter + 1;
counter.set(value);
}
};
html! {
<div>
<button {onclick}>{ "+1" }</button>
<p>{ *counter }</p>
</div>
}
}
fn main() {
yew::Renderer::<App>::new().render();
}
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<title>Yew App</title>
</head>
<body></body>
</html>
今、count
フォルダー内はこのようになります。
PS C:\⋯\count> tree /F
Folder PATH listing for volume OS
Volume serial number is ⋯
C:.
│ .gitignore
│ Cargo.lock
│ Cargo.toml
│ index.html
│
└───src
main.rs
実行
先述の通り、コマンド一つで実行します。
trunk serve --open
--open
を付けると、ブラウザーが勝手に起動します。
拡大率
+1
ボタンを押すと、下の数字が増えます。
実行後は後片付けをしておきましょう。
PS C:\⋯\count> cargo clean
Removed 923 files, 516.2MiB total
たったこれだけのアプリケーションですが、
もう一つのチュートリアル
しかし
では真に参考にするべきものはどれかと言うと、こちらです。
チュートリアルにしては文量があるため気圧されますが、
なお、先のチュートリアルで行った手順(導入など)は、爾下への記載を省略します。
セットアップ
パッケージを別に作ります。ここではfull-tutorial
の名前で作ります。
cargo new full-tutorial
最初の静的ページ
静的ページとは、動きの無いページです。先のチュートリアルではボタンを押すことで数値が増えましたが、今回は文字が映るだけです。
Cargo.toml
[package]
name = "full-tutorial"
version = "0.1.0"
edition = "2024"
[dependencies]
# cargo add yew --git https://github.com/yewstack/yew/ --features csr
yew = { git = "https://github.com/yewstack/yew/", version = "0.21.0", features = ["csr"] }
main.rs
プログラムはそのまま使います。
use yew::prelude::*;
#[function_component(App)]
fn app() -> Html {
html! {
<h1>{ "Hello World" }</h1>
}
}
fn main() {
yew::Renderer::<App>::new().render();
}
static
フォルダー
ここで、
これまでは、パッケージとなるフォルダーにそのまま
これでは整理整頓ができません。よくある慣習の一つとして、こうしたファイル(static
フォルダーにまとめておくことがあります。今回はこれを真似しましょう。
Trunk.toml
しかしこれでは、index.html
を探せなくなります。従って、Trunk.toml
というファイルを作ります。
Trunk.toml
に関する説明はこちらに在ります。
本記事ではstatic
フォルダーに加えて幾つか設定を付しました。通常は127.0.0.1:8080
ですが、何となく127.0.0.1:8888
にしました。また、open = true
にすることで、trunk serve
に--open
を付ける必要がなくなります。
[build]
target = "static/index.html" # HTMLファイルの位置
[serve]
addresses = ["127.0.0.1"] # IP address
port = 8888 # Port
open = true # 自動でブラウザーを開く
Trunkの設定
つまり、
[build]
dist = "dist"
[serve]
port = 8080
build:
dist: "dist"
serve:
port: 8080
なお、{}
を使うため、文字数が多くなったり、書き方に揺れが起こったりします。コメントアウトが使えないことも有名です。こうした特徴から、
index.html
最後に、index.html
の内容を記述します。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Yew App</title>
<link data-trunk rel="rust" href="../Cargo.toml">
</head>
<body></body>
</html>
チュートリアルのものとは幾つか差がありますが、意味のある変更は次の一行のみです。こちらの記事を参考にしました。
<link data-trunk rel="rust" href="../Cargo.toml">
これは<link data-trunk rel="css" href="~~.css">
とする必要があります。他の項目についてはドキュメントをご覧ください。
実行
Trunk.toml
に設定を記載した通り、コマンドに--open
を付ける必要がなくなりました。
trunk serve
拡大率
への変換
クラシック
ここからは、元のページがやや不親切になります。本記事ではこれを補足する意で、プログラム等を省略せず記載します。但し長くなるので、アコーディオンに畳んでおきます。
以降のチュートリアルは、全て同一のmain.rs
を上書きしていく形で進めて往きます。新たなパッケージを作る必要はありません。それぞれ別に残したい場合は、パッケージ名の重複に留意ください。
このチュートリアルでは、
main.rs
基本的には、元のプログラムに大きな変更を施すことはありません。しかし、https://via.placeholder.com/640x360.png?text=Video+Player+Placeholder
が使えませんでした。そこで、現在有効なhttps://placehold.jp/640x360.png?text=Video+Player+Placeholder
に変更します。
main.rs
use yew::prelude::*;
#[function_component(App)]
fn app() -> 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>
</div>
<div>
<h3>{ "John Doe: Building and breaking things" }</h3>
<img src="https://placehold.jp/640x360.png?text=Video+Player+Placeholder" alt="video thumbnail" />
</div>
</>
}
}
fn main() {
yew::Renderer::<App>::new().render();
}
実行結果
の構造を使用する
マークアップ内で
現状は次のように、同様の
<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>
そこで本チュートリアルでは、構造体を用いてこれを効率化します。
内部的な処理を効率化するだけであり、実行結果は変わりません。そのため、
main.rs
main.rs
use yew::prelude::*;
#[derive(Clone, PartialEq)]
struct Video {
id: usize,
title: String,
speaker: String,
url: String,
}
#[derive(Properties, PartialEq)]
struct VideosListProps {
videos: Vec<Video>,
}
#[function_component(App)]
fn app() -> Html {
let videos: Vec<Video> = 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(),
},
];
html! {
<>
<h1>{ "RustConf Explorer" }</h1>
<div>
<h3>{"Videos to watch"}</h3>
<VideosList videos={videos} />
</div>
<div>
<h3>{ "John Doe: Building and breaking things" }</h3>
<img src="https://placehold.jp/640x360.png?text=Video+Player+Placeholder" alt="video thumbnail" />
</div>
</>
}
}
#[function_component(VideosList)]
fn videos_list(VideosListProps { videos }: &VideosListProps) -> Html {
videos
.iter()
.map(
|video: &Video| html! {
/* <p key={video.id} onclick={on_video_select}>{format!("{}: {}", video.speaker, video.title)}</p> */
<p key={video.id}>{format!("{}: {} ({})", video.speaker, video.title, video.url)}</p>
}
)
.collect()
}
fn main() {
yew::Renderer::<App>::new().render();
}
実行結果
アプリケーションをインタラクティブにする
本チュートリアルでは、クリックに感応する処理(
main.rs
main.rs
use yew::prelude::*;
#[derive(Clone, PartialEq)]
struct Video {
id: usize,
title: String,
speaker: String,
url: String,
}
#[derive(Properties, PartialEq)]
struct VideosListProps {
videos: Vec<Video>,
on_click: Callback<Video>
}
#[derive(Properties, PartialEq)]
struct VideosDetailsProps {
video: Video,
}
#[function_component(App)]
fn app() -> Html {
let videos: Vec<Video> = 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(),
},
];
let selected_video: UseStateHandle<Option<_>> = use_state(|| None);
let on_video_select: Callback<Video> = {
let selected_video: UseStateHandle<Option<Video>> = selected_video.clone();
Callback::from(
move |video: Video| {
selected_video.set(Some(video))
}
)
};
let details: Option<yew::virtual_dom::VNode> = selected_video.as_ref().map(|video: &Video| html! {<VideoDetails video={video.clone()} />});
html! {
<>
<h1>{ "RustConf Explorer" }</h1>
<div>
<h3>{"Videos to watch"}</h3>
<VideosList videos={videos} on_click={on_video_select.clone()} />
</div>
{ for details }
</>
}
}
#[function_component(VideosList)]
fn videos_list(VideosListProps { videos, on_click }: &VideosListProps) -> Html {
let on_click: Callback<Video> = on_click.clone();
videos
.iter()
.map(
|video: &Video| {
let on_video_select: Callback<_> = {
let on_click: Callback<Video> = on_click.clone();
let video: Video = video.clone();
Callback::from(
move |_| {
on_click.emit(video.clone())
}
)
};
html! {
<p key={video.id} onclick={on_video_select}>{format!("{}: {} ({})", video.speaker, video.title, video.url)}</p>
}
}
)
.collect()
}
#[function_component(VideoDetails)]
fn video_details(VideosDetailsProps { video }: &VideosDetailsProps) -> Html {
html! {
<div>
<h3>{ video.title.clone() }</h3>
<img src="https://placehold.jp/640x360.png?text=Video+Player+Placeholder" alt="video thumbnail" />
</div>
}
}
fn main() {
yew::Renderer::<App>::new().render();
}
実行結果
初期状態
プレースホルダーが表示されず、一見して何か間違えたように見えますが、正しい結果です。ここでJohn Doe: Building and breaking things (https://youtu.be/PsaFVLr8t4E)
などの行をクリックすると、対応するタイトルで表示されます。
各行をクリックした結果
Building and breaking things
をクリックした場合
The development process
をクリックした場合
The Web 7.0
をクリックした場合
Mouseless development
をクリックした場合
の使用)
データの取得(外部
最後に、構造体をプログラム中に定義するのではなく、インターネットから取得します。要するに
取得できる情報は変わらないため、実行結果も変わりません。
Cargo.toml
コマンドはコメントに併記しています。
Cargo.toml
[package]
name = "full-tutorial"
version = "0.1.0"
edition = "2024"
[dependencies]
# cargo add gloo-net
gloo-net = "0.6.0"
# cargo add serde --features derive
serde = { version = "1.0.219", features = ["derive"] }
# cargo add wasm-bindgen-futures
wasm-bindgen-futures = "0.4.50"
# cargo add yew --git https://github.com/yewstack/yew/ --features csr
yew = { git = "https://github.com/yewstack/yew/", version = "0.21.0", features = ["csr"] }
Trunk.toml
https://yew.rs/tutorial/data.json
へ接続するにあたり、proxy-backend
を設定する必要があります。
コマンドの場合はtrunk serve --proxy-backend=https://yew.rs/tutorial
とする必要がありますが、これもTrunk.toml
に記載することで省略できます。
Trunk.toml
[build]
target = "static/index.html" # HTMLファイルの位置
[serve]
addresses = ["127.0.0.1"] # IP address
port = 8888 # Port
open = true # 自動でブラウザーを開く
[[proxy]]
backend = "https://yew.rs/tutorial" # Proxy
main.rs
main.rs
use yew::prelude::*;
use serde::Deserialize;
use gloo_net::http::Request;
#[derive(Clone, PartialEq, Deserialize)]
struct Video {
id: usize,
title: String,
speaker: String,
url: String,
}
#[derive(Properties, PartialEq)]
struct VideosListProps {
videos: Vec<Video>,
on_click: Callback<Video>
}
#[derive(Properties, PartialEq)]
struct VideosDetailsProps {
video: Video,
}
#[function_component(App)]
fn app() -> Html {
let videos: UseStateHandle<Vec<Video>> = use_state(|| vec![]);
{
let videos: UseStateHandle<Vec<Video>> = videos.clone();
use_effect_with((), move |_| {
let videos: UseStateHandle<Vec<Video>> = videos.clone();
wasm_bindgen_futures::spawn_local(
async move {
let fetched_videos: Vec<Video> = Request::get("tutorial/data.json").send().await.unwrap().json().await.unwrap();
videos.set(fetched_videos);
}
);
|| ()
});
}
let selected_video: UseStateHandle<Option<_>> = use_state(|| None);
let on_video_select: Callback<Video> = {
let selected_video: UseStateHandle<Option<Video>> = selected_video.clone();
Callback::from(
move |video: Video| {
selected_video.set(Some(video))
}
)
};
let details: Option<yew::virtual_dom::VNode> = selected_video.as_ref().map(|video: &Video| html! {<VideoDetails video={video.clone()} />});
html! {
<>
<h1>{ "RustConf Explorer" }</h1>
<div>
<h3>{"Videos to watch"}</h3>
<VideosList videos={(*videos).clone()} on_click={on_video_select.clone()} />
</div>
{ for details }
</>
}
}
#[function_component(VideosList)]
fn videos_list(VideosListProps { videos, on_click }: &VideosListProps) -> Html {
let on_click: Callback<Video> = on_click.clone();
videos
.iter()
.map(
|video: &Video| {
let on_video_select: Callback<_> = {
let on_click: Callback<Video> = on_click.clone();
let video: Video = video.clone();
Callback::from(
move |_| {
on_click.emit(video.clone())
}
)
};
html! {
<p key={video.id} onclick={on_video_select}>{format!("{}: {} ({})", video.speaker, video.title, video.url)}</p>
}
}
)
.collect()
}
#[function_component(VideoDetails)]
fn video_details(VideosDetailsProps { video }: &VideosDetailsProps) -> Html {
html! {
<div>
<h3>{ video.title.clone() }</h3>
<img src="https://placehold.jp/640x360.png?text=Video+Player+Placeholder" alt="video thumbnail" />
</div>
}
}
fn main() {
yew::Renderer::<App>::new().render();
}
少し改変
変化がないのも味気ないので、変化するように改変しましょう(結局変化せず)。
https://youtu.be/PsaFVLr8t4E
)を取得していますが、これは実在のものです。つまり、サムネイルを取得できます。取得の仕方はこちらを参考にしました。
本来は正規表現でPsaFVLr8t4E
の部分を確実に抽出する必要があります。更にhttps://www.youtube.com/watch?app=desktop&v=PsaFVLr8t4E
・https://m.youtube.com/watch?v=PsaFVLr8t4E
)ため、その抽出にも注意が必要です。
しかしそんなことは面倒である上、https://yew.rs/tutorial/data.json
を見ると分かるように、今回取得されるhttps://youtu.be/PsaFVLr8t4E
のみです。完全に固定ですから、今回はずるい方法を使います。
main.rs
main.rs
use yew::prelude::*;
use serde::Deserialize;
use gloo_net::http::Request;
#[derive(Clone, PartialEq, Deserialize)]
struct Video {
id: usize,
title: String,
speaker: String,
url: String,
}
#[derive(Properties, PartialEq)]
struct VideosListProps {
videos: Vec<Video>,
on_click: Callback<Video>
}
#[derive(Properties, PartialEq)]
struct VideosDetailsProps {
video: Video,
}
#[function_component(App)]
fn app() -> Html {
let videos: UseStateHandle<Vec<Video>> = use_state(|| vec![]);
{
let videos: UseStateHandle<Vec<Video>> = videos.clone();
use_effect_with((), move |_| {
let videos: UseStateHandle<Vec<Video>> = videos.clone();
wasm_bindgen_futures::spawn_local(
async move {
let fetched_videos: Vec<Video> = Request::get("tutorial/data.json").send().await.unwrap().json().await.unwrap();
videos.set(fetched_videos);
}
);
|| ()
});
}
let selected_video: UseStateHandle<Option<_>> = use_state(|| None);
let on_video_select: Callback<Video> = {
let selected_video: UseStateHandle<Option<Video>> = selected_video.clone();
Callback::from(
move |video: Video| {
selected_video.set(Some(video))
}
)
};
let details: Option<yew::virtual_dom::VNode> = selected_video.as_ref().map(|video: &Video| html! {<VideoDetails video={video.clone()} />});
html! {
<>
<h1>{ "RustConf Explorer" }</h1>
<div>
<h3>{"Videos to watch"}</h3>
<VideosList videos={(*videos).clone()} on_click={on_video_select.clone()} />
</div>
{ for details }
</>
}
}
#[function_component(VideosList)]
fn videos_list(VideosListProps { videos, on_click }: &VideosListProps) -> Html {
let on_click: Callback<Video> = on_click.clone();
videos
.iter()
.map(
|video: &Video| {
let on_video_select: Callback<_> = {
let on_click: Callback<Video> = on_click.clone();
let video: Video = video.clone();
Callback::from(
move |_| {
on_click.emit(video.clone())
}
)
};
html! {
<p key={video.id} onclick={on_video_select}>{format!("{}: {} ({})", video.speaker, video.title, video.url)}</p>
}
}
)
.collect()
}
#[function_component(VideoDetails)]
fn video_details(VideosDetailsProps { video }: &VideosDetailsProps) -> Html {
html! {
<div>
<h3>{ video.title.clone() }</h3>
if let Some(posision) = video.url.find("https://youtu.be/") {
<img src={format!("https://img.youtube.com/vi/{}/hqdefault.jpg", &video.url[posision+17..].split(&['&', '?', '#'][..]).next().unwrap())} alt="video thumbnail" />
} else {
<img src="https://placehold.jp/640x360.png?text=Video+Player+Placeholder" alt="video thumbnail" />
}
</div>
}
}
fn main() {
yew::Renderer::<App>::new().render();
}
実行結果
余談
以前から
Discussion
「環境構築」の章で target triple という言葉が使われていますが、これは古い表現で、今は target tuple といいます ( https://github.com/rust-lang/rust/releases/tag/1.84.0 の Compiler の1つめ参照 )