はじめに

これは 株式会社 RetailAI X Advent Calendar 2022 の 9 日目の記事です。
前回は MLエンジニアである@atsukishさんの【MLOps】Vertex AIによるモデルモニタリングサービスの構築でした。

本日は「Rust UIライブラリ「Dioxus」をReact+Typescriptと比較してみた」です。

フロントエンドエンジニアの私は最近Rustに興味を持っているのですが、自分の特に興味のあるフロントエンド分野で学習できるライブラリ等がないかと探していたところDioxusを見つけました。
Reactを触ったことがある方なら割と違和感なくRustの学習ができるのではないかと思いましたので共有します。

Dioxusとは?

DioxusはRustでGUIアプリケーションを構築できるクレート(Rustの世界ではライブラリ、フレームワークのことをクレートと呼びます)です。
Reactライクな文法で記述することができて、
・WebAssembly
・Desktop
・Mobile(現在iOSのみ?)
・TUI
など、様々な形で出力することができます。
また、デフォルトで
・Reduxのような状態管理機能(Fermi)
・ルーティング機能(Router)
も組み込まれているため、発展途上ではありますが現状でもそれなりのフロントエンドは構築できるようになっています。
今回の記事ではWebAssemblyを出力する方法を紹介したいと思います。

Rust、Dioxusの導入

1・rustupのインストール

まずrustupをインストールしましょう。これによりコンパイラであるrustc,パッケージマネージャのcargo等のRust開発に必要なツールを一律でインストールすることが可能です。
下記の公式のやり方でインストールが可能です。

 

また、macOSの場合はhomebrewでのインストールも可能です。

//rustupをインストール
$ brew install rustup-init
$ rustup-init
//シェルの再起動
$ exec $SHELL -l

インストールが終わったら念の為バージョン確認をして、インストールされていることを確認しましょう。

//rustcのバージョン確認
$ rustc --version

>rustc 1.65.0 (897e37553 2022-11-02)
//cargoのバージョン確認
$ cargo --version

>cargo 1.65.0 (4bc8f24d3 2022-10-20)

2・周辺ツールのインストール(Trunk,Cargo-edit等)

・Trunk

TrunkはRust用のWebAssemblyバンドラーです。(JSでいうWebpack,Viteのようなもの)
index.htmlをエントリーポイントとして、WASM(WebAssembly。以降WASMと呼びます)や画像、CSSをまとめてくれるだけでなく、ホットリロード機能付きの開発サーバーを立ち上げてくれます。
個人的に現状ではWebの開発をするにはTrunkが最適なのではないかと考えます。
Trunkはcargoを利用してインストールすることができます。

//Trunkをインストール
$ cargo install trunk

また、RustコードをWebAssemblyをコンパイルするために、Rustにコンパイルターゲットを追加する必要があります。
以下のように追加します。

$ rustup target add wasm32-unknown-unknown

//※Rustがサポートしているコンパイルターゲットは以下のコマンドで確認できる
$ rustup target list

>aarch64-apple-darwin
aarch64-apple-ios
aarch64-apple-ios-sim
aarch64-fuchsia
aarch64-linux-android
省略...

コンパイルターゲットに関する参考資料

・cargo-edit

Rustでクレートをインストールする際はcargo.tomlに指定したクレートを記述する必要がありますが、都度tomlファイルに入力するのは面倒だったりします。
cargo-editはcargo add {クレート名}とコマンド名を入力することでtomlファイルにクレートの情報を入力してくれるため便利です。(JSのnpm install,yarn addみたいなもの)
また、クレートによっては使いたい機能をfeaturesとして指定する必要があったりするのですが、cargo-editではcargo add {クレート名} –features {機能名}で入力することが可能です。

3・VSCode拡張機能の導入

rust-analyzer

rust-analyzerはRustコードの型や未使用関数等のチェック、型推論等をおこなってくれるVSCode拡張機能です。
通常ですとcargo runコマンドを実行しなければわからないエラー等がVSCode上で視覚的にわかるようになり、これがあるとないとでは開発効率が全然違うと思いますので、ぜひインストールすることをお勧めします。

BetterToml

