🪐

Rust×Tauriでアプリ開発するときにYewとstylistに触れてみた

2024/12/16に公開3

この記事は Rust Advent Calender 2024 の 16 日目の記事です。

普段はタコスとちいかわとホラー映画を愛でている WebアプリやWindowsデスクトップアプリ開発、画像解析をしているエンジニアです!久々にZenn記事書きますm(__)m

要約

  • Tauriを利用したデスクトップアプリのフロントエンド開発にYewフレームワークを選んだよ
  • スタイル管理には、コード内で安全にCSSを記述できるstylistクレートを活用するよ
  • コード可読性のために、style.rsを作成してスタイル管理を効率化したよ

Tauriとは?

https://tauri.app/

2024年10月に2.0正式版がリリースされたGUIフレームワークです。
バックエンドにRustを利用したデスクトップアプリケーション、モバイルアプリケーションを作成することができるというものです。
バンドルサイズが非常に小さい、Windows/Linux/macOSサポートといった特徴をもっています。
タウリと呼べばいいのか、それともタオライなのか、悩んでいます🤔

フロントエンドの開発には、以下3種から選ぶことができます。

? Choose which language to use for your frontend ›
  Rust  (cargo)
  TypeScript / JavaScript  (pnpm, yarn, npm, deno, bun)
  .NET  (dotnet)

おわかりいただけただろうか?
我らがRustを利用したフロントエンド開発が可能ということに。
フロント/バックエンドをRustで統一して、楽しいアプリ開発ができるのです!やったぜ。
実際に開発するときにTypeScript / JavaScriptを自然と選んでしまったことは言えない😇

Rustでフロントエンド解発を体験してみたい、フロント/バックエンド間でのデータ受け渡しを簡単にしたい、フロント/バックエンドでデバッグ環境を切り替えたくない、Rustで縛りプレイをしている...などなどの条件があるときにTauriフロントエンドにRustを選ぶと良いかもですね!

Rustを選択したときに利用できるフレームワークは以下の通りです。

? Choose your UI template ›
  Vanilla
  Yew
  Leptos
  Sycamore
  Dioxus

それぞれの詳細は割愛します😇
ReactとWasmに慣れ親しんでいる僕(諸説あり)が選ぶフレームワークは...

Yew!!!

単純にStarが多いことが理由の一つですw
(とはいえ直近のリリースが1年前なので少し悩みました。本音はDioxus気になる。)

ちなみに、TypeScript / JavaScriptを選んだ場合はReact, Vue, Svelteなどが選択できます。.NETを選ぶとBlazorとなります。

Yew

https://yew.rs/
発音はイウ?ユー?

ここでは簡単に紹介しますが、以下のような特徴を持っているようです。

  • Reactライクな構文でUIを記述可能。
  • use_stateuse_reducerのような状態管理の仕組みをもつ。
  • Wasm(WebAssembly)を基盤としているため高速な描画が可能。

Reactに慣れているRustaceanは、とりあえずYewを選ぶみたいなケースが多いかも?

環境構築

公式のQuick Startに沿って実行すれば基本OKです。

Tauriインストール

cargo install create-tauri-app --locked

プロジェクト作成

cargo create-tauri-app

上記のコマンドを実行すると、プロジェクト名やフロントエンド開発言語、UI Templateなどについて聞かれます。
今回は以下のようになります。

