RustのフロントエンドフレームワークYewに入門してみた
作ったもの
↓一応 (ちなみに Mac で矢印は全角 z + k,l,j,h で出せます)
経緯
コンピュータの仕組みを簡単に説明する授業の中で、「メモリ」「レジスタ」といった構成要素を図解するために使われていた
というページが元ネタです (基本的に仕様はこれに準拠しているので、そのあたりは reference として元ページの説明を参照する形で手を抜きました)。このページを見た頃ちょうど Yew に興味を持ち始めていて、- これくらいシンプルなものなら、初めて Rust でフロントエンドを書く練習にちょうどよさそう
- このページはレスポンシブデザインについては考慮されていないので、それを改善する意図で作り直す意味はありそう
と思ったのが作り始めたきっかけです。小規模かつ http リクエストなども扱わないぶん、そこまで実用性のある内容にはなっていないかもしれませんが、まだまだ少ない Yew 開発の一例として、少しでも Yew を触ってみたい人の助けになればと思って記事を書いています。
ディレクトリ構成・開発の流れ
今回はシンプルに
.
├── dist
├── src
│ ├── components
│ ├── utils
│ └── components.rs, utils.rs, main.rs
├── target
└── .gitignore, Cargo.lock, Cargo.toml, index.html
というディレクトリ構成をとりました。開発環境の構築は
がわかりやすく、これらに倣えば問題ないです。- 保存するたびに
target
以下にファイルがどんどん生成される - 同時に
dist
以下に最新のビルド結果 (html, js, wasm の3ファイル) が入ってホットリロードで即反映される - デプロイ時は
dist
をそのまま公開すれば OK
という流れになります (dist というディレクトリ名や index.html の場所は Trunk.toml
で変えられる) 。
JSX ライクな HTML in Rust で書けるし、コンポーネントに渡す props や use_state
・use_effect
などの hooks も、clone
と move
の感覚にさえ慣れれば React とおよそ同じように使えます。
デバッグまわりは、Yew はコンポーネント志向なので Rust 標準の test が使いやすいし、web-sys
の console
モジュールでコンソールデバッグもできるので、今回の開発では特に困った場面はなかったです。
スタイリング
レスポンシブデザインに関わる部分だけ index.html の <style></style>
、固定でいい部分は html の style 属性に直書きしています。今後もしもっと大きいサイトや Web アプリを Yew で作るとなったらスタイリングフレームワークを探すかもしれません。
工夫した点としては、一部コンポーネントで、 style が長くなって読みにくい場合に
use web_sys::MouseEvent;
use yew::{function_component, html, Properties, Callback};
#[derive(Properties, PartialEq)]
pub struct ProcessCallbacks {
pub handle_step: Callback<MouseEvent>,
pub handle_go_through: Callback<MouseEvent>
}
#[function_component(ProcessButtons)]
pub fn process_buttons(prop: &ProcessCallbacks) -> Html {
let button_style = "
width: 42px;
height: 42px;
border-color: white;
border-radius: 21px;
font-size: 15px;
padding: 0;
";
html!{
<span style="margin-right: 4%;">
<button
class="process-buttons" disabled=true
style={button_style} onclick={prop.handle_step.clone()}
>{"step"}</button>
<button
class="process-buttons" disabled=true
style={button_style} onclick={prop.handle_go_through.clone()}
>{"go"}</button>
</span>
}
}
のようにスタイルを &str
変数として切り出して style 属性に渡すという書き方をしています。やろうと思えば
- prop として
&'static str
を渡すことで、そのコンポーネントの style (または style の一部) を親コンポーネントから与える - style 文字列をスタイリング用のモジュールに切り出して
pub const
(あるいは、引数によってパターンを切り替えるスタイルならpub fn 〜() -> &'static str
) とし、複数のコンポーネントで使い回す
といったこともできます。
困った点
-
html!
内の HTML タグ関係はほぼ補完が効かない
-
move
を含むコールバック関数を渡すと (?) クリックしてもラジオボタンの見た目が切り替わらなくなる
use web_sys::{MouseEvent, console::log_1};
use wasm_bindgen::{JsValue};
use yew::{function_component, html};
#[function_component(App)]
fn app() -> Html {
// move しない
let onclick_left = |_:MouseEvent| {
log_1(&JsValue::from("left clicked!"));
};
let onclick_right = |_:MouseEvent| {
log_1(&JsValue::from("right clicked!"));
};
html!(
<>
{"Yew"}
<input
type="radio" name="yew" checked=true
onclick={onclick_left}
/>
<input
type="radio" name="yew"
onclick={onclick_right}
/>
</>
)
}
fn main() {
yew::start_app::<App>();
}
use web_sys::{MouseEvent, console::log_1};
use wasm_bindgen::{JsValue};
use yew::{function_component, html, use_state, Callback};
#[function_component(App)]
fn app() -> Html {
// move する
let count = use_state(|| 0_i8);
let onclick_left = {
let count = count.clone();
Callback::from(move|_:MouseEvent| {
count.set(*count + 1);
log_1(&JsValue::from(*count));
})
};
let onclick_right = {
let count = count.clone();
Callback::from(move|_:MouseEvent| {
count.set(*count - 1);
log_1(&JsValue::from(*count));
})
};
html!(
<>
{"Yew"}
<input
type="radio" name="yew" checked=true
onclick={onclick_left}
/>
<input
type="radio" name="yew"
onclick={onclick_right}
/>
</>
)
}
fn main() {
yew::start_app::<App>();
}
-
Callback!
内で、move
する前にunwrap
やexpect
でElement
を取り出す操作をすると (?) ホワイトアウトする
use web_sys::{MouseEvent, window};
use yew::{function_component, html, use_state, Callback};
#[function_component(App)]
fn app() -> Html {
let count = use_state(|| 0_i8);
let onclick = {
let count = count.clone();
Callback::from(move |_:MouseEvent| {
// 意味はまったくないが Element を取得してみる
window().unwrap() //: Window
.document().unwrap() //: Document
.get_element_by_id("count") //: Option<Element>
.unwrap();
count.set(*count + 1);
})
};
let button_style = "
width: 42px;
height: 42px;
border-radius: 21px;
border-color: white;
padding: 0;
";
html!(
<div style="text-align: center;">
<p>{*count}</p>
<button style={button_style} {onclick}>{"inc"}</button>
</div>
)
}
fn main() {
yew::start_app::<App>();
}
gifのコマ数の問題で、押してないのにインクリメントされてるように見えるところがありますが気にしないでください
use web_sys::{MouseEvent, window};
use yew::{function_component, html, use_state, Callback};
#[function_component(App)]
fn app() -> Html {
let count = use_state(|| 0_i8);
let onclick = {
let count = count.clone();
// 意味はまったくないが Element を取得してみる
window().unwrap() //: Window
.document().unwrap() //: Document
.get_element_by_id("count") //: Option<Element>
.unwrap(); // ← この unwrap があるとホワイトアウトする
Callback::from(move |_:MouseEvent| {
count.set(*count + 1);
})
};
let button_style = "
width: 42px;
height: 42px;
border-radius: 21px;
border-color: white;
padding: 0;
";
html!(
<div style="text-align: center;">
<p id="count">{*count}</p>
<button style={button_style} {onclick}>{"inc"}</button>
</div>
)
}
fn main() {
yew::start_app::<App>();
}
特に2つ目 (ラジオボタン問題) は、今回作ったものにラジオボタンが含まれるため、割とクリティカルな問題でした。詳しくはリポジトリの mode_radio.rs
と main.rs
を見ていただけると分かると思うのですが、多少無理やり突破しています。
感想など
全体としては「意外とできる」という感じで、
のようにブログくらいなら普通に作れそうですが、現状は、同じく React 系列の JS・TS フレームワークたちに比べるとまだまだ微妙なところがあり、フロントエンド開発においてメインで使いたいとは思えません。とはいえ、TS に比べ厳密な型システム、分かりやすい(異論は認める)モジュールシステム、コンポーネント志向と相性のよさそうな標準の test 機構などなど期待できる部分が多々あると感じたので、今後も注目していこうと思います。
Discussion