Better-Tomlはtomlファイルのシンタックスハイライト等を行なってくれる拡張機能です。
tomlファイルを編集したい時に便利なのでインストールすることをお勧めします。

crates

cratesはtomlファイルにあるクレートの依存関係を管理するための拡張機能です。
クレートのバージョンが最新かどうかは今まではcrates.ioを確認しなければいけなかったのですが、この拡張機能はtomlファイル上でクレートのバージョンを確認したり、セレクトボックスでバージョンを変更したりできます。
これも非常に便利だと思うのでインストールすることをお勧めします。

TailwindCSS IntelliSense

TailwindCSS IntelliSenseは、TailwindCSSのクラス名の補完をしてくれる拡張機能です。すでに用意されてあるクラスの他、tailfind.config.jsで定義したクラスの補完もしてくれます。
TailwindCSSを使用する場合、この拡張機能をインストールすることをお勧めします。

4・プロジェクトの作成、Dioxusの導入

それでは新しくDioxusのプロジェクトを作成してみましょう。cargo new {プロジェクト名}で作成することができます。
その後プロジェクトのディレクトリに移動し、Dioxusをcargo-editを用いてインストールしましょう。

//cargoを使用して新しいRustのプロジェクトを作成
$cargo new {任意のプロジェクト名}

$cd {任意のプロジェクト名}

//Dioxusをfeaturesを「Web」に指定してインストール
$ cargo add dioxus --features web

インストールしたら、プロジェクトのルートにTrunkのエントリーポイントになるindex.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" />
  </head>
  <body>
    <div id="main"></div>
  </body>
</html>

次にmain.rsファイルにDioxusのコードを記述します。

use dioxus::prelude::*;

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

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

あとはtrunk serveコマンドを入力してローカルサーバーを起動させ、ブラウザを開きlocalhost:8080が開ければOKです。

5・TailwindCSSの導入(お好みで)

DioxusにはTailwindCSSを導入することも可能です。
こちらの記事が参考になるかと思います。
↑の記事に補足すると、このままではTailwindCSSのインテリセンスが効かないので、Rustコード上でTailwindCSSのインテリセンスが効くように設定をする必要があります。以下の設定をsettings.jsonに追加します。

  //「class: ""」という構文でもTailwindCSSのインテリセンスが効くようにする
 "tailwindCSS.experimental.classRegex": ["class:\\s*\"([^\"]*)\""],
 //.rsファイルもhtmlとみなし、インテリセンスの対象とする
 "tailwindCSS.includeLanguages": {
    "rust": "html",
    "*.rs": "html"
  }
スクリーンショット 2022-12-06 9.55.53.png

Reactとの比較

1・関数コンポーネントの作成

React

export const Sample = () => {
    return (
        <div>hello world</div>
    )
}

Dioxus

Dioxusはrsx!マクロを使用してコンポーネントを作成します。

use dioxus::prelude::*;

#[allow(non_snake_case)]
pub fn Sample(cx: Scope) -> Element {
    cx.render(rsx! {
            div {
                "hello world"
            }
        }
    )
}

#[allow(non_snake_case)]としているのは、パスカルケースで関数を定義するとrust-analyzerで警告が発生してしまうので記述しています。

2・Propsの定義、渡し方

React

type Props {
    text: string
}

export const sample = ({text}: Props) => {
    return (
        <div>{text}</div>
    )
}

Dioxus

DioxusでPropsを定義する方法として
・構造体(Struct)を使用して定義する方法
・inline_propsマクロを使用して定義する方法
の二種類があります。

//構造体を使用して定義する方法
#[derive(Props, PartialEq)]
pub struct Props {
    text: String,
}

#[allow(non_snake_case)]
pub fn Sample(cx: Scope<Props>) -> Element {
    cx.render(rsx! {
        div { "{cx.props.text}" }
    })
}
//inline_propsマクロを使用して定義する方法
#[allow(non_snake_case)]
#[inline_props]
pub fn Sample(cx: Scope, text: String) -> Element {
    cx.render(rsx! {
        div { "{text}" }
    })
}

