Help us understand the problem. What is going on with this article?

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

More than 1 year has passed since last update.

作った
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

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away