はじめに
これは 株式会社 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"
}
こうすることで、.rs
ファイル上でもTailwindCSSのインテリセンスが効くようになります。
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の場合モジュールをインポートする際は、基本的に
- モジュールと同階層に
{モジュール名}.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
を指定しようとすると、以下のようなエラーに遭遇します。
これはtype
というキーワードが既に型定義をする際のキーワードとして存在するためにtypeというキーワードを入力すると型定義をする際のキーワードとして認識されてしまいます。
「じゃあinput要素のtype
が指定できないじゃん」と思われるかもしれませんが、raw identifierという仕組みを利用すれば可能になります。
r#{予約語}
という形で記述すれば、そのキーワードは予約されたキーワードではないということになり、使用することができます。
従ってinput要素のtype
を指定したいときはこのように記述すればOKです。
まとめ
本記事ではDioxusとRustの基本的な使い方について紹介しました。Dioxusはまだまだ発展途上ですが、様々なプラットフォーム向けにコンパイルできる点が魅力的だと感じました。Web、Desktop、Mobile、全てをRustで実装・・なんてできたらいいですね。
私自身Rustの理解がまだまだなので、今後も継続的に学習してこの記事のコンテンツをもっと充実させようと考えています!
次回は@nishi_takuyaさんのテストケースを作るときに気を付けていることです!
参考資料
Rust Document
Dioxus Document
The Rust Edition Guide
The rustc book
Rust入門