15
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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

Last updated at Posted at 2022-03-14

はじめに

これから Rust が来そう

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

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

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

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

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

実装したコードはこちら

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

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 アセンブリーなので高速に動作します

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

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

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

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

  • React ライクに書けます

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

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

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

Hello, world

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

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

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

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

cargo install trunk

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

rustup target add wasm32-unknown-unknown

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

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

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

cargo install cargo-edit

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

cargo add dioxus --features web

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

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

  • meta タグを分離
  • title タグを追加
<!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>

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

use dioxus::prelude::*;

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

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

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

trunk serve

スクリーンショット 2022-03-09 19.27.06.png

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

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

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

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

trunk build --release

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

カウンターを作る

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

スクリーンショット 2022-03-09 19.31.08.png

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

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

Tailwind CSS

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

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

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

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

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

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

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

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

スクリーンショット 2022-03-09 19.44.27.png

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

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

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

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

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

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

{
  "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 を作ります

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

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

<!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 を以下のように編集します

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 を知っている人なら見ただけで大体どんなスタイルが適用されるか分かりますね

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

npx tailwindcss -o tailwind.css

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

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

/*
! 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-3rounded-lg は存在しません

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

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

npx tailwindcss -w -o tailwind.css

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

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

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

{
  "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:servetrunk serve
npm run dev:css で npx tailwindcss -w -o tailwind.css が実行されます

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

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

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

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

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

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

...
    "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

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

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

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

...

            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

use dioxus::prelude::*;

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

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

gauge.gif

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

おわりに

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

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

15
7
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
15
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?