Rust製Webフロントエンドフレームワーク「Kagura」の紹介

# 概要

RustがWebAssemblyにコンパイルできるので、Rust製のWebフロントエンドフレームワークを開発してみました。この記事はそのフレームワークの紹介です。

参考：https://github.com/SoundRabbit/Kagura
Kaguraを使用して作成したWebページ：https://soundrabbit.github.io/

# 基本構成

KaguraはTEA（The Elm Architecture）をベースとしたコンポーネント指向のWebフロントエンドフレームワークです。

コンポーネントは状態（State）を持つことができ、その状態（State）をメッセージ（Msg）をもとにupdate関数により更新し、その状態（State）をもとにrender関数により描画します。これは以下の図のようなイメージで表すことができます。（ここではKaguraの外の世界をBrowserとしています。）

![アセット 1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/199139/5913738d-e6e2-8e1a-8101-ecbe3c20924c.png)

実際には、直接的に上のような動作を行うことができないので、Kaguraが間に入ることでページの描画・更新を行います。

![アセット 4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/199139/aa330e59-eed4-4020-1429-f4a88aa5d470.png)

たとえば、あるボタンを押すごとにそのボタンのテキストが1ずつ増加するだけのWebアプリを書いてみると以下のようになります。

```rust
extern crate kagura;
extern crate wasm_bindgen;

use kagura::prelude::*;
use wasm_bindgen::prelude::*;

#[wasm_bindgen(start)]
pub fn main() {
    kagura::run(Component::new(init, update, render), "app");
}

type State = u32;

enum Msg {
    CountUp,
}

struct Sub();

fn init() -> (State, Cmd<Msg, Sub>) {
    (0, Cmd::none())
}

fn update(state: &mut State, msg: Msg) -> Cmd<Msg, Sub> {
    match msg {
        Msg::CountUp => {
            *state = *state + 1;
            Cmd::none()
        }
    }
}

fn render(state: &State) -> Html<Msg> {
    Html::button(
        Attributes::new(),
        Events::new().on_click(|_| Msg::CountUp),
        vec![Html::text(state.to_string())],
    )
}

```

`update` には `msg` に基づく `state` の更新方法を記述し、 `render` には `state` をどのように描画するのかを記述しています。また `on_click(|_| Msg::CountUp)` があることで、ボタンがクリックされたときにKaguraは `|_| Msg::CountUp` を実行し、 `Msg::CountUp` というメッセージを取得します。そして、Kaguraはこのメッセージを `State` への可変参照と共に `update` に渡します。

実際にKaguraが関数を実行する順序は以下のようになります。

1. Kaguraは `init` 関数を実行することで初期**状態**を取得します。今回の場合、初期**状態**は0になります。
1. Kaguraは `render` 関数に初期**状態**を渡します。
1. `render` 関数は渡された状態に基づいて描画する内容を返します。
1. Kaguraは `render` 関数の戻り値に基づき描画を行います。
1. 描画したボタンがクリックされると、Kaguraは `on_click` の引数として渡されたクロージャを実行し、その戻り値として**メッセージ**を取得します。
1. Kaguraは取得した**メッセージ**をその時の**状態**への可変参照と共に `update` 関数に渡します。
1. `update` 関数は**メッセージ**に基づき**状態**を更新します。

# コンポーネントについて

コンポーネント（ `Component<Msg, State, Sub>` ）は `init` ・ `update` ・ `render` により構成されます。コンポーネントは `Component::new(init, update, render)` により作成できます。

`init`は引数を取らず `(State, Cmd<Msg, Sub>)` を返す関数で、 状態の初期化を行います。

`update` は状態への可変参照（`&mut State`）とメッセージ（`Msg`）を引数に取り`Cmd<Msg, Sub>` を返す関数で、状態の更新を行います。

`render` は状態への参照（`&State`）を引数に取り`Html<Msg>`を返す関数で、状態の描画を行います。

## コンポーネントを描画する

コンポーネントは `Html::component(some_component)` により `Html<Msg>` として描画できます。

## コンポーネント間でメッセージをやり取りする

コンポーネントを作成する関数を作成し、その引数として子コンポーネントに何らかの値を渡すことができます。

`Cmd::Sub(some_message)` を `update` の戻り値とすることで、親コンポーネントに `Sub` 型のメッセージを送ることができます。親コンポーネントでは、 `child_component.subscribe` によりそのメッセージを受け取り、親コンポーネント自身のメッセージにマップすることができます。

コンポーネントの描画とコンポーネント間のやり取りをする例を以下に記します。