構造体を用いる方法はReact+Typescriptと同じような感じで馴染みがあるのではないかと思います。
inline_propsマクロを用いる方法は構造体を用いる方法よりもスッキリと記述できるのが特徴です。
基本inline_propsを用いる方法で問題ないかとは思いますが、公式ドキュメントによると、「Propドキュメントを制御できない可能性があるので、ライブラリの作者はこれを使用しないでください」とのことでした。

3・State管理、Hooks

useState

react

export const sample = () => {
    const [value, setValue] = useState("")
    
    return (
        <>
            <div>{value}</div>
            <button onClick={() => setValue("sample")}>button</button>      
        </>
    )
}

Dioxus

pub fn sample(cx: Scope) -> Element {
    let value = use_state(&cx, || "")

    cx.render(rsx!(
        button { 
            onclick: move |_| value.set("sample"), 
            "button"
        }
    ))
}

DioxusはHooksを定義する際にはSetterは書かないようです。

4・演算子

React(TS,JS)では&&や三項演算子等の演算子を用いて実装することがあると思いますが、Reactの演算子を用いた実装法をDioxusではどうやって実装するのか紹介したいと思います。(以下の例からは外側の関数宣言のコード等を省略した状態で書きますので、ご了承ください。)

&&

Reactでは結構出てくると思いますが、左辺の条件を満たす時に右辺の処理を実行する演算子です。

//isOpenがtrueの時にdivタグを表示する

const isOpen = true

return (
    isOpen && { <div>open!</div> }
)

Dioxusだとこんな感じになります。

let is_open = true;

is_open.then(|| rsx!(
    div {
        "open!"
    }
))

三項演算子

react

const hello = "world"

return (
    {hello === "world" ? <div>variable is world</div> : <div>other</div>}
)

Dioxus

Rustでは三項演算子がありません。そのためif式(Rustのifは文ではなく式です)で同等の機能が実装できます。

let hello = "world";

if hello == "world" {
    rsx!(
      div {
          "variable is world"
      }
    )
} else {
    rsx!(
        div {
            "other"
        }
    )
}

また条件分岐が多岐に渡る場合は、TS/JSの標準機能にはない(ts-patternのようなライブラリで代替はできますが)Rustの機能としてmatch式が使えると思います。
可読性も良く、個人的におすすめです。

let hello = "world";

match hello {
    "world" => rsx!(
        div {
            "hello world"
        }
    ),
    "guys" => rsx!(
        div {
            "hello guys"
        }
    ),
    //「 _ 」はそれ以外の条件の場合
    _ => rsx!(
        div {
            "other"
        }
    )
}

Reactに慣れていると戸惑うかもしれないRustの機能

1・所有権

まず第一に所有権が挙げられると思います。
RustはGCがないため、基本的にletで宣言した変数はスコープから外れると破棄されます。
Reactと違って変数の定義の仕方はもちろん、変数を使用する際もそのまま使うのか、不変参照(&をつける。↑でもuseStateのセクションで行なっています)にするか、可変参照(&mutをつける)にするか等に気を配る必要が出てきます。

2・モジュール宣言、インポートの方法

Reactの場合はモジュールのインポートは以下のようにして行います。

//default export+絶対パスの場合
import Sample from 'src/components/Sample'
//named exportの+相対パスの場合
import { Sample } from './components/Sample'

Rustの場合モジュールをインポートする際は、基本的に

    1. モジュールと同階層に{モジュール名}.rsファイルを置いて、その{モジュール名}.rsファイル内で作成してあるモジュール、ファイルをpub mod {ファイル名}で公開する

main.rsまたはlib.rsに同階層にあるモジュールをmod {モジュール名}で使用する

use crate::{モジュール名},use super::{モジュール名}等でインポートする

という手順を踏む必要があります。
言葉にすると訳がわからないと思うので以下の図のようなディレクトリ構成の場合のやり方を説明します。

