はじめに
これから 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
無事 hello, wasm! と表示されました
Rust の中で HTML の div 要素を記述しているあたりが React っぽいですね
cx.render(rsx!{
div { "hello, wasm!" }
})
以下のコマンドでリリース用にビルドします
trunk build --release
ただ、これだけだと価値がわからないので、もう少し掘り下げてみました
カウンターを作る
公式サイトの中央に大きく書かれているカウンターのサンプルを作ってみましょう
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 丸出しですね
ボタンを押すと数字が増減するようになりました
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 自体を寡聞にして知らなかったので、調べてみると面白そうでした
ざっくり言うと、クラスでスタイルを指定できる簡単なスタイルシートフレームワークです
これも公式サイトの例が分かりやすいですね
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-3
や rounded-lg
は存在しません
使われているものだけを出力することで、 CSS を最小限にしているわけですね
以下のコマンドを実行しておくことで、 tailwind.config.js の content
に指定したファイルが変更されたとき、
自動的に CSS も書き換えてくれるようになります
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
で追加しておくと、
スクリプトで逐次実行、並列実行ができるようになります
{
"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 を出力します
従って、リリース用のスクリプトは以下のようになります
...
"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 らしいです
カウンターに応じてゲージが伸びるようになりました
おわりに
Rust 自体まだ全然習得できていませんが、かなり可能性を感じました
次は WebSocket を大量に受けて高速レンダリングできるか検証してみたいと思います