```rust
extern crate kagura;
extern crate wasm_bindgen;

use kagura::prelude::*;
use wasm_bindgen::prelude::*;

#[wasm_bindgen(start)]
pub fn main() {
    kagura::run(Component::new(init, update, render), "app");
}

type State = bool;

enum Msg {
    Over32ed,
}

struct Sub();

fn init() -> (State, Cmd<Msg, Sub>) {
    (true, Cmd::none())
}

fn update(state: &mut State, msg: Msg) -> Cmd<Msg, Sub> {
    match msg {
        Msg::Over32ed => {
            *state = false;
            Cmd::none()
        }
    }
}

fn render(state: &State) -> Html<Msg> {
    if *state {
        Html::div(
            Attributes::new(),
            Events::new(),
            vec![Html::component(some_component::new(16).subscribe(
                |sub| match sub {
                    some_component::Sub::Over32 => Msg::Over32ed,
                },
            ))],
        )
    } else {
        Html::text("over 32 !")
    }
}

mod some_component {
    use kagura::prelude::*;

    pub fn new(initial_number: u32) -> Component<Msg, State, Sub> {
        Component::new(|| (initial_number, Cmd::none()), update, render)
    }

    pub type State = u32;

    pub enum Msg {
        CountUp,
    }

    pub enum Sub {
        Over32,
    }

    fn update(state: &mut State, msg: Msg) -> Cmd<Msg, Sub> {
        match msg {
            Msg::CountUp => {
                *state = *state + 1;
                if (*state > 32) {
                    Cmd::sub(Sub::Over32)
                } else {
                    Cmd::none()
                }
            }
        }
    }

    fn render(state: &State) -> Html<Msg> {
        Html::button(
            Attributes::new(),
            Events::new().on_click(|_| Msg::CountUp),
            vec![Html::text(state.to_string())],
        )
    }
}
```

# Cmd::taskで非同期処理

`Cmd::task` は `FnOnce(Resolver<Msg>)`を引数に取ります。`Resolver<Msg>` の型は `Box<FnOnce(Msg)>` です。JavaScriptのPromiseだとresolveとrejectを使っていたものがresolveだけになったと考えればわかりやすいと思います。

![アセット 5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/199139/4cbb3ce0-98b7-5f04-9928-4b3b028fc217.png)

実際に使っている例を見た方が分かりやすいと思うので、まず以下に例を記します。

```rust
extern crate kagura;
extern crate wasm_bindgen;
extern crate web_sys;

use kagura::prelude::*;
use wasm_bindgen::prelude::*;
use wasm_bindgen::JsCast;

#[wasm_bindgen(start)]
pub fn main() {
    kagura::run(Component::new(init, update, render), "app");
}

type State = String;

enum Msg {
    ChangeMessage(String),
}

struct Sub();

fn init() -> (State, Cmd<Msg, Sub>) {
    (
        "Hello".to_string(),
        Cmd::task(|resolver| {
            let a = Closure::once(Box::new(|| {
                resolver(Msg::ChangeMessage("World".to_string()));
            }));
            web_sys::window()
                .unwrap()
                .set_timeout_with_callback_and_timeout_and_arguments_0(
                    a.as_ref().unchecked_ref(),
                    3000,
                );
            a.forget();
        }),
    )
}

fn update(state: &mut State, msg: Msg) -> Cmd<Msg, Sub> {
    match msg {
        Msg::ChangeMessage(message) => {
            *state = message;
            Cmd::none()
        }
    }
}

fn render(state: &State) -> Html<Msg> {
    Html::text(state)
}
```

3000ms（3秒）後に「Hello」が「World」に変わります。`resolver(Msg::ChangeMessage("World".to_string()));` が実行されたタイミングで`update`が走って再描画が行われます。`Cmd<Msg,Sub>` は `update` の戻り値だけではなく、`init` の戻り値にも設定できるので、このようにコンポーネントが作成されたタイミングで `Cmd::task` を実行することができます。

# `Component::batch` で外部からイベントを発生させる

`Cmd::task` を使えば1回のリクエストにつき1回の応答が来るHttpリクエストなどもできます。しかし、Server-Sent Eventsや `setinterval` などのようなことには対応できません。なぜなら `resolver` は1度しか呼び出せないからです。そこでKaguraにはbatchという機能があります。batchは `FnOnce(FnMut(Msg))` を引数に取ります。

![アセット 6.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/199139/c7873fa7-c025-01e6-d048-16dc549f667a.png)

以下に `setInterval` を使用する例を記します。

```rust
extern crate kagura;
extern crate wasm_bindgen;
extern crate web_sys;

use kagura::prelude::*;
use wasm_bindgen::prelude::*;
use wasm_bindgen::JsCast;

#[wasm_bindgen(start)]
pub fn main() {
    kagura::run(
        Component::new(init, update, render).batch(|mut handler| {
            let a = Closure::wrap(Box::new(move || {
                handler(Msg::CountUp);
            }) as Box<dyn FnMut()>);
            web_sys::window()
                .unwrap()
                .set_interval_with_callback_and_timeout_and_arguments_0(
                    a.as_ref().unchecked_ref(),
                    500,
                );
            a.forget();
        }),
        "app",
    );
}

type State = u32;

enum Msg {
    CountUp,
}

struct Sub();

fn init() -> (State, Cmd<Msg, Sub>) {
    (0, Cmd::none())
}

fn update(state: &mut State, msg: Msg) -> Cmd<Msg, Sub> {
    match msg {
        Msg::CountUp => {
            *state = *state + 1;
            Cmd::none()
        }
    }
}

fn render(state: &State) -> Html<Msg> {
    Html::text(state.to_string())
}
```

500ms おきに1ずつ値が増加していきます。

# 今後について

開発開始から約半年がたち、一応ではありますがWebSocketやXMLHttpRequestにも対応できました。ただ、Cmd::taskとComponent::batchの実装については個人的に気に入らない点があるので今後修正の必要があるようにも思えます。今後はWebAudioAPIへの対応を目指して開発をしていく予定です。