elm-spaで温まりシングルページアプリケーションの厳しい寒さを乗り越える

![YAMA_DSC2132_TP_V.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64695/575a41b3-017d-fb3f-a2d7-efdaebafd59f.jpeg)



先月、**[elm-spa](https://github.com/ryannhg/elm-spa)**というツールが[Discourseで発表されて](https://discourse.elm-lang.org/t/elm-spa-a-tool-for-building-single-page-apps/)話題になり[^discourse]、先日筆者が参加したイベントでもこのツールについての紹介がありました。Elmでシングルページアプリケーションを作ると、退屈なボイラープレートを書かされてそれなり面倒なのですが、それを解決してくれる便利なツールのようです。どんなものなのか気になったので、筆者も試してみることにしました。ちなみに、アイキャッチ画像は**平湯温泉**です。

（※野暮と思いつつ恐縮ながらこのネタを解説させて頂くと、シングルページアプリケーションはSPAと略されますが、"spa"とは英語で温泉の意でして、そこから温泉の画像なのであります）

[^discourse]: このトピック、みんなやけにテンション高いな！

# とりあえずインストール

とりあえず`npm install`しました。

```shell-session
$ npm install elm-spa
$ npx elm-spa

usage: elm-spa <command> [...]

commands:

  init [options] <path>      create a new project at <path>

  options:
     --ui=<module>           the module your `view` uses (default: Element)

                             examples:
                             elm-spa init your-project
                             elm-spa init --ui=Element your-project


  build <path>               generate pages and routes

                             examples:
                             elm-spa build .


  add static <module>       create a new static page
      sandbox <module>      create a new sandbox page
      element <module>      create a new element page
      component <module>    create a new component page

                            examples:
                            elm-spa add static AboutUs
                            elm-spa add element Settings.Index


  help                      print this help screen

                            examples:
                            elm-spa help
                            elm-spa wat
                            elm-spa huh?
```

まだドキュメントはほとんどなくREADMEも薄いので、Discourseの投稿などを見ながらの手探りです。CLIのヘルプを見ながら**勘**で動かしてみます。どうやら`elm-spa init`でプロジェクトをスキャフォールディングできるっぽいのでやってみます。

```shell-session
$ npx elm-spa init hello

elm-spa created a new project in:

  ~/test/spa-test/hello

  run these commands to get started:
  cd ~/test/spa-test/hello
  npm start
...
```

それっぽいプロジェクトファイル群が生成されました。起動してみます。

```shell-session
$ cd hello
$ npm start
```

![elm-spa.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64695/e5d32404-8f95-3b48-c0f2-c77fdf9264d8.png)

elm-liveが起動してページが表示されました。ふむふむ。上の`home`と`nowhere`というリンクを踏むとページが切り替わることが確認できます。ちゃんとSPAになっているようです。

# ページを追加してみる

`elm-spa add`というコマンドがあるので使ってみます。おそらくこのコマンドで新たなページを追加するのでしょう。

```
$ npx elm-spa add static kin-iro

  kin-iro doesn't look like an Elm module.

  Here are some examples of what I'm expecting:
  elm-spa add static Example
  elm-spa add static Settings.User

$ npx elm-spa add static KinIro

 elm-spa created 1 file:

  ~\test\spa-test\hello\src\Pages\KinIro.elm
```

指定する名前はモジュール名でなければいけないみたいです。これで`src/Pages/KinIro.elm`というファイルが追加されて、`http://localhost:1234/kin-iro`にアクセスすると`KinIro`と書かれたページが表示されました。なるほどです。

![elm-spa2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64695/d9c99696-6dab-442a-643b-bfbbdaa92047.png)

ページ上部のリンクは、どのページでも共通で表示されるみたいですね。`src/Layout.elm`というファイルにこのあたりの`view`が書いてあるみたいなので、このヘッダ部分をカスタマイズするには`src/Layout.elm`を編集すればいいみたいです。サンプルはelm-uiで書かれているようで、私はちょっと慣れないです。


```elm
viewHeader : Route -> Element msg
viewHeader currentRoute =
    row
        [ spacing 24
        , paddingEach { top = 32, left = 16, right = 16, bottom = 0 }
        , centerX
        , width (fill |> maximum 480)
        ]
        [ viewLink currentRoute ( "home", routes.top )
        , viewLink currentRoute ( "nowhere", routes.notFound )
        , viewLink currentRoute ( "kin-iro", routes.kinIro )   -- これを追加！
        ]
```

![elm-spa3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64695/65da8d33-b6a6-98c5-008c-9d012125f20c.png)

なお、`elm-spa add static`だと、`title`と`view`しかない動きのないページが作成されるみたいです。

```elm:src/Pages/KinIro.elm
page : Page Params.KinIro Model Msg model msg appMsg
page =
    Spa.Page.static
        { title = always "KinIro"
        , view = always view
        }
```

それに対し`elm-spa add element`だと、`init`や`update`、`subscription`も備えた動きのあるページが作れるようになるみたいです。

```shell-session
$ npx elm-spa add element Mosaic
```

```elm:src/Pages/Mosaic.elm
page : Page Params.Mosaic Model Msg model msg appMsg
page =
    Spa.Page.element
        { title = always "Mosaic"
        , init = always init
        , update = always update
        , subscriptions = always subscriptions
        , view = always view
        }
```



ふむふむ。ここまで普通に動かせました。





# どんなファイルが自動生成されているの？

各ページを取りまとめる役目のファイルは、`elm-stuff/.elm-spa/Generated/*.elm`に自動で生成されて配置されているようです。`elm-stuff`に配置されていることからわかるとおり、これらのファイルはユーザは通常は触らないファイルでしょう。たとえば、`elm-stuff/.elm-spa/Generated/Routes.elm`は次のような内容になっていました。ルータが自動的に構成されていることがわかります。モジュール名がケバブケースに変換されたものがパス名として使われるようです。

```elm:Routes.elm
-- ROUTES


type alias Routes =
    { kinIro : Route
    , notFound : Route
    , top : Route
    }


routes : Routes
routes =
    { kinIro =
        Generated.Route.KinIro {}
    , notFound =
        Generated.Route.NotFound {}
    , top =
        Generated.Route.Top {}
    }
 

parsers : List (Parser (Route -> a) a)
parsers =
    [ map routes.kinIro
        (s "kin-iro")
    , map routes.notFound
        (s "not-found")
    , map routes.top
        (top)
    ]
```


# さいごに

ElmでSPAを作ってみた人はみんな同じことを考えるようで、筆者(hiruberuto)も[Alchelmy という似たような目的のツール](https://github.com/aratama/alchelmy)を作ったことがあります。ちょうど[昨年のアドベントカレンダーの記事](https://qiita.com/hiruberuto/items/0b92bca97632bad9a3b8)のネタがまさにこれでした。ツールの目的としてはほとんど同じだと思ってよさそうです。なぜこのようなツールが欲しくなるのかについては、筆者のこの以前の記事が参考になるかと思います。

あえて違いを挙げるとすれば、次のような感じでしょうか。

* elm-spaではモジュール名からパスのパーサが自動生成されており、モジュール名に`Dynamic`というキーワードを含めるとそれがパラメータになるようです。alchelmyではモジュール名とパスは無関係で、ページごとに好きなようにパーサを書けるのでパラメータも普通のSPAと同じように扱えますし、完全に自由です
* Alchelmyではページ間を遷移したときに`Session`というデータを受け渡しができますが、elm-spaではそのような機構がないようです
* elm-spaではグローバルな`update`や`subscription`が指定できるようになっていますが、Alchelmyには各ページごとにしかありません[^sub]
* elm-spaはCLIのほかにElmのライブラリとしても機能を提供します。Alchelmyはライブラリとしては何も提供しません。例えば、elm-spaは`Spa.Transition`というページ遷移時のトランジションを定義する機能を提供しています。alchelmyはその種の機能を提供していません[^transition]
* elm-spaは`Platform.worker`を使ってElmで書かれています。AlchelmyはPureScriptで書かれています

[^sub]: 共通のsubscriptionが欲しければ、どこかでそのsubscriptionを定義して、各ページでその共通のsubscriptionをインポートして使います。一度は筆者もいったんAlchelmyにそのような機能を実装したのですが、シンプルにするためにあえて削りました。

[^transition]: トランジションが不可能というわけではなくて、Alchelmy側で機能を提供するのではなく、アプリ側で自力で実装するようになってます

なんとなくですが、alchelmyはrtfeldman/elm-spa-exampleのようなSPAの仕組みに忠実に従いつつ最小限度だけ拡張するに留めたシンプル寄りのツールであり、それに対してelm-spaは`Spa.Transition`やグローバルな`init`/`update`みたいな付加的な機能を提供している多機能寄りのツールだという感じがします。

この種のツールがどこまでうまくいくのかはまだまだ未知数だと思いますので、興味のある方は使ってみてぜひ知見を共有していただければと思います。

# 参考文献

* [Elm-spa: a tool for building single page apps](https://discourse.elm-lang.org/t/elm-spa-a-tool-for-building-single-page-apps/)
* [豊かすぎる源泉を出し惜しみなく注ぎ込み平湯館の岩露天風呂](https://www.pakutaso.com/20170308087post-10780.html) アイキャッチ画像。行ったことはないんですが写真が綺麗だったので
* [aratama/alchelmy](https://github.com/aratama/alchelmy) 手前味噌
* [複数ページのSPAをElmで書くときのボイラープレート部分をalchelmyで自動的に†錬成†する](https://qiita.com/hiruberuto/items/0b92bca97632bad9a3b8) 手前味噌２