Edited at

Elm SPA テンプレートを作った 1

作った

https://github.com/kawausokun/elm-my-spa


概要

elm-spa-example というリポジトリがありまして、Elm で大きめの SPA(Single Page Application) を構築する際に非常に参考になるものです。ただ自分が Elm 初心者ゆえ仕組みを理解するのに少し時間がかかったため、後続向けに理解の手助けになる記事を書きたいなと思った次第。

...が、最初は elm-spa-example のコードを解説していくつもりだったんですけど、まともにやろうとすると結構時間かかりそう。ちょっと方針を変えて、よりシンプルな Elm SPA テンプレート elm-my-spa を作ったので、それをベースにざっくり解説しようかと思います。

対象は Elm guide を読み終わったくらいの人です。

elm-my-spa は;


  • 複数ページを持つ SPA を作るとっかかりに使えるテンプレートです。

  • 1ページ 1Elm ファイルというちょうどよい単位でコード分割できます

  • ほとんどの仕組みは elm-spa-example のパクりですが、自分にとって不要だったロジックを省略することでシンプルになっています

  • このテンプレートを基本にして自社製品を作り完成させたので、実績あり :)

解説を始めます。コードの細部は説明せず、重要な箇所だけ言及していきます。


どんな画面

https://kawausokun.github.io/elm-my-spa/

2ページ構成で、リンクをクリックすることで各ページを行き来できます。


プロジェクト構成

Screenshot 2018-12-05 09.31.05.png

src/Main.elm が Elm のエントリーポイントです。 src/Page/*.elm が各ページのコードです。


ページを切り替える仕組み

src/Main.elm でページ切り替えを行っています。

view 関数を見てみましょう。


Main.elm

view : Model -> Browser.Document Msg

view model =
let
viewPage toMsg { title, body } =
{ title = title, body = List.map (Html.map toMsg) body }
in
case model of
NotFound _ ->
{ title = "Not Found", body = [ Html.text "Not Found" ] }

Index _ subModel ->
viewPage GotIndexMsg (IndexPage.view subModel)

View _ _ subModel ->
viewPage GotViewMsg (ViewPage.view subModel)


Model が Custom Types で、 NotFound, Index, View と3パターン定義されていて、これでどのページを表示するか判断しています。

IndexPage.view または ViewPage.view で、 src/Page/*.elm で定義されている各ページの view 関数を呼び出しています。

では各ページを表現している Model をどう変化させているのか。Elm guide > Navigation で説明があったように、URLが変化したタイミングで Msg が受け取れるので、そこで Model を切り替えます。

update 関数を見てみます。


Main.elm

update : Msg -> Model -> ( Model, Cmd Msg )

update message model =
let
env =
toEnv model
in
case ( message, model ) of
( LinkClicked urlRequest, _ ) ->
case urlRequest of
Browser.Internal url ->
case Route.fromUrl url of
Just _ ->
( model, Nav.pushUrl (Env.navKey env) (Url.toString url) )

Nothing ->
( model, Nav.load <| Url.toString url )

Browser.External href ->
if String.length href == 0 then
( model, Cmd.none )

else
( model, Nav.load href )

( UrlChanged url, _ ) ->
changeRouteTo (Route.fromUrl url) model


LinkClicked が画面でリンクをクリックした際に呼び出される Msg です。これは Elm guide 同様 Browser.Navigation.pushUrl または Browser.Navigation.load しているだけです。

UrlChanged が実際に URL が変化した際に呼び出される Msg です。 Route は URL 文字列と各ページの対応表のような役割を持つモデルです。 Route.fromUrl url で URL 文字列から現在のページに対応する Route に変換します。 changeRouteTo 関数の定義は以下。


Main.elm

changeRouteTo : Maybe Route -> Model -> ( Model, Cmd Msg )

changeRouteTo maybeRoute model =
let
env =
toEnv model
in
case maybeRoute of
Nothing ->
( NotFound env, Cmd.none )

Just Route.Index ->
IndexPage.init env
|> updateWith (Index env) GotIndexMsg

Just (Route.View id) ->
ViewPage.init env id
|> updateWith (View env id) GotViewMsg


この関数で Route を受け取って 対応する Model に変換しています。

IndexPage.init または ViewPage.init で、 src/Page/*.elm で定義されている各ページの init 関数を呼び出しています。各ページの init 関数は、そのページごとに定義されている ( Model, Cmd Msg) を返します。 changeRouteTo 関数は最終的に Main で定義している ( Model, Cmd Msg ) を返さないといけないわけですが... updateWith 関数を見てみましょう。


Main.elm

updateWith : (subModel -> Model) -> (subMsg -> Msg) -> ( subModel, Cmd subMsg ) -> ( Model, Cmd Msg )

updateWith toModel toMsg ( subModel, subCmd ) =
( toModel subModel
, Cmd.map toMsg subCmd
)

...簡単に言うと、 各ページの Model (コード中の subModel) と Msg (コード中の subMsg) を、 Main の Model と Msg に変換しています。


Main.elm

type Model =

...
| Index Env IndexPage.Model
| View Env Id ViewPage.Model

type Msg =
...
| GotIndexMsg IndexPage.Msg
| GotViewMsg ViewPage.Msg


Model と Msg が各ページの Model と Msg をラップして保持してくれるわけです。

ちょっと長くなってきたので記事を分けます。

Elm SPA テンプレートを作った 2