Open7

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初心者でも理解できるレベルで説明されている。

このスクラップの目次

Zennのスクラップは投稿日時順に表示されてしまい、投稿後には表示順を並び替えることができないっぽい。なので先頭のピン留めアイテムにこのスクラップの章立てみたいなものをに置いてみた。各アイテムは投稿した時系列順に連なっているが、内容的なまとまりはこの目次↑を参照のこと。

(※ 一応「ピン留め」によってアイテムを先頭に表示させることはできるが、ピン留めアイテムが複数あると先頭内でやはり投稿日時順に表示される)

えとあるえとある

The Elm Architecture (TEA) - Seedのアプリケーション構成

Seedのアプリケーション構成にはThe Elm Architecture (TEA)と呼ばれるフレームワークが採用されている。TEAはその名前にも入っている純粋関数型言語Elmに由来するwebフロントエンドのためのフレームワークであり、状態管理ライブラリReduxなどの設計思想の源流となったらしい。

TEAを構成する5つの基本要素

  1. モデル: Model
  2. メッセージ: Msg
  3. ビュー関数: view()
  4. アップデート関数: update()
  5. 初期化関数: init()

Seedアプリケーションの構成要素も上記のTEAほぼそのままであり、これらの要素を組み合わせてwebフロントエンドを構築する。

えとあるえとある

モデル / Model

ぶっちゃけ以下の公式ガイドの個人的まとめ(a.k.a. 劣化版日本語訳)


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 page Models
    • 純粋なGUIデータ (e.g. mouse_over) などに対してはstate hooksを使う
えとあるえとある

メッセージ / Msg

ぶっちゃけ以下の公式ガイドの個人的まとめ(a.k.a. 劣化版日本語訳)


enum Msg {...}

メッセージ = イベントや命令を伝えるもの

  • 大抵の場合は 列挙型 (enum)
  • staticライフタイムを持つ
    • i.e. Msgに参照を持たせることは出来ない

良いMsgを書くポイント

留意すべきポイントは Model の場合と似ている。

  • 極力シンプルに保つ
    • イベントの情報を伝えるのに必要なデータだけを持たせる
      • 理想的には何も持たせない
  • イベントに持たせるidの型はStringu32ではなく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.
    • イベント: Something + Happened
      • e.g.ButtonClicked, UrlChanged(subs::UrlChanged), TextUpdated(String), etc.
えとあるえとある

ビュー関数 / view()

ぶっちゃけ以下の公式ガイドの個人的まとめ(a.k.a. 劣化版日本語訳)


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. 劣化版日本語訳)


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でハンドリングするのが有効
    • 特にMsgResult型や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メソッドを実装する
    • たとえばこんなかんじ↓
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" のルールを守る