💭

elm-spa v5 -> v6 に移行ガイド

2022/04/24に公開

elm-spaは、ElmでSPAをするためのボイラプレートを生成してくれる、とても便利なツールです。v5からv6で変更点がいくつかあり、びっくりしてしまう部分があります。しかし、よくよく見ていくと、そこまで大きく内容が変わっているというよりも機能やモジュールがまとめられたり命名が変わっているだけだったりするので、マッピングがしっかりできていればそこまで驚くことはありません。この記事では移行の手順と大まかな変更点が伝えればなと思います。

この記事では、以下の順番で移行の手順をお伝えできればと思います。既に手元にv5のプロジェクトがある場合、いきなり書き換えるのではなく、新規プロジェクトを立ち上げて、v6の構造を把握したり、新しいPagesの仕組みを把握してから書き換えをした方がスムーズに行くように感じました。

  • 設定
  • elm-spaコマンド
  • ソースコード

設定の変更点

elm-spa v6用の設定変更点を説明していきます。これらは、手で変更する必要はなく、elm-spaを導入すれば、elm-spa initコマンドで設定をしてくれます。特に新規でプロジェクトを立ち上げる場合は、そちらをご利用下さい。

package.json

elm-spaは自動生成をするためのコマンドライブラリとelmのライブラリの2つがあります。コマンドライブラリは、通常のnpmプロジェクトと同じ、package.jsonにあるので、6系のライブラリにアップグレードしましょう。

"elm-spa": "5.0.4"

"elm-spa": "6.0.4"

.gitignore

elm-spaはSPA用の自動生成ファイルをディレクトリにまとめて運用をします。v5では、src配下にElmのメインプロジェクトファイルとして生成されていましたが、プロジェクト直下の.elm-spaファイルに生成されることになりました。

src/Spa/Generated

.elm-spa

elm.json

Elmプロジェクトを単に作った場合、srcディレクトリだけがビルドの対象となります。しかし、内部ライブラリなどの用途で任意のディレクトリをビルド対象にすることができます。v6では、.elm-spa/defaults, .elm-spa/generatedの2つのディレクトリが新たにビルドの対象となります。これは、先ほど.gitignoreに書いてあった、.elm-spaディレクトリの中身に当たります。こう書かれた場合、ビルド時には、すべてsrcディレクトリ配下にある状態だと思ってもらえれば大丈夫です。また、.elm-spa/defaults下のファイルはsrcに移動することで上書きが可能です。(generatedも可能ですが、推奨はされていません)

また、v6からElmライブラリとしても、elm-spaが提供されるようになったので、必ずinstallするようにしましょう。

