【Rust】yewでWebアプリケーションを作ってみた話。

# はじめに
今回RustでWebアプリケーションを制作するにあたり、色々と戸惑ったり遠回りをしたことがあったので記事にしました。
この記事ではひとつのプロジェクトを通して、

* yewってなんぞや？wasmってなんぞや？
* RustでWebアプリケーションを開発するのってメリットあるの？
* yewでHTMLの要素の状態を得るにはどうするん？

ということなどについて言及していきます。

また事前知識として、以下のようなものがあると理解がしやすいかなと思います。
もし本記事で分からないところがあれば、参考文献として見てみてください。

<ul>
    <li>
        <dl>
            <dt>Rust</dt>
            <dd><a href="https://doc.rust-jp.rs/book-ja/title-page.html" target="_blank">The Rust Programming Language</a><br>
                10. ジェネリック型、トレイト、ライフタイム くらいまで</dd>
        </dl>
    </li>
    <li>
        <dl>
            <dt>HTML</dt>
            <dd><a href="https://creatorquest.jp/lessons/html/" target="_blank">HTML入門 - CreatorQuest</a><br>
                Chapter17 ラベルと部品を関連付けよう くらいまで</dd>
        </dl>
    </li>
    <li>
        <dl>
            <dt>CSS</dt>
            <dd><a href="https://creatorquest.jp/lessons/css/" target="_blank">CSS入門 - CreatorQuest</a><br>
                Chapter02 基本的なセレクタ くらいまで</dd>
        </dl>
    </li>
</ul>

# yewとは
yewとは以下のような特徴をもつRustのWebフレームワークです。

* Elmアーキテクチャを採用
* マクロにより、HTMLタグを(ほとんどそのまま)埋め込める
* 仮想DOMを搭載
* 最終的にWebAssemblyにコンパイルされるため、めちゃんこ爆速🚀(になる予定)

