Zenn
🔜

【Rust】Yew公式サイトの日本語対応が進むらしい+チュートリアルを試す

2025/03/17に公開
1
11

YewYewNextNextバージョンに変わろうとしている

https://yew.rs/ja/

YewYewRustRustに於ける「WebWebアプリケーションフレームワーク」として知られるものです。WebWebの中でも画面を作ることに特化(?)したもので、かつJavaScriptJavaScriptではなくWebAssemblyWebAssemblyを使います。

そんなYewYewですが、ドキュメントだけでなく公式サイトが存在します。しかしこのサイト、言語として日本語を選ぶことができるにもかかわらず、実際には殆ど対応していませんでした。

トップ画面
日本語トップ画面(2025/03/16現在)
https://yew.rs/ja/

Getting Started
0.210.21バージョン日本語ドキュメント(2025/03/16現在)
https://yew.rs/ja/docs/getting-started/introduction
日本語⋯?

しかし最近になり、現在最新の「0.210.21バージョン」から進んで「NextNextバージョン」が現れました。この「NextNextバージョン」は未だリリース前のため試すことはできないようですが、驚くことに日本語のページが日本語になっていました(?)。

始めに
NextNextバージョン日本語ドキュメント(2025/03/16現在)
https://yew.rs/ja/docs/next/getting-started/introduction

どうやら全てのページが日本語になっているわけではないようですが、これまで日本語を探す方が大変だったのに対して、今度は英語のページを探す方が難しくなりました。それだけのことですが、なんだか嬉しく感じます。

環境構築

https://yew.rs/ja/docs/next/getting-started/introduction

YewYewを使用するには、次の手順が必要です。

  1. RustRustを用意する
    プログラミング言語であるRustRustを用意します。但し、環境によって導入手順が異なるため、本記事は具体的な手順を省略します。順路はどうあれ、最終的に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.
    
  2. WebAssemblyWebAssemblyの「targettarget tripletriple」を取得する

    targettarget tripletripleという語には聞き馴染がないかもしれません。私はWritingWriting anan OSOS inin RustRustの記事で初めて知りました。百聞は一見に如かず、rustupで一覧を見ることができます。

    rustup target listの様子
    1. WindowsWindowsで実行した場合
    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
    
    1. UbuntuUbuntu(GoogleGoogle CloudCloud ShellShell)で実行した場合
    ⋯@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
    

    私のWindowsWindows環境で(installed)となっているものを例に挙げましょう。

    • thumbv7em-none-eabihf
      これはWioWio TerminalTerminal(ARMARM CortexCortex-M4FM4F)のtargettarget tripletripleです。所謂OSOSを搭載しないbarebare metalmetal環境を指します。
    • wasm32-unknown-unknown
      これはWebAssemblyWebAssemblytargettarget tripletripleです。
    • x86_64-pc-windows-msvc
      インストールした覚えがないので、多分WindowsWindowstargettarget tripletripleです。

    通常WindowsWindowsでは、WindowsWindowsで動くプログラムを作ります。よってWindowsWindowsを示すx86_64-pc-windows-msvcを使用します。しかしYewYewでは、ブラウザーで動くプログラム、即ちWebAssemblyWebAssemblyのプログラムを作ることになります。従って、WebAssemblyWebAssemblyを示すwasm32-unknown-unknownを使用することになります。

    targettarget tripletriplerustup target addで取得します。

    rustup target add wasm32-unknown-unknown
    
  3. TrunkTrunkを用意する
    TrunkTrunkは、麤略に言えばRustRustWebAssemblyWebAssemblyを使いやすくするものです。

    --lockedは無くてもインストールできた
    cargo install --locked trunk
    

    RustRustからWebAssemblyWebAssemblyにコンパイルする手順は煩雑で、正直私もよくわかりません。大雑把には、少なくとも次の手順が必要です。

    1. RustRustからWebAssemblyWebAssemblyにコンパイルする
    2. WebAssemblyWebAssemblyJavaScriptJavaScriptで参照する
    3. WebWebサーバーを起動する

    しかしTrunkTrunkを使えば、これらの手順を周く全く無視することができます(私が手順を分かっていないのもそのため)。その代替として、次のコマンドを実行するだけです。

    trunk serve
    

チュートリアルを試す

YewYewのチュートリアルには或る問題がありました。

0.210.21バージョンのBuildBuild aa samplesample appappにはサンプルプログラムとその手順が載っていますが、ここで使用されているYewYewのバージョンは0.170.17であり、0.210.21では動きません。

ところがNextNextバージョンになったことで、この問題も解消されました。こちらを試しましょう。

