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

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 をラップして保持してくれるわけです。

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

コメント

このブログの人気の投稿

投稿時間:2021-06-20 02:06:12 RSSフィード2021-06-20 02:00 分まとめ(3871件)

投稿時間:2021-04-30 23:37:32 RSSフィード2021-04-30 23:00 分まとめ(42件)

投稿時間:2023-02-05 02:09:04 RSSフィード2023-02-05 02:00 分まとめ(9件)