まとめると、yewというのはRust版Reactのようなものなんだなと思って間違いないと思います。
ちなみに爆速🚀(になる予定)としたのは、以下の[yew公式](https://yew.rs/ja/)からの引用が理由です。

> WebAssembly(Wasm)はRustがコンパイル可能な軽量で低レベルな言語です。WebAssemblyはブラウザでネイティブ並の速度で動き、JavaScriptと互換性があり、そして全ての主要なブラウザでサポートされています。(省略)
> 
> 気をつけなければいけないこととして、Wasmは(まだ)Webアプリのパフォーマンスを改善する銀の弾丸ではありません。**現時点では、DOMのAPIをWebAssemblyから使うのはまだJavaScriptから直接呼ぶよりも遅い**のです。これは WebAssembly Interface Types が解決しようとしている今のところの課題です。詳しい情報についてはMozillaの提案が書いてある excellent article をご確認ください。

とはいえ、簡単な計算ならば本当にネイティブと同様のスピードが提供されるので、速度面での心配はいらないでしょう。

:::note warn
2022/01/15現在、yewは開発が活発に進められており、今後のアップデートで一部の仕様が変更される場合があります。
yew以外にも言えることですが、参考にする情報がいつのものなのかについては、十分に注意する必要があると思います。
二敗

ちなみに、今回使用したバージョンは現在最新の0.19.3です。
:::

# 導入
yewもといwasmを動作させるためには、[Trunk](https://trunkrs.dev/#getting-started)というバンドラーをインストールする必要があります。
コマンドラインで以下のコマンドを実行しましょう。

```powershell
# wasmをRustのコンパイルターゲットに追加する.
rustup target add wasm32-unknown-unknown

# cargoによるTrunkのインストール.
cargo install trunk
```

これで準備は完了です。

プロジェクトのビルドには以下のコマンドを、

```powershell
trunk build
```

プロジェクトのビルドと実行には以下のコマンドをプロジェクトのルートフォルダで実行しましょう。

```powershell
trunk serve --open
```

`trunk serve --open`はホットリロードに対応しているので、プロジェクトフォルダ内で変更があれば即座にビルドし、ブラウザのページをリロードしてくれます。
めちゃ便利ですね。

# アプリケーションの設計
開発環境が整ったので、アプリケーションの設計を考えていきます。
とはいっても、フレームワークはもう決まっているので、内部でどんなことをしたいのかを考えましょう。
今回はyewがどんなものか試してみるだけなので、**第n項目の素数を求める**くらいがちょうどいいと思いました。
Rustには素数算出用の外部クレートがあるので、[エラトステネスの篩](https://ja.wikipedia.org/wiki/エラトステネスの篩)だとか[ミラー・ラビン素数判定法](https://ja.wikipedia.org/wiki/ミラー–ラビン素数判定法)だとかのアルゴリズムを理解する必要はありません。

# 実装
設計が定まったので、実際にコーディングしていきます。
そしてできたものがこちら。

https://prime-lime.web.app

https://github.com/dino3616/prime-lime

## ソースコード

<details><summary>src/main.rs</summary><div>

```Rust:src/main.rs
use yew::{Component,Context,html,Html,InputEvent,TargetCast};
use web_sys::HtmlInputElement;
use js_sys::Date;
use primal::StreamingSieve;

#[derive(Default)]
struct Prime{
    n: usize,
    prime: usize,
    run_time: f64,
}

enum Message{
    Inputted(String),
    Run,
}

impl Component for Prime{
    type Message=Message;
    type Properties=();

    fn create(_ctx: &Context<Self>)->Self{
        Self::default()
    }

    fn update(&mut self,_ctx: &Context<Self>,msg: Self::Message)->bool{
        match msg{
            Message::Inputted(n)=>{
                self.n=match n.trim().parse(){
                    Ok(ok)=>ok,
                    Err(_)=>usize::default(),
                };
                false
            }
            Message::Run=>{
                if self.n>0{
                    let start=Date::now();
                    self.prime=StreamingSieve::nth_prime(self.n);
                    self.run_time=Date::now()-start;
                }

                true
            }
        }
    }

    fn view(&self,ctx: &Context<Self>)->Html{
        html!{
            <div>
                <div>
                    <h1>{"Let's Witness the Nth Prime Number!"}</h1>
                </div>
                <div class="text_input_container">
                    <input type="text" id="nth_input" oninput={ctx.link().callback(|e: InputEvent| Message::Inputted(e.target_unchecked_into::<HtmlInputElement>().value()))} />
                    <label for="nth_input">{"write the N here!"}</label>
                </div>
                <div id="button_container">
                    <button class="Run" onclick={ctx.link().callback(|_| Message::Run)}>
                        <span class="circle" >
                            <span class="icon arrow"></span>
                        </span>
                        <span class="button_text">{"Run"}</span>
                    </button>
                </div>
                <div>
                    <div class="prime">
                        if (self.prime)!=usize::default(){
                            {self.prime}
                        }
                    </div>
                </div>
                <div>
                    <p class="run_time">{format!("run time: {} msec",self.run_time)}</p>
                </div>
            </div>
        }
    }
}

fn main(){
    yew::start_app::<Prime>();
}
```

</div></details>

<details><summary>Cargo.toml</summary><div>

```toml:Cargo.toml
[package]
name = "prime"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
yew = "0.19.3"
web-sys = "0.3.55"
js-sys = "0.3.55"
primal = "0.3.0"
```

</div></details>

<details><summary>index.html</summary><div>

```html:index.html
<!doctype html>

<html lang="ja">
<head>
    <meta charset="utf-8" />
    <title>Prime-Lime - 第n番目の素数を求める</title>
    <link data-trunk rel="icon" type="image/png" href="icon.png">
    <link data-trunk rel="sass" href="style.scss" />
</head>
<body>
</body>
</html>
```

</div></details>

<details><summary>style.scss</summary><div>

```scss:style.scss
@import url(https://fonts.googleapis.com/css?family=Montserrat);

$background: #313E50;
$white: #fff;
$black: #282936;

body{
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    font-family: Montserrat;
    background: $background;
}

h1{
    color: $black;
}

.text_input_container{
    position: relative;
    margin-top: 50px;
    margin-left: 50px;

    input[type="text"]{
        display: inline-block;
        width: 500px;
        height: 40px;
        box-sizing: border-box;
        outline: none;
        border: 1px solid lightgray;
        border-radius: 3px;
        padding: 10px 10px 10px 180px;
        transition: all 0.1s ease-out;
    }

    input[type="text"]+label{
        position: absolute;
        top: 0;
        left: 0;
        bottom: 0;
        height: 40px;
        line-height: 40px;
        color: white;
        border-radius: 3px 0 0 3px;
        padding: 0 20px;
        background: #E03616;
        transform: translateZ(0) translateX(0);
        transition: all 0.3s ease-in;
        transition-delay: 0.2s;
    }

    input[type="text"]:focus+label{
        transform: translateY(-120%) translateX(0%);
        border-radius: 3px;
        transition: all 0.1s ease-out;
    }

    input[type="text"]:focus{
        padding: 10px;
        transition: all 0.3s ease-out;
        transition-delay: 0.2s;
    }
}

@mixin transition($property: all, $duration: 0.45s, $ease: cubic-bezier(0.65,0,.076,1)){
  transition: $property $duration $ease;
}

#button_container{
    display: block;
    text-align: center;
    margin-top: 50px;
}

button{
    position: relative;
    display: inline-block;
    cursor: pointer;
    outline: none;
    border: 0;
    vertical-align: middle;
    text-decoration: none;
    background: transparent;
    padding: 0;
    font-size: inherit;
    font-family: inherit;
    &.Run{
        width: 12rem;
        height: auto;
        .circle{
            @include transition(all, 0.45s, cubic-bezier(0.65,0,.076,1));
            position: relative;
            display: block;
            margin: 0;
            width: 3rem;
            height: 3rem;
            background: $black;
            border-radius: 1.625rem;
            .icon{
                @include transition(all, 0.45s, cubic-bezier(0.65,0,.076,1));
                position: absolute;
                top: 0;
                bottom: 0;
                margin: auto;
                background: $white;
                &.arrow{
                    @include transition(all, 0.45s, cubic-bezier(0.65,0,.076,1));
                    left: 0.625rem;
                    width: 1.125rem;
                    height: 0.125rem;
                    background: none;
                    &::before{
                        position: absolute;
                        content: '';
                        top: -0.25rem;
                        right: 0.0625rem;
                        width: 0.625rem;
                        height: 0.625rem;
                        border-top: 0.125rem solid #fff;
                        border-right: 0.125rem solid #fff;
                        transform: rotate(45deg);
                    }
                }
            }
        }
        .button_text{
            @include transition(all, 0.45s, cubic-bezier(0.65,0,.076,1));
            position: absolute;
            top: 0;
            left: 0;
            right: 0;
            bottom: 0;
            padding: 0.75rem 0;
            margin-left: auto; margin-right: auto;
            color: $black;
            font-weight: 700;
            line-height: 1.6;
            text-align: center;
            text-transform: uppercase;
        }
    }
    &:hover{
        .circle{
            width: 100%;
            .icon{
                &.arrow{
                    background: $white;
                    transform: translate(1rem, 0);
                }
            }
        }
        .button_text{
            color: $white;
        }
    }
}

.prime{
    margin-top: 100px;
    text-align: center;
    font-size: 100px;
}

.run_time{
    margin-top: 100px;
    text-align: center;
}
```

</div></details>

ぱっと見かなり短いし、何よりトレイトによって実行が保証されていることが安心感を持たせてくれますね。
Elmアーキテクチャってやっぱいいなぁ...(しみじみ
気を取り直して、コードの説明をしていきます。

## 解説

### Primeオブジェクトの定義

```Rust:src/main.rs
#[derive(Default)]
struct Prime{
    n: usize,
    prime: usize,
    run_time: f64,
}
```

アプリに関係する変数をまとめます。
`usize`型と`f64`型は`Default`トレイトを実装しているので、ついでに`Prime`オブジェクトにも`derive`で実装しておくと便利です。

***

### Message列挙体の定義

```Rust:src/main.rs
enum Message{
    Inputted(String),
    Run,
}
```

`<button>`や`<input type="text">`などにユーザーが変更を加えたときに送受信されるメッセージをまとめます。

***

### Componentトレイト実装

```Rust:src/main.rs
impl Component for Prime{/*省略*/}
```

後述の`start_app()`を実行するために、`Prime`オブジェクトに`Component`トレイトを実装します。

トレイトというのはJavaやC#でいうインターフェースのようなものです。
トレイトには**定義されなければならないオブジェクト**が存在し、オブジェクトがトレイトを得るにはそのオブジェクトを絶対に定義しなければなりません。
言い換えれば、オブジェクトに外部からインポートしたあるトレイトが実装されたとき、そのオブジェクトには**外部クレート開発者がユーザーに定義しておいてほしいと思ったオブジェクトが定義されている**ことが保証されているということになります。
これにより、ユーザーは後述する`update()`や`view()`がいつ実行されるものなのかを正確に理解する必要はなく、どんな振る舞いをするのかにのみ重点を置けます。
なんて素晴らしい言語機能なんだぁ...。

***

#### create()実装

```Rust:src/main.rs
fn create(_ctx: &Context<Self>)->Self{
    Self::default()
}
```

`Prime`の初期化を定義します。
`Self::default()`は先述の`#[derive(Default)]`をしていないと使用できないので注意してください。
ちなみに`Self::default()`の中身は以下のようになっています。

```Rust:
fn default()->Prime{
    Prime{
        n: 0,
        prime: 0,
        run_time: 0.0,
    }
}
```

***

#### update()実装

```Rust:
fn update(&mut self,_ctx: &Context<Self>,msg: Self::Message)->bool{/*省略*/}
```

メッセージを受け取った際の、`Prime`オブジェクトのメンバ変数や画面の更新処理を定義します。
このとき受信したメッセージが引数となっているので、メッセージごとの処理をパターンマッチングで網羅的に定義していきます。
返り値は画面を更新するか否かを示し、これにより**値は更新するが画面は更新しない**ということも可能になります。

***

##### 第n項目の素数算出・実行時間計測

```Rust:src/main.rs
if self.n>0{
    let start=Date::now();
    self.prime=StreamingSieve::nth_prime(self.n);
    self.run_time=Date::now()-start;
}
```

第n項目の素数を算出し、その実行時間を計測します。

ここで、`Date`はjs-sysという外部クレートのオブジェクトです。
jsという名前から分かる通り、js-sysはJavaScript の標準的な組み込みオブジェクトをRustで扱えるようにしたものです。
Rustにも`std::time::Instant`というオブジェクトで実行時間の計測は可能なのですが、残念ながら多くのブラウザは`std::time::Instant`に対応していませんでした。
よって、js-sysより`Date`オブジェクトで代用する必要があります。

ちなみに、`nth_prime()`は[アトキンの篩](https://en.wikipedia.org/wiki/Sieve_of_Atkin)という前述のエラトステネスの篩の進化版みたいなアルゴリズムを基に算出しているそうです。
興味のある方は是非速度対決してみて下さい。
~~自分は大敗しました。~~

***

#### view()実装

```Rust:src/main.rs
fn view(&self,ctx: &Context<Self>)->Html{/*省略*/}
```

画面への描画処理を定義します。
ここで、`Html`オブジェクトは`html!`マクロによって得られ、`html!`マクロ内部ではJSXライクな記述ができます。
つまるところ、HTMLを書きつつもそこでRustのあらゆる言語機能を使用できるということです。
優秀だなぁ...。

***

##### &lt;input type="text"&gt;からの入力情報取得

```Rust:src/main.rs
<div class="text_input_container">
    <input type="text" id="nth_input" oninput={ctx.link().callback(|e: InputEvent| Message::Inputted(e.target_unchecked_into::<HtmlInputElement>().value()))} />
    <label for="nth_input">{"write the N here!"}</label>
</div>
```

ユーザーが`self.n`を入力するテキストボックスを作成します。
ここで、`<input>`の'oninput'属性がテキストボックスに入力された際に実行される処理となります。
が、この記法は今後変更される可能性が高いと思われます。
というのも、`e.target_unchecked_into::<HtmlInputElement>().value()`の`HtmlInputElement`は`web-sys`という外部クレートからインポートしたオブジェクトだからです。
なぜ`HtmlInputElement`である必要があるのかは分かりませんが、少し前のバージョンではまた記法が異なっており、さらに現状`unchecked`であることからも、今後のアップデートで記法が変わるであろうことが予想されます。

### .icoの読み込み

```html:index.html
<link data-trunk rel="icon" type="image/png" href="icon.png">
```

ブラウザでページタイトルの左側に表示されるアイコンを設定します。
何かそういう衝動に駆られたので、急遽手書きしました。
なんかアイコンがあると特別感が倍増しますね。

また、今回はTrunkでビルドを行うので、属性に`data-trunk`を追加する必要があります。
これをすることでdistフォルダへ手動でコピーせずに済みます。

***

### .scssの読み込み

```html:index.html
<link data-trunk rel="sass" href="style.scss" />
```

`style.scss`を読み込みます。
先程と同様に`data-trunk`属性を追加するとscssからcssへのコンパイルだけでなくdistフォルダへのコピーまで行ってくれるので、めちゃんこ便利ですね。

解説はこんなところです。
もし不足があれば、コメントで是非その旨をお伝え下さい。
お答えできる範囲でお答えします。

# 完成！！！
ようやっと完成しました！

余談ですが、はじめてのWebアプリケーション制作が無事終了したのでテンションが上がり、流れでFirebaseによるデプロイ(サーバーにファイルを置いてインターネット上で公開すること)もしました。
デプロイにはこちらの記事がとても参考になりました。

https://qiita.com/namn1125/items/8ffa0f87fa03f59f3acc

最後に成果物のスクショを貼っておきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2470169/84adeb9d-c0cf-d90b-e191-403e87147c41.png)

# おわりに
今回はお試し感覚でyewを使用してみました。
yewは未だ発展途上であるとはいえ、とても使いやすいフレームワークだという印象を受けました。
実行速度という観点だけではなく言語機能の充実さという観点からも、今後はRustによるWebアプリケーション制作が流行ってもおかしくはない気がします。
~~というか流行ってくれ...そしたらその分ドキュメントもexampleも増えるから...。~~

またね。