{
  ...
  "source-directories": [
    "src"
  ],
  "dependencies": {
	...
    },
    ...

{
  ...
  "source-directories": [
    "src",
    ".elm-spa/defaults",
    ".elm-spa/generated"
  ],
  "dependencies": {
      ...
      "ryannhg/elm-spa": "6.0.0"
    },

elm-spaコマンドの変更点

v5では対話形式でルーティングファイルの種類を選択し、Pages直下にファイルを生成していましたが、v6ではコマンドを一度で打ち切る形に変更されました。しかし、ルーティングからモジュール名に脳内変換して入力をしていましたが、v6ではルーティングを直接書けば良いので、より直感的になりました。

npx elm-spa add
? What kind of page? › - Use arrow-keys. Return to submit.
    static
    sandbox
    element
❯   application - Needs read-write access to Shared.Model
✔ What's the module name? … UserId_String.Posts.PostId_String
✔ Added a new application page at:
...[Project Path]/src/Pages/UserId_String/Posts/PostId_String.elm

npx elm-spa add /:userId/posts/:postId element
New page created at:
[Project Path]/src/Pages/UserId_/Posts/PostId_.elm

ページの種類が、static, sandbox, elementは変わりませんが、applicationは、advancedとなったのでご注意ください。また、ここで使われるテンプレートは、templatesディレクトリ以下に用意されており、あらかじめ、どのようなコードが生成されるかわかるため、不安な方は事前にご確認ください。

 tree .elm-spa/templates
.elm-spa/templates
├── advanced.elm
├── element.elm
├── sandbox.elm
└── static.elm

elm-spa add コマンドでPagesファイルを生成した場合、Params等の自動生成モジュールはこの時点では作られないため、コンパイルエラーが発生します。npx elm-spa servernpx elm-spa buildなどのコマンドを叩くことで生成はされますが、もっと手っ取り早く.elm-spaファイルを生成するには、npx elm-spa genコマンドを叩くと良いでしょう。

ソースコードの変更点

Pagesモジュールファイルの命名

これは、コマンドの変更点でお気づきかもしれませんが、URLの変数部分にUserId_Stringのような形でStringがついていましたが、実用的にString一択のためか、Stringに統一された上で、変数名_のように、型名は省略されるようになりました。

[Project Path]/src/Pages/UserId_String/Posts/PostId_String.elm

[Project Path]/src/Pages/UserId_/Posts/PostId_.elm

自動生成やライブラリのimport

Spaモジュールが撤廃されて、elm.jsonsource-directoriesに従って、importがシンプルになりました。また、Document -> View, Url -> Requestのように命名が変わり、中の機能が少しだけ変更になりました。差分については、ソースコードを見比べたり、記事一番下の関連記事などをご覧ください。

import Spa.Document exposing (Document)
import Spa.Page as Page exposing (Page)
import Spa.Url exposing (Url)
import Shared

import View exposing (View)
import Page
import Request
import Shared

Document -> Viewの差分は以下のような形です。特に関数の中身には変更がありません。

view : Model -> Document Msg
view : Model -> View Msg

Request

RequestはUrlの代わりとなる型ですが、少しだけ複雑です。主にPagesファイル配下では、Request.With Paramsと言う型で使われることが多いはずです。Request.Withは、以下のようなレコードのaliasです。

type alias With params =
    ElmSpa.Request Route params

Paramsは、Pagesモジュールでは以下のようにimportされているはずです。

import Gen.Params.UserId_.Posts.PostId_ exposing (Params)

Paramsの中を見ると、以下のようなレコードです。ルーティングで定義した変数が取得できるようになっているわけですね。

type alias Params =
    { userId : String, postId : String }

改めて、ElmSpa.Requestの定義を見てみましょう。Request.With Params`とは、HTTPリクエストに関わるものを一つにまとめた型であると言うことがわかりました。

{ url : Url
, key : Key
, route : route
, params : params
, query : Dict String String
}

例えば、現在/:userId/posts/:postIdと言うルーティングにいて、ブログ投稿を削除するための、/:userId/posts/:postId/deleteと言うページに遷移するようなCmd Msgをupdateで返すようなシチュエーションを考えてみましょう。すると、Requestを利用して、以下のように書くことができます。Gen.Routeなどは自動生成されたもので、補完が効くのでご安心ください。

update : Request.With Params -> Msg -> Model -> ( Model, Cmd Msg )
update req msg model =
...
   GoDeletePost ->
     ( model, 
       Request.pushRoute                                       
        (Gen.Route.UserId___Posts___PostId___Delete
         { userId = params.userId, postId = params.postId }
        )
        req
     )

Ports

v6からPagesモジュールはPorts Moduleとなることが禁止になりました。そのため、もしport関数を定義したい場合には、別モジュールで定義をしてそれをimportするような形にすることでこの問題を回避することができます。Pagesと対応したいため、私は以下のようPortsモジュールを作り、Pagesに対応させてimportすることにしました。

port module Pages.UserId_String.Posts.PostId_String

port hoge: ~~~
port foo: ~~~

module Pages.UserId_.Posts.PostId_

import Ports.UserId_.Posts.PostId_ exposing (..)
...
module Ports.UserId_.Posts.PostId_ exposing (..)

port hoge:~~~
port foo:~~~

Shared State と ユーザ認証

こちらは、差分が多いため、一番下の関連記事をご覧ください。

ごく簡単に説明すると、application(advanced)のときのsave, load関数が消え、Effectモジュールとpages関数の引数にSharedとRequestが渡されるようになりました。

関連記事

elm-spa v6関連の記事を他にも書いているので、良ければご覧ください。

https://zenn.dev/ababup1192/articles/2c534ec01f0a67

https://zenn.dev/ababup1192/articles/a4634829e8884f

Discussion