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/
にアクセスすれば以下のようになるでしょう。
開発サーバでは、ソースの変更で自動的にブラウザに反映されるため、効率良く開発を行うことができます。
この時点でのプロジェクト構成は以下のようになります。
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/
にアクセスすれば以下のようになり、ボタン押下に応じてカウントアップします。
まとめ
Rust のフロントエンドフレームワークYew を簡単に見てみました。 React ライクに書けるため、親しみやすい感じです。
Rust から作成した WebAssembly でフロントエンドを作成するフレームワークは、Yew の他、Percy や Seed など、色々と出てきており、今後発展していくものと思われます。
少しずつウォッチして行きたいですね。