✔ Project name · tauri-yew
✔ Identifier · com.tauri-yew.app
✔ Choose which language to use for your frontend · Rust - (cargo)
✔ Choose your UI template · Yew - (https://yew.rs/)

プロジェクトが作成されると、いくつか足りないものがあると言われるかもしれないです。
僕の場合はtrunkというビルドツールが足りないと言われたので、これのインストールも行いました。

cargo install trunk locked

アプリ起動(初回はコンパイルが走ります)

cd tauri-dev
cargo tauri dev


やったぜ。

アプリのデザイン

アプリ開発において、一貫したデザインはUXを向上させるために重要なことだと思います。
僕が所属する会社でも、アプリ全体で統一感を保つためにデザインシステムが導入されています。
デザインシステムでは、ボタンやフォーム、色、間隔などのスタイル要素が細かく定義されており、それに基づいてアプリケーションを構築しています。
Rustを利用したフロントエンド開発でも、同様にスタイルを管理する手段が必要です。
Yewフレームワークでは、スタイル管理の選択肢として複数のクレートが存在します。その中で今回は、stylistというクレートを利用しました!

stylist

https://github.com/futursolo/stylist-rs

スタイルを直接Rustコード内に定義できる(CSS-in-Rust)クレートです。グローバルにスタイルを適用したり、変数を利用した動的なスタイル変更が可能です。
以下のようにCargo.tomlにstylistを追加してください。

[package]
name = "tauri-yew-ui"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
yew = { version = "0.21", features = ["csr"] }
wasm-bindgen = "0.2"
wasm-bindgen-futures = "0.4"
web-sys = "0.3"
js-sys = "0.3"
serde = { version = "1", features = ["derive"] }
serde-wasm-bindgen = "0.6"
console_error_panic_hook = "0.1.7"
+ stylist = { version = "0.13.0", features = ["yew"] }

[workspace]
members = ["src-tauri"]

Tauriアプリの初期画面で、Greetボタンにstylistを使ってスタイルを当てる例を見てみましょう。

use stylist::style;

// 省略

#[function_component(App)]
pub fn app() -> Html {

// 省略

+    let button_style = style!(
+        r#"
+        background-color: #0000FF;
+        color: #FFFFFF;
+        font-weight: bold;
+    "#
+    )
+    .unwrap();

    html! {
                <main class="container">
                    <h1>{"Welcome to Tauri + Yew"}</h1>

                    <div class="row">
                        <a href="https://tauri.app" target="_blank">
                            <img src="public/tauri.svg" class="logo tauri" alt="Tauri logo"/>
                        </a>
                        <a href="https://yew.rs" target="_blank">
                            <img src="public/yew.png" class="logo yew" alt="Yew logo"/>
                        </a>
                    </div>
                    <p>{"Click on the Tauri and Yew logos to learn more."}</p>

                    <form class="row" onsubmit={greet}>
                        <input id="greet-input" ref={greet_input_ref} placeholder="Enter a name..." />
-                        <button type="submit">{"Greet"}</button>
+                        <button type="submit" class={button_style}>
+                            {"Greet"}
+                        </button>
                    </form>
                    <p>{ &*greet_msg }</p>
                </main>
    }
}

ボタンを青く(かつ太字に)できました!
このようにstyle!マクロを利用することで、CSSスタイルをRustコード内に直接記述できます。

ボタンコンポーネント作成してみる

アトミックデザインを取り入れて、様々なコンポーネントを構築していくとしましょう。

src
├─components
│ ├─atoms
│ │ ├─button
│ │ │ ├─button.rs
│ │ │ └─mod.rs
│ │ ├─label
│ │ │ ├─label.rs
│ │ │ └─mod.rs
︙︙︙

デザインシステムで、ボタンの種類が複数(Primary, Secondary, Warning, Sccessの色だったり、大きい/小さいボタンだったり)定義されている場合、以下のような実装になるかもしれません。

use stylist::style;
use yew::prelude::*;

#[derive(Clone, PartialEq)]
pub enum ButtonVariant {
    Primary,
    Secondary,
    Warning,
    Success,
}

#[derive(Clone, PartialEq)]
pub enum ButtonSize {
    Medium,
    Small,
}

#[derive(Clone, PartialEq, Properties)]
pub struct ButtonProps {
    pub label: String,
    pub on_click: Callback<()>,
    pub disabled: bool,
    pub size: ButtonSize,
    pub variant: ButtonVariant,
}

#[function_component(Button)]
pub fn button(props: &ButtonProps) -> Html {
    let button_style = style!(
        r#"
        border: none;
        outline: none;
        display: inline-flex;
        justify-content: center;
        align-items: center;

        &.primary {
            background-color: #0000CC;
            color: #FFF;
        }
        &.primary:hover {
            background-color: #0000FF;
        }
        &.primary:active {
            background-color: #000099;
        }

        &.secondary {
            background-color: #FFF;
            border: 1px solid #0000CC;
            color: #0000CC;
        }
        &.secondary:hover {
            border-color: #0000FF;
            color: #0000FF;
        }
        &.secondary:active {
            background-color: #DDD;
            border-color: #000099;
            color: #000099;
        }

        // 省略

        &.medium, &.small {
            font-style: normal;
            transition: 0.2s all;
        }
        &.medium {
            font-size: 14px;
            padding: 8px 12px;
        }
        &.small {
            font-size: 12px;
            padding: 6px 8px;
        }
    "#
    )
    .unwrap();

    let variant = match props.variant {
        ButtonVariant::Primary => "primary",
        ButtonVariant::Secondary => "secondary",
        ButtonVariant::Warning => "warning",
        ButtonVariant::Success => "success",
    };

    let size = match props.size {
        ButtonSize::Medium => "medium",
        ButtonSize::Small => "small",
    };

    let onclick = {
        let on_click = props.on_click.clone();
        Callback::from(move |_: MouseEvent| {
            on_click.emit(());
        })
    };

    html! {
        <button
            class={classes!(button_style, variant, size)}
            onclick={onclick}
            disabled={props.disabled}
        >
            { &props.label }
        </button>
    }
}

途中省略していますが、style!マクロ内のCSSが長く、可読性が低く感じますね。
そんなときは、同ディレクトリ内にCSSスタイルを定義するstyle.rsを作成しましょう。

  src
  ├─components
  │ ├─atoms
  │ │ ├─button
  │ │ │ ├─button.rs
+ │ │ │ ├─style.rs
  │ │ │ └─mod.rs
︙︙︙

style.rsの実装は以下の通り。

use stylist::{style, Style};

pub fn button_style() -> Style {
    style!(
        r#"
        border: none;
        outline: none;
        display: inline-flex;
        justify-content: center;
        align-items: center;

        &.primary {
            background-color: #0000CC;
            color: #FFF;
        }
        &.primary:hover {
            background-color: #0000FF;
        }
        &.primary:active {
            background-color: #000099;
        }

        &.secondary {
            background-color: #FFF;
            border: 1px solid #0000CC;
            color: #0000CC;
        }
        &.secondary:hover {
            border-color: #0000FF;
            color: #0000FF;
        }
        &.secondary:active {
            background-color: #DDD;
            border-color: #000099;
            color: #000099;
        }

        &.warning {
            background-color: #CC0000;
            color: #FFF;
        }
        &.warning:not(:disabled):hover {
            background-color: #FF0000;
        }
        &.warning:not(:disabled):active {
            background-color: #990000;
        }

        &.success {
            background-color: #00CC00;
            color: #FFF;
        }
        &.success:not(:disabled):hover {
            background-color: #00FF00;
        }
        &.success:not(:disabled):active {
            background-color: #009900;
        }

        &.medium, &.small {
            font-style: normal;
            transition: 0.2s all;
        }
        &.medium {
            font-size: 14px;
            padding: 8px 12px;
        }
        &.small {
            font-size: 12px;
            padding: 6px 8px;
        }
    "#
    )
    .unwrap()
}

そしてボタンコンポーネントの実装は以下のようになります。

use crate::components::atoms::button::style::button_style;

#[function_component(Button)]
pub fn button(props: &ButtonProps) -> Html {
-    let button_style = style!(
-    r#"
-    // 省略
-    "#
-    )
-    .unwrap();
 
+    let button_style = button_style();
    let variant = match props.variant {
        ButtonVariant::Primary => "primary",
        ButtonVariant::Secondary => "secondary",
        ButtonVariant::Warning => "warning",
        ButtonVariant::Success => "success",
    };

    let size = match props.size {
        ButtonSize::Medium => "medium",
        ButtonSize::Small => "small",
    };

    let onclick = {
        let on_click = props.on_click.clone();
        Callback::from(move |_: MouseEvent| {
            on_click.emit(());
        })
    };

    html! {
        <button
            class={classes!(button_style, variant, size)}
            onclick={onclick}
            disabled={props.disabled}
        >
            { &props.label }
        </button>
    }
}

だいぶスッキリしますね!
CSSスタイルを別ファイルで管理することで、コードの可読性を向上できますし、場合によってはデザイナーが直接メンテナンスすることもできるはずです。

ボタンコンポーネントを利用例は以下の通りです。

                <div class="btn-row">
                    <Button
                        label="Primary"
                        on_click={on_click("Primary")}
                        disabled={false}
                        size={ButtonSize::Medium}
                        variant={ButtonVariant::Primary}
                    />
                    <Button
                        label="Secondary"
                        on_click={on_click("Secondary")}
                        disabled={false}
                        size={ButtonSize::Medium}
                        variant={ButtonVariant::Secondary}
                    />
                    <Button
                        label="Warning"
                        on_click={on_click("Warning")}
                        disabled={false}
                        size={ButtonSize::Medium}
                        variant={ButtonVariant::Warning}
                    />
                    <Button
                        label="Success"
                        on_click={on_click("Success")}
                        disabled={false}
                        size={ButtonSize::Medium}
                        variant={ButtonVariant::Success}
                    />
                </div>

本ブログ用に作成したリポジトリ↓
https://github.com/SHINue-rebonire/tauri-yew-sample-app

さいごに

Tauriを使ったデスクトップアプリケーション開発について、特にYewフレームワークとstylistクレートを活用したスタイル管理に焦点を当てて紹介しました。

Rustで統一したアプリ開発はまだまだ学びが必要な部分が多いですが、Rustの型安全性や性能を十分に活用できる可能性が大きいのかなと思っています。これからも頑張るぞい!💪

Discussion

kanaruskanarus

Wasm(WebAssembly)を基盤としているため高速な描画が可能。

って本当ですか?現時点では Wasm で直接 DOM を触ることはできないので、「Wasm ベースだから描画が速い」ということはないと思うのですが

shin-ueshin-ue

kanarusさん

ご指摘ありがとうございます!
おっしゃる通り「Wasm ベースだから描画が速い」は正しい表現ではありませんね...。
Wasmが活用されていることでデータ解析や画像処理のような高負荷な計算タスクを高速化できるのであって、結果的(間接的)に描画がスムーズに見える。ということになりますm(__)m

kanaruskanarus

その「データ解析や画像処理」って具体的にどういうデータ解析や画像処理を指してるんでしょうか?