yewでウェブアプリを作ろう
yew
yewとは、ウェブアプリのフロントエンドを書くためのRustのフレームワークです。 RustをWebAssemblyに変換して、フロントエンドアプリケーションとして出力してくれます。
ここでは、yewでアプリを書いてみたので、引っかかったところなんかをメモしていきます。
コンポーネントの書き方
Reactのようにコンポーネントを定義し、どんどんネストしていくことができます。全部入りのコンポーネントは以下のような感じになります。
全部入りのコンポーネント
- テキスト入力できるコンポーネント
- 外部から、ラベル、ラベルを表示するかどうか、テキスト入力確定イベントを渡せる
- 確定ボタン押されたら、入力したテキストを消す
Reactだと以下のような感じで使える想定です。(Reactだとこう書ける、みたいな例を多用します) yewでも似たような感じで使えます。
<MyComponent label="label" show_label={false} on_commit={text => console.log(text)} />
さて、以下が実装です。 Reactでいうところのclass componentだと思っていただければと思います。
use yew::{html, Component, ComponentLink, Properties, Callback}; // state: コンポーネント内部で完結するパラメータのこと。 // props: コンポーネントを呼び出す側が渡すパラメータのこと。 struct MyComponent { // stateやpropsはここに書く。 // propsはstruct Propsを用意しておく。(内部ではわざわざpropsで持つ必要はないが、わかりやすいのでそうしている) text: String, props: Props, // linkは内部でイベントハンドリングを行うために必要。 // イベントハンドリングがいらない(表示だけとか)なら不要。 link: ComponentLink<Self>, } // propsはPropertiesというトレイトを継承する必要がある // <MyComponent prop1="foo".to_string() prop2=true /> みたいな感じで呼べる想定。 #[derive(Properties, Clone, Debug)] struct Props { // requiredにしておくと、呼び出す側で記述がないと怒られるようになる #[props(required)] label: String, // requiredをつけない場合は、Defaultトレイトを実装する必要がある // boolはたぶんいらないけど、MyPropとか独自のstructを用意する場合はMyPropsにDefaultトレイトを実装しましょう show_label: bool, // 外部にイベントを発火できるCallback型。イベントパラメータとしてStringを渡せる。 #[props(required)] on_commit: Callback<String>, } // コンポーネント内で発生するイベントは全て一箇所で処理されるので、そのイベントを定義する // 今回は、text inputとbuttonをおいて、テキスト入力と確定ができるようにする enum Msg { // テキスト入力があったら発生するイベント // Stringをメッセージに含めることができる Change(String) // 確定したら発生するイベント Commit, } // Componentを実装する(Rustではなんて言うのかわからない)ことで、コンポーネントとして利用できるようになる impl Component for MyComponent { // 上で用意したMsgをMessageとして利用できるようにする(おまじない感) type Message = Msg; // 上で用意したPropsをPropertiesとして(略 type Properties = Props; // コンポーネントを新規に生成するときに呼ばれる。 // コンポーネントインスタンスを新規に生成する処理を書きましょう。 fn create(props: Self::Properties, link: ComponentLink<Self>) -> Self { MyComponent { text: "".to_string(), props, link } } // ReactのcomponentDidMountと同じなんじゃないでしょうか。 // 再描画が必要かどうかをboolで返します。 // マウント→通信→通信結果によって再描画とかするときに使うとよいでしょう。 fn mounted(&mut self) -> bool { false } // ReactではshouldComponentUpdateに近い。setStateしたら、これが呼ばれる感じ。 // 違いは自分でstateを保存するとか捨てるとかしないといけないところ。 // 内部で発生したイベントをここで一元管理する。 // イベントに応じて、stateを変更したりする。 fn update(&mut self, msg: Self::Message) -> bool { match msg { // ユーザーがテキストを一文字入力する度に発火する Msg::Change(text) => { // stateのtextを最新の入力テキストで更新する(Reactっぽい) self.text = text; // stateに持ってるテキストでinputエレメントを描画するので、再描画する true } // ユーザーが確定ボタンを押したら、発火する Msg::Commit => { // propsで渡ってきたon_commitをさらに発火する。 self.props.on_commit.emit(self.text.clone()); // 確定ボタン押したら空文字にしたいので、空文字を突っ込む。 self.text = "".to_string(); // inputを再描画したい気持ち true } } } // ReactのcomponentWillReceivePropsに似ている // show_labelとかが変わったら呼ばれるので、内部に保持する処理を書きましょう。 fn change(&mut self, props: Self::Properties) -> bool { self.props = props; // 真面目にやるなら、変わったプロパティに応じてtrue, falseを変えるべきなのでしょうが、めんどいのでやりません。 false } // Reactではrender. // html!というマクロを使うと、マジでJSXみたいに書ける。すごい。 fn view(&self) -> Html { html! { // トップレベルにはひとつのコンポーネントしか書けないので、divとかでくくる <div> // テキスト入力できるところ <input value=self.text // linkというやつでcallbackすることで、例えばoninputなんかのイベントが発火し、ラムダ(Rustではなんて言うんだろう)が呼ばれる // ラムダの戻り値をイベントメッセージにする。下のボタンも同じ。 oninput=self.link.callback(|e: InputData| Msg::Change(e.value))/> <button onclick=self.link.callback(|_| Msg::Commit)> {"Commit"} </button> </div> } } // ReactのcomponentWillUnmount。 // 使ってないからよく知らない。 // WebSocketとか使ってる場合は、ここで切断処理するとか? fn destroy(&mut self) {} }
これをReactで書くと、こんな感じでしょうか。
class MyComponent extends React.Component { constructor(props) { super(props); this.state = { text: "" }; } render() { return ( <div> <input value={this.state.text} onInput={e => this.setState({ text: e.value })} /> <button onClick={() => this.onCommit()}>Commit</button> </div> ); } onCommit() { this.props.onCommit(this.state.text); this.setState({ text: "" }); } }