🌟

Lustreでルーティングの設定をする

に公開

こんにちは!
最近は色々な Web アプリケーションフレームワークをつまみ食いするのにハマっています 😎

今回は Gleam 製の Web アプリケーションフレームワークである Lustre にルーティング設定を追加したものをテンプレート用に GitHub に公開したので、具体的にどんな処理を追加してルーティング処理をしているのか備忘録も兼ねて説明していきたいと思います!

公開したテンプレート

https://github.com/rerelurelu/lustre-routing-template

ルーティング設定内容

まず前提として、Rails や Django など他の Web アプリケーションフレームワークにはルーティング用のファイルがあり、そこにルーティング用の path などを追加していきますが、Lustre にはそれがありません。プロジェクト作成時にそれ用のファイルが作られるわけでもなく、実装側が一から実装してあげる必要があります。一応、公式からルーティングの実装例が公開されていますが、1 ファイルに他の view なども書かれていて少し情報が多いので実際にアプリに近いような構成にして、テンプレートとして扱いやすくしています。

実装内容

それでは実装コードを参照しながらそれぞれ何をしているのか確認していきたいと思います!

実装コード全体はこちら
app.gleam
pub type Route { Home About }
pub type Msg { UrlChanged(Route) }

fn parse_route(u: uri.Uri) -> Route {
  case uri.path_segments(u.path) {
    ["about"] -> About
    _ -> Home
  }
}

fn on_url_change(u: uri.Uri) -> Msg {
  UrlChanged(parse_route(u))
}

fn init(_) -> #(Route, effect.Effect(Msg)) {
  let route =
    modem.initial_uri()
    |> result.map(parse_route)
    |> result.unwrap(Home)
  #(route, modem.init(on_url_change))
}

fn update(_model: Route, msg: Msg) -> #(Route, effect.Effect(Msg)) {
  case msg { UrlChanged(next) -> #(next, effect.none()) }
}

fn view(route: Route) {
  layout.view([], [
    case route {
      Home -> index.view()
      About -> about.view()
    }
  ])
}

pub fn main() {
  let app = lustre.application(init, update, view)
  let assert Ok(_) = lustre.start(app, "#app", [])
  Nil
}

型定義部分

pub type Route { Home About }
pub type Msg { UrlChanged(Route) }

Route 型はアプリ上で扱うページを型として定義されたものです。

Msg 型はアプリ内で発生するイベントを表しています。Lustre はElm Architectureを採用していて、すべての状態変更は Msg を通して行われるようになっています。この Msg によって「URL が変更された」という情報と「どのルートに変更されたか」という情報を伝達できるようになります。また、UrlChanged はカスタム型の中で定義されたコンストラクタで、以下の 2 つの用途で使用されます

  1. メッセージの作成: Route 型の値を受け取って Msg 型の値を返す関数として動作
  2. パターンマッチング: update 関数内でメッセージの種類を判別し、データを取り出すために使用

ルート解析部分

fn parse_route(u: uri.Uri) -> Route {
  case uri.path_segments(u.path) {
    ["about"] -> About
    _ -> Home
  }
}

この関数は URI を受け取ってそれに対応する Route を返します。この実装だと URL のパスが/about以外の場合は全部 Home を返すようになっています。

URL 変更ハンドラー

fn on_url_change(u: uri.Uri) -> Msg {
  UrlChanged(parse_route(u))
}

これはuri.Uri経由で取得されるpathを元に Msg を作成する関数です。例えば URL がhttps://example.com/aboutに変更された場合、u.path = "/about"になります。取得した path を元にparse_routeからはAboutが返され、UrlChangedによって Msg に変換されます。Msg はすでに説明したように状態変更を伝達するためのもので、これによって URL の変更をアプリ側で理解できるようになります!

初期化処理

fn init(_) -> #(Route, effect.Effect(Msg)) {
  let route =
    modem.initial_uri()
    |> result.map(parse_route)
    |> result.unwrap(Home)
  #(route, modem.init(on_url_change))
}

