Seed: Rust (WASM) frontend framework 使い方・仕様メモ
Seed とは?
Seed is a frontend Rust framework for creating fast and reliable web apps with an elm-like architecture.
Rust製のフロントエンドフレームワーク。速さと信頼性の高いwebアプリケーションを作れることを謳っている。コードの記述スタイルや状態管理などのアーキテクチャはかの有名な The Elm Architecture (TEA) に則っている。
公式ガイドがとても丁寧にまとめられており、Rust初心者でも理解できるレベルで説明されている。
- 公式ガイド: https://seed-rs.org/
- GitHubリポジトリ: https://github.com/seed-rs/seed
このスクラップの目次
Zennのスクラップは投稿日時順に表示されてしまい、投稿後には表示順を並び替えることができないっぽい。なので先頭のピン留めアイテムにこのスクラップの章立てみたいなものをに置いてみた。各アイテムは投稿した時系列順に連なっているが、内容的なまとまりはこの目次↑を参照のこと。
(※ 一応「ピン留め」によってアイテムを先頭に表示させることはできるが、ピン留めアイテムが複数あると先頭内でやはり投稿日時順に表示される)
The Elm Architecture (TEA) - Seedのアプリケーション構成
Seedのアプリケーション構成にはThe Elm Architecture (TEA)と呼ばれるフレームワークが採用されている。TEAはその名前にも入っている純粋関数型言語Elmに由来するwebフロントエンドのためのフレームワークであり、状態管理ライブラリReduxなどの設計思想の源流となったらしい。
- Elm公式によるTEAの説明: https://guide.elm-lang.jp/architecture/
TEAを構成する5つの基本要素
Seedアプリケーションの構成要素も上記のTEAほぼそのままであり、これらの要素を組み合わせてwebフロントエンドを構築する。
モデル / Model
ぶっちゃけ以下の公式ガイドの個人的まとめ(a.k.a. 劣化版日本語訳)
- 公式ガイドのモデルに関する説明: https://seed-rs.org/0.8.0/model
struct Model {...}
モデル = アプリケーションの状態を保持するもの(a.k.a. データストア)
- 複数のデータを持たせるため、大抵の場合は 構造体 (
struct
) にすることが多い- シンプルなアプリケーションの場合には型エイリアスだったり列挙型だったりで十分かもしれない
-
static
ライフタイムを持つ- i.e. Model内に参照を持たせることはできない
良いModelを書くポイント
- 極力シンプルに保つ
- 導出可能なデータは多少パフォーマンスを犠牲にしてでも導出するようにすべし
- むやみに属性を増やしたりしないこと
- データはModelのみに持たせる
- データを保持する他のオブジェクトをやたらに作らない
- 自分の人生をややこしくするな
-
Model
をジェネリック型にしない -
Model
に自前のメソッドを実装しない
-
- Modelを "single source of truth" にする
- 各コンポーネントにデータ(状態)を持たせない
- Modelをできるだけexpressiveにし、型システムを用いて許容できないビジネスルールを排除する
-
bool
型やOption
型の数は最小限にする - 同じ型を持つフィールドが複数あるなら再モデリングした方がいいかも
-
- 独自定義の型を属性に使うときには "children below the parent" の法則を守る
Single source of truth vs. Components
- 標準的なSeedのModel構成
- 単一のroot Model
- 各ページごとのModels
- 少数のコンポーネントModels
- JS標準のWeb ComponentsやSeed Hooksを導入すれば、ローカルな状態を簡単に作れるようになってしまう
- Seed appにおける状態管理のTips
- できるだけSeed標準のModel構成にすべし
- 理想的には root
Model
and/or pageModel
s
- 理想的には root
- 純粋なGUIデータ (e.g.
mouse_over
) などに対してはstate hooksを使う
- できるだけSeed標準のModel構成にすべし
メッセージ / Msg
ぶっちゃけ以下の公式ガイドの個人的まとめ(a.k.a. 劣化版日本語訳)
- 公式ガイドのメッセージに関する説明: https://seed-rs.org/0.8.0/msg
enum Msg {...}
メッセージ = イベントや命令を伝えるもの
- 大抵の場合は 列挙型 (
enum
) -
static
ライフタイムを持つ- i.e. Msgに参照を持たせることは出来ない
良いMsgを書くポイント
留意すべきポイントは Model
の場合と似ている。
- 極力シンプルに保つ
- イベントの情報を伝えるのに必要なデータだけを持たせる
- 理想的には何も持たせない
- イベントの情報を伝えるのに必要なデータだけを持たせる
- イベントに持たせるidの型は
String
やu32
ではなくUuid
を使う-
Uuid
はCopyトレイトを実装しているので、フロントでentityを作成できる(e.g. サーバーに送る前にentityをCopyする、など)
-
- 自分の人生をややこしくするな
-
Msg
をジェネリック型にしない -
Msg
に自前のメソッドを実装しない
-
- Msgをできるだけexpressiveにする
-
bool
型やOption
型などのシンプルな型の数は最小限にする - ↑これらを型エイリアスにするだけでも可読性が爆上がりする
-
- Msgには基本的に 命令 と イベント の2種類に分けられるので、以下のように命名規則を一貫させると良い
- 命令: Do + Something
- e.g.
ScrollToTop
,ToggleMenu
,RemoveItem(ItemId)
, etc.
- e.g.
- イベント: Something + Happened
- e.g.
ButtonClicked
,UrlChanged(subs::UrlChanged)
,TextUpdated(String)
, etc.
- e.g.
- 命令: Do + Something
ビュー関数 / view()
ぶっちゃけ以下の公式ガイドの個人的まとめ(a.k.a. 劣化版日本語訳)
- 公式ガイドのビュー関数に関する説明: https://seed-rs.org/0.8.0/view
fn view(model: &Model) -> Node<Msg>
ビュー関数 = モデル(状態)を引数として受け取り、描画するHTML要素を返す関数
- 返り値:
Node<Msg>
|Vec<Node<Msg>>
| ←これらをOption
型でwrapしたもの- HTML要素 or 要素の中身のテキスト
-
Node
: 様々な DOM API オブジェクト型が継承するインターフェイスのこと
良いview関数を書くポイント
- view 関数を適切にヘルパ関数やサブビュー関数に分割する
- 分割した関数の名前は
view_*
にするといいかも
- 分割した関数の名前は
- 関数分割の際には "children below the parent" の法則を守る
- 関数シグニチャに出力値の型を明記する
-
Option
になることもある
-
- 必要な
Model
変数だけを渡すようにする
アップデート関数 / update()
ぶっちゃけ以下の公式ガイドの個人的まとめ(a.k.a. 劣化版日本語訳)
- 公式ガイドのアップデート関数に関する説明: https://seed-rs.org/0.8.0/update
fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
match msg {
Msg::DoSomething => ...,
Msg::SomethingHappened => ...,
}
}
アップデート関数 = メッセージを引数として受け取り、モデル(状態)を更新する関数
- 返り値:なし
- データ (Model) を変更して良い唯一の場所
- Seed appが新規に
Msg
インスタンスを受け取ったときにupdate
関数が呼び出される
良いupdate関数を書くポイント
- 単一の
match
式にする(シンプルに保つ) -
update
がアプリケーション内で一番長い関数になるかも- だからといって下手に短くしようとするとコードの質が下がるよ
-
update
ヘルパを作るのは本当に必要なときだけにしとけ- そして書くときには "children below the parent" のルールを忘れずに
- catch-all
match
armは書かない - 1つのMsgを複数のmatch armでハンドリングするのが有効
- 特に
Msg
がResult
型やOption
型の場合、ネストや定型コードを減らせる
- 特に
- Msgがたくさんある場合、コメントを活用してMsgとupdate関数の中身をグループ化すると良い
初期化関数 / init()
fn init(url: Url, orders: &mut impl Orders<Msg>) -> Model {...}
初期化関数 = URLを引数として受け取り、最初にページを表示するのに必要なモデル(状態)を返す関数
- 返り値:
Model
- 初期化関数の主な役割は
Model
インスタンスを作成すること
- 初期化関数の主な役割は
- 引数
-
url: Url
- Modelには現在のURLに依存する属性を持たせることが多い
-
-
orders: &mut impl Orders<Msg>
- Seedに命令を渡す
- (加筆予定)
- 余談:
orders
は何でそんな変な型なの?
- ページを開始時に1度だけ呼ばれる
- app内の特定のページでのみ必要な初期化をしたいときには、たとえばModelに列挙型属性 (e.g.
enum Page
) を持たせてその型にinit
メソッドを実装する - たとえばこんなかんじ↓
- app内の特定のページでのみ必要な初期化をしたいときには、たとえばModelに列挙型属性 (e.g.
use page // src/page.rs 内でblogモジュールを定義しておく
const BLOG: &str = "blog";
Model {
base_url: url.to_base_url(),
page: Page::init(url),
...
}
enum Page {
Home,
Blog(page::blog::Model),
NotFound,
}
impl Page {
fn init(mut url: Url) -> Self {
match url.next_path_part() {
None => Self::Home,
Some(BLOG) => page::blog::init(url).map_or(Self::NotFound, Self::Blog),
Some(_) => Self::NotFound,
}
}
}
良いinit関数を書くポイント
- 短くシンプルにする
- 何かヘルパを作るときは "children below the parent" のルールを守る