Dioxus と Tailwind CSS を使って簡易 Web サイトを実装してみた

## はじめに

これから Rust が来そう

https://www.rust-lang.org/ja

ということでフレームワークを調べていたところ、以下のリポジトリーで色々比較されていました

https://github.com/flosse/rust-web-framework-comparison#frontend-frameworks-wasm

Frontend frameworks (WASM) の項を見ると、 yew の Star が 20k で圧倒的になっていました

https://yew.rs/

やっぱり yew がいいのかな、と思いましたが、 Activity を見ると Dioxus が 1.2k/year でダントツです

https://dioxuslabs.com/

yew は成熟期に入った感じですが、「これから」という意味では Dioxus が熱そうです

Qiita にも記事がなかったので、使ってみることにしました

実装したコードはこちら

https://github.com/RyoWakabayashi/dioxus-web-example

また、以下の記事を参考にしました

https://zenn.dev/mizchi/scraps/449f0591e2cee9

## Dioxus とは

公式サイトの紹介文は以下の通りです

> a React-like library for building fast, portable, and beautiful user interfaces with Rust.
> Runs on the web, desktop, mobile, and more.

Rustで高速、ポータブル、美しいユーザーインターフェースを構築するためのReactライクなライブラリです
Web、デスクトップ、モバイルなど、さまざまな環境で動作します

- Web アセンブリーなので高速に動作します

https://developer.mozilla.org/ja/docs/WebAssembly

- Tauri を使っているため、 Linux でも Windows でも macOS でも動かせます

https://github.com/tauri-apps/tauri

- iOS と Android のサポートはまだ現状微妙そうですが、将来的にはきちんとできそうです

- SSR (サーバーサイドレンダリング)　もできます

- 静的型付なので、コンパイル時にエラーを検出できます

- React ライクに書けます
    - 状態管理が簡単に書けます
    - 再利用性の高いコンポーネントが簡単に書けます

これだけ聞くと、最強のフレームワークのようです

まだまだ発展途上ではありますが、やってみる価値はありそうです

## Hello, world

公式のチュートリアルはこちら

https://dioxuslabs.com/reference/web/index.html

いつも使っている asdf (超便利) を使って Rust の最新版をインストールし、早速やってみました

https://github.com/asdf-vm/asdf

参考にした記事にもありますが、チュートリアルにはいくつか漏れがあるので、
追加でコマンドを実行する必要がありました

まず WASM をビルドするために trunk をインストールします

```bash
cargo install trunk
```

ターゲットアーキテクチャーとして WASM32 を指定できるようにするため、
wasm32-unknown-unknown を追加します

```bash
rustup target add wasm32-unknown-unknown
```

新しいプロジェクトを作成します

```bash
cargo new <プロジェクト名>
cd <プロジェクト名>
```

`cargo add` コマンドを実行するため、 cargo-edit をインストールします

```bash
cargo install cargo-edit
```

Dioxus をプロジェクトの依存パッケージ追加します

```bash
cargo add dioxus --features web
```

index.html を以下の内容で保存します

チュートリアルの index.html だと htmlhint に怒られたため、若干変更しています

- meta タグを分離
- title タグを追加

```html
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>タイトル</title>
  </head>
  <body>
    <div id="main"> </div>
  </body>
</html>
```

https://github.com/htmlhint/HTMLHint

src/main.rs を以下のように編集します

```rust
use dioxus::prelude::*;

fn main() {
    dioxus::web::launch(app);
}

fn app(cx: Scope) -> Element {
    cx.render(rsx!{
        div { "hello, wasm!" }
    })
}
```

以下のコマンドを実行して、ブラウザで http://localhost:8080 を開きます

```bash
trunk serve
```

![スクリーンショット 2022-03-09 19.27.06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/57de111a-c3e2-4723-d4a2-fab7a1d85c2e.png)

無事 hello, wasm! と表示されました

Rust の中で HTML の div 要素を記述しているあたりが React っぽいですね 

```rust
    cx.render(rsx!{
        div { "hello, wasm!" }
    })
```

以下のコマンドでリリース用にビルドします

```bash
trunk build --release
```

ただ、これだけだと価値がわからないので、もう少し掘り下げてみました

## カウンターを作る

公式サイトの中央に大きく書かれているカウンターのサンプルを作ってみましょう

![スクリーンショット 2022-03-09 19.31.08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/ab6d1b73-32cf-048e-7f93-e26e8dbb6a80.png)

src/main.rs を以下の通り編集します

```rust
use dioxus::prelude::*;

fn main() {
    dioxus::web::launch(app);
}

fn app(cx: Scope) -> Element {
    let (count, set_count) = use_state(&cx, || 0);

    cx.render(rsx!(
        h1 { "High-Five counter: {count}" }
        button { onclick: move |_| set_count(count + 1), "Up high!" }
        button { onclick: move |_| set_count(count - 1), "Down low!" }
    ))
}
```

`use_state` は React 丸出しですね