src
 ┣components
 ┃     ┣atoms
 ┃     ┃   ┣AtomsSampleA.rs
 ┃     ┃   ┗AromsSampleB.rs
 ┃     ┣molecules
 ┃     ┃   ┗MoleculesSample.rs
 ┃     ┣organisms
 ┃     ┃   ┗OrganismsSample.rs
 ┃     ┃
 ┃     ┣aroms.rs ←モジュールと同階層に{モジュール名}.rsファイルを置く
 ┃   ┣molecules.rs ←モジュールと同階層に{モジュール名}.rsファイルを置く
 ┃   ┗organisms.rs ←モジュールと同階層に{モジュール名}.rsファイルを置く
 ┃
 ┣components.rs ←モジュールと同階層に{モジュール名}.rsファイルを置く
 ┗main.rs

src配下の階層には「components」モジュールがあるため、同階層に「components.rs」を作成します。
components配下の階層には「atoms」「molecules」「organisms」があるため、同階層に「atoms.rs」「molecules.rs」「organisms.rs」を作成します。
次に{モジュール名}.rsで、各モジュール内にあるモジュール、ファイルを公開します。
components.rsの場合はこうなります。

pub mod aroms;
pub mod molecules;
pub mod organisms;

aroms.rsの場合はこうなります。

//↓パスカルケースで記述しているのでこれを記述していますが、スネークケースで記述している場合は必要ありません
#![allow(non_snake_case)]
pub mod AtomsSampleA;
pub mod AtomsSampleB;

同様にmolecules.rs,organisms.rsも自モジュール内のファイルを公開していきます。
最後にmain.rs内で同階層のモジュールを読み込みます。

mod components;

これでuseを用いてモジュールをインポートすることができるようになります。

//絶対パス
use crate::components::molecules::MoleculesSample::MoleculesSample;
//相対パス
use super::super::molecules::MoleculesSample::MoleculesSample;

TS/JSとは全然勝手が違って戸惑うかとは思いますが、Rustではこのようにモジュールをインポートします。
ちなみにネットで調べると同階層に{モジュール名}.rsではなくmod.rsを用いてモジュールを公開する方法がかなり出てきますが、mod.rsを用いる方法は2015エディションの方法らしく、2021エディションの現在は推奨されていません。(後方互換性を保つためか一応今でもできるようにはなっていますが、同名のmod.rsが大量にコード上に出てくるようになるのでコードの規模が大きくなるほど見づらくなります。)参考資料:Rust Edition Guide

3・構造体(Struct)、Trait、継承

構造体やトレイトの概念もReactには登場しないので最初は少し戸惑うかもしれません。
構造体とはデータ型の要素を集めたものでTypescriptでいうinterfaceやtypeに近いものです。
本記事のPropsの定義、渡し方のセクションにも構造体を使用してPropsを定義していた箇所があったかと思います。

#[derive(Props, PartialEq)] //←これは何?
pub struct Props {
    text: String,
}

もう一度見た時に、#[derive()]の記述があることに気がつきますが、これにはRustのTraitと継承(derive)の機能が関わってきます。
Trait(トレイト)とは、型に定義できる共通の振る舞いのことです。
Traitは自分で実装することもできるのですが、#[derive()]アトリビュートを使用して標準で用意されているTraitを構造体に実装することが可能です。deriveできる標準Trait
これにより、構造体に様々な機能を付与することが可能になります。

この他にも、構造体にはimplキーワードでメソッドを関連づけることもできます。

4・raw identifier

Dioxusでinput要素のtypeを指定しようとすると、以下のようなエラーに遭遇します。

スクリーンショット 2022-12-08 12.04.51.png
スクリーンショット 2022-12-08 13.28.38.png

まとめ

本記事ではDioxusとRustの基本的な使い方について紹介しました。Dioxusはまだまだ発展途上ですが、様々なプラットフォーム向けにコンパイルできる点が魅力的だと感じました。Web、Desktop、Mobile、全てをRustで実装・・なんてできたらいいですね。
私自身Rustの理解がまだまだなので、今後も継続的に学習してこの記事のコンテンツをもっと充実させようと考えています!
次回は@nishi_takuyaさんのテストケースを作るときに気を付けていることです!

参考資料

Rust Document
Dioxus Document
The Rust Edition Guide
The rustc book
Rust入門

广告
将在 10 秒后关闭
bannerAds