これはアプリ初期化時に実行される関数で、初期ルートと URL 変更を監視するオブジェクトを返します。

modemは Lustre 用のライブラリで、ブラウザナビゲーションや URL の管理を簡単にしてくれるものです。modem.initial_uri()でブラウザの現在の URL を取得して、Result が成功の場合はそれに対応するparse_routeを通して対応する Route が取得され、失敗の場合はHomeが返されるようになっています。この Result 型を使った一連の処理は Gleam の良さが現れてて好きです!

modem.init(on_url_change)は URL の変更監視の開始するためのエフェクトです。戻る/進むの操作を含む URL の変更の監視を開始して、URL 変更時にコールバック関数としてon_url_changeを呼びます。

状態更新処理

fn update(_model: Route, msg: Msg) -> #(Route, effect.Effect(Msg)) {
  case msg { UrlChanged(next) -> #(next, effect.none()) }
}

updateには監視中の値の変更検知時にどんな処理をするかが定義されています。update関数は Msg の受信によって URL の変更を知り、UrlChanged(next)によって Route のパターンにマッチするかを調べ、マッチした場合は新しい Route をnextとして返す仕組みになっています。また、今回は状態更新だけで完結するのでエフェクトに対してはeffect.none()で何もしないようにしています。

これでルーティングのメイン処理が終わったので、一旦まとめてみると

  1. ブラウザ URL 変更 → modem が検知
  2. modemon_url_change 関数を呼び出し
  3. on_url_changeUrlChanged(next) Msg を作成
  4. Lustre ランタイムupdate 関数に Msg を伝達
  5. update 関数 → URL 変更が起きたことを知り、更新時の処理を行う

こんな流れになっています。こう見るとupdate関数は

  • 何に反応するか
  • どう反応するか
  • 反応の結果を返す

これをやれば良く、外の世界で何が行われているか知る必要がないので責務分離が綺麗にできるのでとても良いですね!

View 描画処理

fn view(route: Route) {
  layout.view([], [
    case route {
      Home -> index.view()
      About -> about.view()
    }
  ])
}

これは見たまんまですね。Route に応じた View を返すだけです。

ただここには Gleam の関数型プログラミング + 強力な型システムの恩恵があって、Route型に新しい Type を追加するとここの case 式で「パターンが足りてないよ」ってエラーが出ます。これによって、新しくページを追加したときでも漏れなく修正できるので変更に強くてバグが混入しにくい開発ができます!やっぱ関数型プログラミングなんだよなぁ〜🥰

main 関数

pub fn main() {
  let app = lustre.application(init, update, view)
  let assert Ok(_) = lustre.start(app, "#app", [])
  Nil
}

これはアプリケーションのエントリーポイントで Lustre のお作法に従って書くだけのやつです。

lustre.application(init, update, view)は初期化関数と状態更新関数、View 描画関数を渡して Lustre アプリの定義をしています。

lustre.start(app, "#app", [])ではアプリの定義、マウント時の DOM 要素の CSS セレクタ、起動時の引数(今回は何もないので空配列)を渡してアプリの起動をしています。

これでルーティング処理の基盤ができた状態でアプリが起動できます!

感想

実装完走した感想ですが、Gleam で Web 開発楽しそうだなって思いました!
本文中でも言及しましたが

  • 型安全
  • 関数型プログラミングの恩恵
  • Elm Architecture の明快さ

これらは開発者体験としてとても良かったです!
気になる方はぜひ触ってみてください!

それでは、最後まで読んでいただきありがとうございました!👋

参考文献

https://gleam.run/

https://hexdocs.pm/lustre/index.html

https://hexdocs.pm/modem/index.html

https://hexdocs.pm/lustre_dev_tools/lustre/dev.html

https://zenn.dev/comamoca/articles/gleam-tour-for-typescript-user

GitHubで編集を提案

Discussion