ボタンを押すと数字が増減するようになりました

![counter.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/433156e3-f200-5335-eb80-bd2bf49c8ce6.gif)

## Tailwind CSS

しかし、これではとても美しい UI とは言えませんね

公式サイトのイメージとも違います

公式サイト自体も Dioxus で作られているので、コードを見てみましょう

https://github.com/DioxusLabs/docsite

ここの index.html を見ると、以下のように CSS を読み込んでいました

```html
    <!-- style stuff -->
    <!-- <script src="https://cdn.tailwindcss.com"></script> -->
    <link data-trunk rel="css" href="tailwind.css" />
```

Tailwind CSS　自体を寡聞にして知らなかったので、調べてみると面白そうでした

https://tailwindcss.jp/

ざっくり言うと、クラスでスタイルを指定できる簡単なスタイルシートフレームワークです

これも公式サイトの例が分かりやすいですね

![スクリーンショット 2022-03-09 19.44.27.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/88c5a9c9-4e14-0416-3e45-9b584c2876b9.png)

`bg-white` で背景色が白、 `rounded-lg` で丸みが強めの角丸、 `h-16 w-16` でサイズを指定しています

自分でごちゃごちゃ CSS を書くのではなく、準備されたクラスを使って指定していくわけです

カスタマイズしたければこれに追加で CSS を書けば良いだけです

Dioxus の公式サイトも利用しているので、これを使ってスタイルを変えてみましょう

依存パッケージとしてプロジェクトに追加したいため、
package.json を作って devDependencies に追加しておきます

（特に必要なくても commitlint のために全プロジェクトに入れていますが）

```json
{
  "name": "dioxus-web-example",
  "version": "1.0.0",
  ...
  "devDependencies": {
    "@commitlint/cli": "^16.2.1",
    "@commitlint/config-conventional": "^16.2.1",
    "npm-run-all": "^4.1.5",
    "tailwindcss": "^3.0.23"
  }
}
```

`npm install` すればプロジェクト内に入ります

続いて、 tailwindcss の設定ファイル tailwind.config.js を作ります

```js
module.exports = {
  content: [
    './src/**/*.rs',
    './index.html',
    './src/**/*.html',
    './src/**/*.css'
  ],
  theme: {},
  variants: {},
  plugins: []
}
```

そして、 index.html に CSS の読み込みを加えます

```html
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link data-trunk href="./tailwind.css" rel="css" /><!-- この行を追加 -->
    <title>Dioxus Web Example</title>
  </head>
  <body>
    <div id="main"> </div>
  </body>
</html>
```

src/main.rs を以下のように編集します

```rust
use dioxus::prelude::*;

fn main() {
    dioxus::web::launch(app);
}

fn app(cx: Scope) -> Element {
    let (count, set_count) = use_state(&cx, || 0);

    cx.render(rsx!(
        div {
            class: "flex justify-center p-2 mt-5",

            div {
                h1 {
                    class: "mb-8 text-4xl font-light",
                    "The count is: {count}"
                }

                button {
                    class: "mb-4 mr-2 text-white bg-blue-500 border-0 rounded py-1 px-4 focus:outline-none hover:bg-gray-300",
                    onclick: move |_| set_count(count - 1),
                    "-"
                }

                button {
                    class: "mb-4 text-white bg-blue-500 border-0 rounded py-1 px-4 focus:outline-none hover:bg-gray-300",
                    onclick: move |_| set_count(count + 1),
                    "+"
                }
            }
        }
    ))
}
```

たくさんクラス名が増えましたが、 CSS を知っている人なら見ただけで大体どんなスタイルが適用されるか分かりますね

この状態で以下のコマンドを実行します

```bash
npx tailwindcss -o tailwind.css
```

すると、 tailwind.css が以下のように生成されます

このファイルは生成されるものなので、 .gitignore に入れておきましょう

```css
/*
! tailwindcss v3.0.23 | MIT License | https://tailwindcss.com
*/

...

.mt-5 {
  margin-top: 1.25rem;
}

.mb-8 {
  margin-bottom: 2rem;
}

.mb-4 {
  margin-bottom: 1rem;
}

.mr-2 {
  margin-right: 0.5rem;
}

.flex {
  display: flex;
}

.justify-center {
  justify-content: center;
}

.rounded {
  border-radius: 0.25rem;
}

.border-0 {
  border-width: 0px;
}

...
```

これの面白いところは、 Rust 側で指定したクラスだけが記載されている点です

`mb-3` や `rounded-lg` は存在しません

使われているものだけを出力することで、 CSS を最小限にしているわけですね

以下のコマンドを実行しておくことで、 tailwind.config.js の `content` に指定したファイルが変更されたとき、
自動的に CSS も書き換えてくれるようになります

```bash
npx tailwindcss -w -o tailwind.css
```

ただ、 `trunk serve` と `npx tailwindcss -w -o tailwind.css` を
それぞれ別ターミナルで実行しておくのはめんどくさいです