https://yew.rs/ja/docs/next/getting-started/build-a-sample-app#プロジェクトを-yew-web-アプリケーションに設定する

プログラム

このチュートリアルで作られるプログラムは、ボタンを押すと数字が増えていくものです。yew-appという名前は紛らわしいため、countという名前でパッケージを作ります。以降の作業は、このパッケージ内で行われます。

パッケージを作る
cargo new count

このパッケージに対してYewYewを導入します。記事上では、Cargo.toml[dependencies]下に手動で追記するように指南しています。

Cargo.tomlの引用
[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はこのようになります。

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"] }

プログラムはチュートリアルのものをそのまま使います。

main.rs
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();
}

HTMLHTMLファイルもそのまま使います。

index.html
<!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を付けると、ブラウザーが勝手に起動します。

カウンターアプリ
拡大率500%500\%

+1ボタンを押すと、下の数字が増えます。

実行後は後片付けをしておきましょう。RustRust一般にも言える事ですが、YewYewは容量が膨れやすいため、使わないのであれば消すことを勧めます。

PS C:\\count> cargo clean
     Removed 923 files, 516.2MiB total

たったこれだけのアプリケーションですが、500MB500MB程の容量になっていたようです。更に複雑なアプリケーションになると、平気でGBGB単位のものになります。

もう一つのチュートリアル

YewYewのチュートリアルを巡るもう一つの問題は、どのチュートリアルを学べばよいのか選択肢が多い点でした。実際、先のチュートリアルだけでは、大したことが分かりません。

しかし0.210.21バージョンの時点では、ExamplesExamplesと称してほぼ全て0.170.17バージョンでのプログラムを紹介していました。NextNextバージョンでは解消されたようですが、何も紹介しないという方向に解消されています。

では真に参考にするべきものはどれかと言うと、こちらです。

https://yew.rs/ja/docs/next/tutorial

チュートリアルにしては文量があるため気圧されますが、YewYewを使うならばこれに習うのが一番早いでしょう。

なお、先のチュートリアルで行った手順(導入など)は、爾下への記載を省略します。

セットアップ

パッケージを別に作ります。ここではfull-tutorialの名前で作ります。

cargo new full-tutorial

最初の静的ページ

https://yew.rs/ja/docs/next/tutorial#最初の静的ページ

静的ページとは、動きの無いページです。先のチュートリアルではボタンを押すことで数値が増えましたが、今回は文字が映るだけです。

Cargo.toml

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

プログラムはそのまま使います。

main.rs
use yew::prelude::*;

#[function_component(App)]
fn app() -> Html {
    html! {
        <h1>{ "Hello World" }</h1>
    }
}

fn main() {
    yew::Renderer::<App>::new().render();
}

staticフォルダー

ここで、HTMLHTMLファイルに関して少し工夫を加えてみましょう。

これまでは、パッケージとなるフォルダーにそのままHTMLHTMLファイルを配置していました。

これでは整理整頓ができません。よくある慣習の一つとして、こうしたファイル(HTMLHTMLCSSCSSなど)をstaticフォルダーにまとめておくことがあります。今回はこれを真似しましょう。

Trunk.toml

しかしこれでは、TrunkTrunkindex.htmlを探せなくなります。従って、Trunk.tomlというファイルを作ります。

Trunk.tomlに関する説明はこちらに在ります。

https://trunkrs.dev/configuration/

本記事ではstaticフォルダーに加えて幾つか設定を付しました。通常は127.0.0.1:8080ですが、何となく127.0.0.1:8888にしました。また、open = trueにすることで、trunk serve--openを付ける必要がなくなります。

Trunk.toml
[build]
target      = "static/index.html"   # HTMLファイルの位置

[serve]
addresses   = ["127.0.0.1"]         # IP address
port        = 8888                  # Port
open        = true                  # 自動でブラウザーを開く

Trunkの設定

TrunkTrunkの設定に関して、このような記事もあります。

https://trunkrs.dev/guide/configuration/index.html#configuration-files

つまり、TOMLTOMLではなくYAMLYAMLJSONJSONでも書けるようです。YAMLYAMLPythonPythonのように行頭下げ(インデント)を使います。

TOML
[build]
dist = "dist"
[serve]
port = 8080
YAML
build:
  dist: "dist"
serve:
  port: 8080

なお、JSONJSONの例は記載がありません。詮ずるに、JSONJSONは推奨しないのでしょう。構造を表すために{}を使うため、文字数が多くなったり、書き方に揺れが起こったりします。コメントアウトが使えないことも有名です。こうした特徴から、JSONJSONからの脱却を薦める声も上がっています。

https://qiita.com/takuya77088/items/935ca090323049280e64

index.html

最後に、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>

