Rust のフロントエンドフレームワーク Yew の始め方

f:id:Naotsugu:20220224232020p:plain


Yew とは

Rust のフロントエンドフレームワークです。 React のような感じで、フロントエンドアプリケーション全体を、Rust コードからコンパイルした WebAssembly で構築することができます。

Rust で WebAssembly を扱う場合は、wasm-bindgen クレートにより JavaScript とのブリッジを行い、計算負荷が高いなど、特定の部分で WebAssembly を使うといった利用方法が現在の主流となります。 WebAssembly では、DOM や Web API を直接操作できないため、JavaScript を通してブリッジする必要があり、余分な複雑さが追加され、パフォーマンスも低下します。 余分な複雑さやパフォーマンスの低下は将来改善されると思われますが、現時点では WebAssembly を使っているいるからといって、良好なパフォーマンスが得られるものではありません。

WebAssembly で、フロントエンドアプリケーション全体を構築するには、このような課題はありますが、Yew は DOM API 呼び出しを最小化するなどの工夫を盛り込んだ野心的なフレームワークとなっています。


Yew の始め方

Yew は 1.56.0 以上の Rust が必要です。 古いバージョンの Rust は以下でアップデートしておきます。

$ rustup update

$ rustc --version
rustc 1.58.1 (db9d1b20b 2022-01-20)

Rust で WebAssembly のコンパイルを行うには、Rust のクロスコンパイル機能を利用します。

Rust がサポートするターゲットは以下のように確認できます。

$ rustup target list
aarch64-apple-darwin
aarch64-apple-ios
aarch64-apple-ios-sim
...
wasm32-unknown-emscripten
wasm32-unknown-unknown
wasm32-wasi
...

このコンパイルターゲットの中の wasm32-unknown-unknown を利用して、Rust から WebAssembly へのコンパイルを行います。

WebAssembly コンパイルのツールチェーンを以下のように導入します。

$ rustup target add wasm32-unknown-unknown

これで、Rust コードを WebAssembly にコンパイルする準備ができました。


WebAssembly と JavaScript の相互運用を行うため、Yew では、ウェブアプリケーション構築用の包括的なサポートが得られる trunk というビルドツールの利用が推奨されています。

trunk により、WASM、JSスニペット、その他のアセット(画像、CSS、SCS)をソースHTMLファイル経由でビルド&バンドルすることができます。

cargo install コマンドにより trunk バイナリクレートをローカルにインストールします。

$ cargo install trunk

$ trunk --version
trunk 0.14.0

cargo installでインストールされたバイナリは $HOME/.cargo/bin に保存されます(アンインストールは cargo uninstall trunk)。


Hello World

準備が整ったので、Yew アプリケーションを作成していきます。

普通に cargo でプロジェクトを作成します。

$ cargo new yew-app
$ cd yew-app

Cargo.toml に yew の依存を設定します。

[package]
name = "yew-app"
version = "0.1.0"
edition = "2021"

[dependencies]
yew = "0.19"

src/app.rs を作成し、以下のように編集します。

use yew::prelude::*;

#[function_component(App)]
pub fn app() -> Html {
    html! {
        <main>
            <img class="logo" src="https://yew.rs/img/logo.png" alt="Yew logo" />
            <h1>{ "Hello World!" }</h1>
            <span class="subtitle">{ "from Yew with " }<i class="heart" /></span>
        </main>
    }
}

html! マクロの中に JSX のように HTML でUIを定義します。

#[function_component(App)] により、App というコンポーネントを定義したことになります。

src/main.rs を以下のように編集します。

mod app;

use app::App;

fn main() {
    yew::start_app::<App>();
}


プロジェクトルートに index.html を作成します。

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>Yew App</title>
    <link data-trunk rel="sass" href="index.scss" />
  </head>
</html>

同様にプロジェクトルートに index.scss を作成します。

html,
body {
  height: 100%;
  margin: 0;
}

body {
  align-items: center;
  display: flex;
  justify-content: center;
  font-size: 1.5rem;
}