というわけで、 package.json にスクリプトとして登録して並行実行しましょう

`npm-run-all` を `npm install npm-run-all --save-dev`　で追加しておくと、
スクリプトで逐次実行、並列実行ができるようになります

```json
{
  "name": "dioxus-web-example",
  "version": "1.0.0",
  ...
  "scripts": {
    ...
    "dev": "run-p dev:*",
    "dev:serve": "trunk serve",
    "dev:css": "tailwindcss -w -o tailwind.css",
    ...
  },
  "devDependencies": {
    "@commitlint/cli": "^16.2.1",
    "@commitlint/config-conventional": "^16.2.1",
    "npm-run-all": "^4.1.5",
    "tailwindcss": "^3.0.23"
  }
}
```

上記のように定義すると、 `npm run dev:serve` で `trunk serve` 、
`npm run dev:css`　で `npx tailwindcss -w -o tailwind.css`　が実行されます

`npm-run-all` をインストールしている状態で `run-p` を使うと、指定したコマンドを並列実行します

`run-p dev:*` とすれば `npm run dev` で `dev:` から始まる全てのスクリプトが同時に実行されます

これで　`npm run dev` でスタイルも含めてホットリロードされるようになりました

ついでに、リリース用ビルドも整理しましょう

`npx tailwindcss -o tailwind.css --minify` でリリース用に最小化された CSS を出力します

従って、リリース用のスクリプトは以下のようになります

```json
...
    "build": "run-s build:css build:dioxus",
    "build:dioxus": "trunk build --release",
    "build:css": "tailwindcss -o tailwind.css --minify"
...
```

`run-s` は逐次実行で、指定した順にスクリプトを実行します

これで `npm run build` を実行すれば最小の CSS を含んだリソースがビルドされます

## コンポーネント

次にコンポーネントを試してみましょう

先ほど作ったカウンターをコンポーネントとして独立させます

src/components/counter.rs

```rust
use dioxus::prelude::*;

pub fn Counter(cx: Scope) -> Element {
    let (count, set_count) = use_state(&cx, || 0);

    cx.render(rsx!(
        div {

            h1 {
                class: "mb-8 text-4xl font-light",
                "The count is: {count}"
            }

            button {
                class: "mb-4 mr-2 text-white bg-blue-500 border-0 rounded py-1 px-4 focus:outline-none hover:bg-gray-300",
                onclick: move |_| set_count(count - 1),
                "-"
            }

            button {
                class: "mb-4 text-white bg-blue-500 border-0 rounded py-1 px-4 focus:outline-none hover:bg-gray-300",
                onclick: move |_| set_count(count + 1),
                "+"
            }
        }
    ))
}
```

src/main.rs

```rust
use dioxus::prelude::*;

pub mod components {
    pub mod counter;
}

fn main() {
    dioxus::web::launch(app);
}

fn app(cx: Scope) -> Element {
    cx.render(rsx!(
        div {
            class: "flex justify-center p-2 mt-5",
            components::counter::Counter {} // コンポーネントを使用
        }
    ))
}
```

簡単に分離できましたね

次はカスタムプロパティ付きのコンポーネントを作ってみましょう

src/components/gauge.rs

```rust
use dioxus::prelude::*;

// カスタムプロパティを定義
#[derive(Props, PartialEq)]
pub struct GaugeProps<'a> {
    num_blocks: &'a i32,
}

pub fn Gauge<'a>(cx: Scope<'a, GaugeProps<'a>>) -> Element {
    cx.render(rsx!(
        div {
            class: "w-[100px] h-[400px]",

            (0..99).map(|index| {
                let class =
                    if &index >= cx.props.num_blocks {
                        "w-[100px] h-[3px] mb-px invisible"
                    } else if index >= 80 {
                        "w-[100px] h-[3px] mb-px bg-[#f00]"
                    } else {
                        "w-[100px] h-[3px] mb-px bg-[#0f0]"
                    };

                rsx! {
                  div {
                      class: "{class}"
                  }
                }
            })
        }
    ))
}
```

src/components/counter.rs

```rust
...

            button {
                class: "mb-4 text-white bg-blue-500 border-0 rounded py-1 px-4 focus:outline-none hover:bg-gray-300",
                onclick: move |_| set_count(count + 1),
                "+"
            }

            // このブロックを追加
            crate::components::gauge::Gauge {
                num_blocks: count // コンポーネントのプロパティを渡す
            }
        }
    ))
}
```

src/main.rs

```rust
use dioxus::prelude::*;

pub mod components {
    pub mod counter;
    pub mod gauge; // この行を追加s
}
```

`props.` のあたりがかなり React らしいです

![gauge.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/864be2d2-eef3-3c9d-b54c-b93257740ffd.gif)

カウンターに応じてゲージが伸びるようになりました

## おわりに

Rust 自体まだ全然習得できていませんが、かなり可能性を感じました

次は WebSocket を大量に受けて高速レンダリングできるか検証してみたいと思います