チュートリアルのものとは幾つか差がありますが、意味のある変更は次の一行のみです。こちらの記事を参考にしました。

https://stackoverflow.com/questions/76740053/in-yew-how-is-it-possible-to-locate-index-html-in-a-static-directory

<link data-trunk rel="rust" href="../Cargo.toml">

これはTrunkTrunk特有の記述です。今回のような場面に加え、CSSCSSファイルを指定する際にも<link data-trunk rel="css" href="~~.css">とする必要があります。他の項目についてはドキュメントをご覧ください。

https://trunkrs.dev/guide/assets/index.html

実行

Trunk.tomlに設定を記載した通り、コマンドに--openを付ける必要がなくなりました。

実行コマンド
trunk serve

静的ページ
拡大率500%500\%

クラシックHTMLHTMLへの変換

https://yew.rs/ja/docs/next/tutorial#クラシック-html-への変換

ここからは、元のページがやや不親切になります。本記事ではこれを補足する意で、プログラム等を省略せず記載します。但し長くなるので、アコーディオンに畳んでおきます。

以降のチュートリアルは、全て同一のmain.rsを上書きしていく形で進めて往きます。新たなパッケージを作る必要はありません。それぞれ別に残したい場合は、パッケージ名の重複に留意ください。

このチュートリアルでは、HTMLHTMLを少し複雑なものに変更します。

main.rs

基本的には、元のプログラムに大きな変更を施すことはありません。しかし、https://via.placeholder.com/640x360.png?text=Video+Player+Placeholderが使えませんでした。そこで、現在有効なhttps://placehold.jp/640x360.png?text=Video+Player+Placeholderに変更します。

main.rs
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();
}

実行結果

HTMLへの変換

マークアップ内でRustRustの構造を使用する

https://yew.rs/ja/docs/next/tutorial#マークアップ内でrustの構造を使用する

現状は次のように、同様のHTMLHTMLを一つ一つ記述しています。

<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>

そこで本チュートリアルでは、構造体を用いてこれを効率化します。

内部的な処理を効率化するだけであり、実行結果は変わりません。そのため、URLURLの表示を加えて変化を作っています。

main.rs

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();
}

実行結果

Rust構造

アプリケーションをインタラクティブにする

https://yew.rs/ja/docs/next/tutorial#アプリケーションをインタラクティブにする

本チュートリアルでは、クリックに感応する処理(onon clickclick listenerlistener)を実装します。これにより、クリック操作で表示が変化する動的なサイトとなります。

main.rs

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
Building and breaking thingsをクリックした場合

The development process
The development processをクリックした場合

The Web 7.0
The Web 7.0をクリックした場合

Mouseless development
Mouseless developmentをクリックした場合

データの取得(外部RESTREST APIAPIの使用)

https://yew.rs/ja/docs/next/tutorial#データの取得外部-rest-api-の使用

最後に、構造体をプログラム中に定義するのではなく、インターネットから取得します。要するにHTTPHTTPによる通信を行います。

取得できる情報は変わらないため、実行結果も変わりません。

Cargo.toml

コマンドはコメントに併記しています。

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
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
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();
}

少し改変

変化がないのも味気ないので、変化するように改変しましょう(結局変化せず)。

YouTubeYouTubeURLURL(https://youtu.be/PsaFVLr8t4E)を取得していますが、これは実在のものです。つまり、サムネイルを取得できます。取得の仕方はこちらを参考にしました。

https://qiita.com/uebo_tongull/items/b518b1831f6041960db2

本来は正規表現でPsaFVLr8t4Eの部分を確実に抽出する必要があります。更にYouTubeYouTubeURLURLには複数の型式がある(例:https://www.youtube.com/watch?app=desktop&v=PsaFVLr8t4Ehttps://m.youtube.com/watch?v=PsaFVLr8t4E)ため、その抽出にも注意が必要です。

しかしそんなことは面倒である上、https://yew.rs/tutorial/data.jsonを見ると分かるように、今回取得されるURLURLhttps://youtu.be/PsaFVLr8t4Eのみです。完全に固定ですから、今回はずるい方法を使います。1717文字目以降が取得できればよいので、スライスしています。

main.rs

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();
}

実行結果

サムネイル

余談

以前からYewYewは使用していましたが、実は後者のチュートリアルを試したことがありませんでした。必要性が分からず、それよりもJavaScriptJavaScriptの代替法を探すことに躍起になっていたようです。しかし今回、HTTPHTTP通信の方法が分からず、調べた末このチュートリアルに帰着しました。そして日本語化を知ったのもその最中です。これで漸く通信がうまくいかない原因が判るかもしれません。

11

Discussion

ログインするとコメントできます