yewでウェブアプリを作ろう

yew

yewとは、ウェブアプリのフロントエンドを書くためのRustのフレームワークです。 RustをWebAssemblyに変換して、フロントエンドアプリケーションとして出力してくれます。

ここでは、yewでアプリを書いてみたので、引っかかったところなんかをメモしていきます。

コンポーネントの書き方

Reactのようにコンポーネントを定義し、どんどんネストしていくことができます。全部入りのコンポーネントは以下のような感じになります。

全部入りのコンポーネント

  1. テキスト入力できるコンポーネント
  2. 外部から、ラベル、ラベルを表示するかどうか、テキスト入力確定イベントを渡せる
  3. 確定ボタン押されたら、入力したテキストを消す

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: "" });
  }
}