[Rust] YewでWebAssemblyを使ったWebアプリのフロントを作ってみる
Introduction
最近巷で話題のWebAssembly(Wasm)。
ブラウザで動く比較的新しいタイプのコードですが、
Rustなどプログラミング言語のコンパイル対象となり、ウェブ上で実行することができます。
Rust + Wasmについてはyewやseed、percyなど
フレームワークもいろいろでてきいます。
本稿ではWasmでWebアプリケーションのフロントを作成できるRust用フレームワーク、yewを使ってみます。
WebAssembly?
WebAssembly(Wasm)は、Webブラウザなどの環境で効率よく実行できる、
安全&コンパクトなバイナリ形式の低レベルアセンブリ風言語です。
WasmはJavaScriptと並行して動くようになっており、連携させることも可能です。
WasmはすでにW3Cがコア仕様をW3C勧告として公開しています。
HTML、CSS、JavaScriptに続く、Webブラウザで実行できる
プログラムを記述できる第4の言語となっています。
なお、Wasmは人間が直接読んだり記述することを想定していません。
C++やRustなどの言語でコンパイルされ、バイナリ(.wasm拡張子)で出力されるように設計されています。
wat-wasmなどのツールを使えばwasmからwat(WasmのS式ベーステキスト表現)
へ変換することは可能です。
WebAssemblyはまだ新しめの技術であり、事例はまだ多くありませんが、
Web/serverless/暗号系/科学技術計算/ゲーム系などで採用が進むと予想されているみたいです。
Yew?
YewはWebAssemblyをつかってマルチスレッドなWebアプリのフロントを開発できるフレームワークです。
Rustを使って開発できる、Reactのようなコンポーネントベースのフレームワークです。
JavaScriptとの互換があったり既存のjsモジュールと統合したりすることが可能です。
他にも仮想DOMを使った効率のいいレンダリングが可能だったり、
JSXみたいなHTMLマクロが使えたりします。
ドキュメントも用意されているので、
とりあえず動かすくらいは簡単にできます。
Environment
以下の環境で試しました。 rustc/cargoはすでに実行可能な前提です。
- OS : MacOS 10.15.7
- Rust: 1.56.1
Setup
TrunkはRust用WASM Webアプリのbundlerです。
HTMLファイルを介してWASMやjsファイルやアセットをbuild&bundleしてくれます。
cargoでインストールしておきましょう。
% cargo install --locked trunk
また、Cargo.tomlを編集するのが面倒なので、cargo-editをインストールします。
% cargo install cargo-edit
プロジェクトを作成し、yewやその他必要なcrateをインストールします。
% cargo new --lib yew-app && cd yew-app % cargo add yew % cargo add wasm-bindgen % cargo add wasm-logger
Create Yew App
ではYewを使ってシンプルなTodoアプリをつくってみます。
フィールドに値を入力してボタンを押すとそれが画面に表示されるだけのプログラムです。
HTMLを作成
アクセスする対象のhtmlファイル(yew-app/index.html)を作成します。
wasm.jsをimportして初期化している以外は何もないファイルです。
<!doctype html> <html lang="ja"> <head> <meta content="text/html;charset=utf-8" http-equiv="Content-Type"/> <title>Yew Sample App</title> <script type="module"> import init from "./wasm.js" init() </script> </head> <body></body> </html>
Rustプログラムを作成
yew-app/src/main.rsを下記内容で作成します。
/// main.rs use std::fmt::{Debug}; use yew::{html, Component, ComponentLink, Html, ShouldRender,InputData}; #[derive(Clone, Debug, Default)] pub struct Todo { pub title: String, pub msg: String, } impl Todo { pub fn render(&self) -> Html { html! { <div> <p>{ format!("title: {}, msg : {}", self.title ,self.msg) }</p> </div> } } } struct Model { link: ComponentLink<Self>, todos: Vec<Todo>, title:String, msg:String } #[derive(Debug)] enum Msg { PostTodo, SetTitle(String), SetMsg(String) } impl Component for Model { type Message = Msg; type Properties = (); fn create(_props: Self::Properties, link: ComponentLink<Self>) -> Self { let todos: Vec<Todo> = Vec::new(); Self { link, todos: todos , title:String::new(), msg:String::new()} } fn change(&mut self, _props: Self::Properties) -> ShouldRender { false } fn update(&mut self, msg: Self::Message) -> ShouldRender { log::info!("Update: {:?}", msg); match msg { Msg::PostTodo => { let todo = Todo { title: self.title.clone(), msg: self.msg.clone(), }; self.todos.push(todo); self.title.clear(); self.msg.clear(); } Msg::SetTitle(value) => { log::info!("value: {:?}",value); self.title = value; } Msg::SetMsg(value) => { log::info!("value: {:?}",value); self.msg = value; } } true } fn view(&self) -> Html { html! { <div> <h1>{"Todo App"}</h1> {"title :"} <input type="text" value=self.title.clone() oninput=self.link.callback(|e:InputData| Msg::SetTitle(e.value))/><br/> {"msg :"} <input type="text" value=self.msg.clone() oninput=self.link.callback(|e:InputData| Msg::SetMsg(e.value))/><br/> <button onclick=self.link.callback(|_| Msg::PostTodo)>{ "Post Todos" }</button> <div> { for self.todos.iter().map(Todo::render) } </div> </div> } } } fn main() { wasm_logger::init(wasm_logger::Config::default()); yew::start_app::<Model>(); }
このテンプレートはルートにyewのComponentを設定し、
Modelと呼ばれる要素(入力フィールドとボタンで構成)を作ります。
main関数に記述しているyew::start_app::
htmlのbodyタグをマウントします。
動作確認
では動かしてみましょう。
プロジェクトのルートでtrunk serveとすればビルドしてWebサーバを起動してくれます。
* デフォルトでは8080ポートで起動
% trunk serve Nov 25 10:20:26.583 INFO ? starting build Nov 25 10:20:26.586 INFO spawning asset pipelines Nov 25 10:20:27.119 INFO building yew-app ・・・ Nov 25 10:20:31.008 INFO ? server listening at 0.0.0.0:8080 Nov 25 10:24:21.702 INFO applying new distribution Nov 25 10:24:21.707 INFO ✅ success
ブラウザでアクセスして動かしてみてください。
ログの出力もされています。
コード説明
コード上のポイントを説明します。
ログ
このアプリではwasm_loggerをつかってログを出力しています。
ブラウザコンソールで簡単にログを表示させることができます。
wasm_logger::init(wasm_logger::Config::default()); // Logging log::info!("Some info"); log::error!("Error message");
詳細はここらへんを確認。
モデルとコンポーネント
ModelはComponentに含まれるデータや
ComponentLink(コールバックやメッセージを管理)を持っています。
この構造体に対してComponentを実装します。
ComponentはYewを構成するブロックです。
状態を管理し、Component自身をDOMへレンダリング可能です。
Componentトレイトを実装することで作成できます。
メソッドはいくつかありますが、
今回はComponent作成時のcreate、メッセージ受信時にそれを処理するupdate、
自身のレイアウトを定義するviewを実装しています。
struct Model { link: ComponentLink<Self>, todos: Vec<Todo>, title:String, msg:String } impl Component for Model { type Message = Msg; type Properties = (); fn create(_props: Self::Properties, link: ComponentLink<Self>) -> Self { let todos: Vec<Todo> = Vec::new(); Self { link, todos: todos , title:String::new(), msg:String::new()} } fn change(&mut self, _props: Self::Properties) -> ShouldRender { false } fn update(&mut self, msg: Self::Message) -> ShouldRender { match msg { Msg::PostTodo => { ・・・ } Msg::SetTitle(value) => { ・・・ } Msg::SetMsg(value) => { ・・・ } } true } fn view(&self) -> Html { html! { #HTML要素を記述 } } }
Componentについての詳細はここを確認してください。
html!マクロでHTML記述
Componentのviewメソッドで実行しているhtml!マクロを使うと、
JSXのようにHTMLコードを宣言的に記述できます。
マクロ内では{ }をつかってRustコードを書けます。
・・・ fn view(&self) -> Html { html! { <div> <h1>{"Todo App"}</h1> {"title :"} <input type="text" value=self.title.clone() oninput=self.link.callback(|e:InputData| Msg::SetTitle(e.value))/><br/> {"msg :"} <input type="text" value=self.msg.clone() oninput=self.link.callback(|e:InputData| Msg::SetMsg(e.value))/><br/> <button onclick=self.link.callback(|_| Msg::PostTodo)>{ "Post Todos" }</button> <div> { for self.todos.iter().map(Todo::render) } </div> </div> } } ・・・
要素の描画
ModelではTodo構造体をVecで持っています。
Todo構造体はhtml!マクロを使って自身を描画する
renderメソッドを実装しています。
#[derive(Clone, Debug, Default)] pub struct Todo { pub title: String, pub msg: String, } impl Todo { pub fn render(&self) -> Html { html! { <div> <p>{ format!("title: {}, msg : {}", self.title ,self.msg) }</p> </div> } } }
ComponentのviewでVec
{ for self.todos.iter().map(Todo::render) }
Sumarry
今回はYewフレームワークをつかってRustでWasmプログラムを動かしてみました。
Yewでは既存jsを動かしたりもできるので、それも試してみたいと思います。