Open4

elm-spa V6を試す

ABAB↑↓BAABAB↑↓BA

挙動を確かめるためにまっさらな状態からプロジェクトを始める

$ mkdir elm-spa-example
$ cd elm-spa-example
$ npm init
// エンター連打
$  npm i -D elm-spa@latest

こんな感じの状態

 cat package.json
{
  "name": "elm-spa-example",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "elm-spa": "^6.0.4"
  }
}
ABAB↑↓BAABAB↑↓BA

プロジェクトを始めるときは、以下のコマンドを叩く

$ npx elm-spa new
// yes

主に以下が生えた

  • public/index.html
  • src/Pages/Home_.elm
  • elm.json

index.htmtlは、Elmのファイルがコンパイルされたt結果の、elm.jsっとElm.Main.init を読み込んでる

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body>
  <script src="/dist/elm.js"></script>
  <script> Elm.Main.init() </script>
</body>
</html>

src/Pages/Home_.elm は、一旦割愛

elm.json, elm-spaがライブラリとして入った 他、.elm-spaがsource-directoriesに入ったが、そのファイルは現時点では出来ていない

{
    "type": "application",
    "source-directories": [
        "src",
        ".elm-spa/defaults",
        ".elm-spa/generated"
    ],
    "elm-version": "0.19.1",
    "dependencies": {
        "direct": {
            "elm/browser": "1.0.2",
            "elm/core": "1.0.5",
            "elm/html": "1.0.0",
            "elm/json": "1.1.3",
            "elm/url": "1.0.0",
            "ryannhg/elm-spa": "6.0.0"
        },
        "indirect": {
            "elm/time": "1.0.0",
            "elm/virtual-dom": "1.0.2"
        }
    },
    "test-dependencies": {
        "direct": {},
        "indirect": {}
    }
}
ABAB↑↓BAABAB↑↓BA

以下のコマンドでサーバを立ち上げると

$ npx elm-spa server
// http://localhost:1234/

.elm-spaディレクトリが出来上がる。プリロードなコアライブラリと言ったところ

ちなみに、elm-spa gen でも出来上がる

ABAB↑↓BAABAB↑↓BA

./elm-spa/defaults/Main.elm の中を見てみる

main関数は、当たり前だけど、SPAの普段の中身と一緒。
Flagsは、defaults/Shared.elm に切り出されている。

main : Program Shared.Flags Model Msg
main =
    Browser.application
        { init = init
        , update = update
        , view = view
        , subscriptions = subscriptions
        , onUrlChange = ChangedUrl
        , onUrlRequest = ClickedLink
        }

Modelは、Pages.Model ./elm-spa/generated/Gen/Pages.elm
さらには、 ./elm-spa/generated/Gen/Model.elm に切り出されている

type alias Model =
    { url : Url
    , key : Key
    , shared : Shared.Model
    , page : Pages.Model
    }

生成されたファイルで、各ページごとのParamsがカスタムタイプで定義されている

type Model
    = Redirecting_
    | Home_ Gen.Params.Home_.Params
    | NotFound Gen.Params.NotFound.Params

initはSharedと各ページごとにinitを呼び出してマージしている。
v6からの新しい概念で各ページのCmdはeffectというもので抽象化されて、MainではじめてCmdに変換される。

init : Shared.Flags -> Url -> Key -> ( Model, Cmd Msg )
init flags url key =
    let
        ( shared, sharedCmd ) =
            Shared.init (Request.create () url key) flags

        ( page, effect ) =
            Pages.init (Route.fromUrl url) shared url key
    in
    ( Model url key shared page
    , Cmd.batch
        [ Cmd.map Shared sharedCmd
        , Effect.toCmd ( Shared, Page ) effect
        ]
    )

Msgはいつものセット + Shared + PageごとのMsg

type Msg
    = ChangedUrl Url
    | ClickedLink Browser.UrlRequest
    | Shared Shared.Msg
    | Page Pages.Msg

updateもいつものセット + Shared + Pageごとのupdate呼び出し。effectからCmdに変換さている

...

  Shared sharedMsg ->
            let
                ( shared, sharedCmd ) =
                    Shared.update (Request.create () model.url model.key) sharedMsg model.shared

                ( page, effect ) =
                    Pages.init (Route.fromUrl model.url) shared model.url model.key
            in
            if page == Gen.Model.Redirecting_ then
                ( { model | shared = shared, page = page }
                , Cmd.batch
                    [ Cmd.map Shared sharedCmd
                    , Effect.toCmd ( Shared, Page ) effect
                    ]
                )

            else
                ( { model | shared = shared }
                , Cmd.map Shared sharedCmd
                )

        Page pageMsg ->
            let
                ( page, effect ) =
                    Pages.update pageMsg model.page model.shared model.url model.key
            in
            ( { model | page = page }
            , Effect.toCmd ( Shared, Page ) effect
            )

viewはBrowser.Documentを抽象化した型で扱われていて、各ページのViewをView.toBrowserDocumentで最後型合わせしている感じ

view : Model -> Browser.Document Msg
view model =
    Pages.view model.page model.shared model.url model.key
        |> View.map Page
        |> View.toBrowserDocument
type alias View msg =
    { title : String
    , body : List (Html msg)
    }

subscriptionも特に変なところなし。

subscriptions : Model -> Sub Msg
subscriptions model =
    Sub.batch
        [ Pages.subscriptions model.page model.shared model.url model.key |> Sub.map Page
        , Shared.subscr