main {
  font-family: sans-serif;
  text-align: center;
}

.logo {
  height: 10em;
}

.heart:after {
  content: "❤️";
  font-size: 1.75em;
}

h1 + .subtitle {
  display: block;
  margin-top: -1em;
}

これで準備は完了です。

次のコマンドを実行して、アプリケーションをビルドし、trunk に組み込みの Webサーバで実行することができます。

$ trunk serve

http://localhost:8080/ にアクセスすれば以下のようになるでしょう。

f:id:Naotsugu:20220224224901p:plain

開発サーバでは、ソースの変更で自動的にブラウザに反映されるため、効率良く開発を行うことができます。

この時点でのプロジェクト構成は以下のようになります。

f:id:Naotsugu:20220224225301p:plain

dist 以下に、wasm、 SCSSから生成されたCSS、HTML として生成されていることがわかります。

参考までに index.html は以下のような内容となっています。

<!DOCTYPE html><html><head>
    <meta charset="utf-8">
    <title>Yew App</title>
    <link rel="stylesheet" href="/index-a5c56e6a7a580e7a.css">

<link rel="preload" href="/index-3d79ba07cecb7e42_bg.wasm" as="fetch" type="application/wasm" crossorigin="">
<link rel="modulepreload" href="/index-3d79ba07cecb7e42.js"></head>
<body><script type="module">import init from '/index-3d79ba07cecb7e42.js';init('/index-3d79ba07cecb7e42_bg.wasm');</script><script>(function () {
    var url = 'ws://' + window.location.host + '/_trunk/ws';
    var poll_interval = 5000;
    var reload_upon_connect = () => {
        window.setTimeout(
            () => {
                // when we successfully reconnect, we'll force a
                // reload (since we presumably lost connection to
                // trunk due to it being killed, so it will have
                // rebuilt on restart)
                var ws = new WebSocket(url);
                ws.onopen = () => window.location.reload();
                ws.onclose = reload_upon_connect;
            },
            poll_interval);
    };

    var ws = new WebSocket(url);
    ws.onmessage = (ev) => {
        const msg = JSON.parse(ev.data);
        if (msg.reload) {
            window.location.reload();
        }
    };
    ws.onclose = reload_upon_connect;
})()
</script></body></html>

なお、リリースビルドは以下のように行います。

trunk build --release


Yew アプリケーション

簡単な動きのあるアプリケーションにしてみましょう。

src/app.rs を以下のように編集します。

use yew::prelude::*;

pub enum Msg {
    AddOne,
}

pub struct App {
    value: i64,
}

impl Component for App {
    type Message = Msg;
    type Properties = ();

    fn create(_ctx: &Context<Self>) -> Self {
        Self {
            value: 0,
        }
    }

    fn update(&mut self, _ctx: &Context<Self>, msg: Self::Message) -> bool {
        match msg {
            Msg::AddOne => {
                self.value += 1;
                true
            }
        }
    }

    fn view(&self, ctx: &Context<Self>) -> Html {
        let link = ctx.link();
        html! {
            <div>
                <button onclick={link.callback(|_| Msg::AddOne)}>{ "+1" }</button>
                <p>{ self.value }</p>
            </div>
        }
    }
}

App という構造体を作成し、Component トレイトを実装します。

view() ではctx.link()によりコンポーネントスコープで link を取得し、ボタン押下で Msg::AddOne メッセージをコールバックにクロージャとして渡しています。

update() ではメッセージに応じて値をインクリメントしています。

http://localhost:8080/ にアクセスすれば以下のようになり、ボタン押下に応じてカウントアップします。

f:id:Naotsugu:20220224230523p:plain


まとめ

Rust のフロントエンドフレームワークYew を簡単に見てみました。 React ライクに書けるため、親しみやすい感じです。

Rust から作成した WebAssembly でフロントエンドを作成するフレームワークは、Yew の他、Percy や Seed など、色々と出てきており、今後発展していくものと思われます。

少しずつウォッチして行きたいですね。