Elm

複数のElmアプリで小さなデータを共有する - Local storage

 シングルページでなく複数ページでそれぞれ別のElmアプリが動作していて、小さなデータを共有したいケースを考えましょう。例えばトップページのApp.elmでログインしてサーバからJWTトークンを取得している場合、それをページ2のApp2.elmで使いたい場合はどうすればいいでしょうか。多分JavaScript(Html5)のLocal storageを使うのが一番簡単でしょう。

 Local storageには、保存したデータに関して、特に有効期限はなく、ブラウザを閉じても喪失することはなく、永続的に利用できる利点があります。もちろんJWT tokenを保存するときは、token自体の有効期限があるので、それに応じて明示的に更新(削除)する必要があるでしょう。またLocal storageの保存データサイズが5MBと比較的大きいのももう一つの利点でしょう。

 以下、今回はJWTトークンでなく、単にuser_id文字列を渡すサンプルコードを考えます。

1.ページ設計

"/"           -- トップページ (App.elm, index.html)
"/page2"      -- ページ2(App2.elm, index2.html)

以下がApp.elmで取得したデータを、Local storageを通して、App2.elmに渡す流れです。

        port                            port
App.elm  -->  JavaScript(Local storage)  -->   App2.elm
        userId                          userId

2.App.elm - Local storageに書く

 App.elmでは入力フォームから入力されたuserId文字列をLocalStorageに保存します。そのためにportでJavaScriptのコードにアクセスします。

2-1.ports設計

 (key,value)のタプルを渡して、JavaScriptにLocalStorageに保存するよう依頼します。

ports
#
-- OUTGOING PORT
port portSetLocalStorage : (String, String) -> Cmd msg
#

2-2.App.elm全コード

 以下がApp.elmの全コードです。

App.elm
port module App exposing (..)

import Html exposing (..)
import Html.Attributes exposing (..)
import Html.Events exposing (onClick, onInput)

-- OUTGOING PORT
port portSetLocalStorage : (String, String) -> Cmd msg


init : ( Model, Cmd Msg )
init =
    (Model "1111") ! []

main : Program Never Model Msg
main =
    Html.program
        { init = init
        , view = view
        , update = update
        , subscriptions = \_ -> Sub.none
        }

type Msg
  = CacheUserId
  | ChangeUserId String

type alias Model =
    {  userId : String
    }

update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
  case msg of
    CacheUserId ->
      ( model
      , portSetLocalStorage ("userId", model.userId)
      )
    ChangeUserId userId ->
      { model | userId=userId } ! []

view : Model -> Html Msg
view model =
  div []
    [ input [ value model.userId, onInput ChangeUserId ] []
    , button [ onClick CacheUserId ] [ text "Cache userId!" ]
    , div [] [ text <| "current user id = " ++ model.userId ]
    ]

2-3.index.html

 以下がindex.htmlです。タプル(key,value)で渡された引数は、req[]配列で受け取れます。

index.html
<!doctype html>
<html>
    <head>
    </head>
    <body>
        <a href="index2.html">index2</a>
        <div id="elm-area"></div>
        <script src="app.js"></script>
        <script>
            const app = Elm.App.embed(document.getElementById("elm-area"));
            app.ports.portSetLocalStorage.subscribe( (req) => {
                localStorage.setItem(req[0],req[1]);
            } )
        </script>
    </body>
</html>

3.App2.elm - Local storageを読む

 App2.elmではLocal storageの値を読み取ります。

3-1.ports設計

portGetLocalStorageにkey文字列を渡して、valueを取得してくれるように依頼します。返り値はportResLocalStorageを通して行われます。(String, String)で受け取ることを宣言しているので、JavaScript側では配列で渡しますが、タプルに自動変換されます。

ports
-- OUTGOING PORT
port portGetLocalStorage : String -> Cmd msg

-- INCOMING PORT
port portResLocalStorage : ((String, String) -> msg) -> Sub msg

3-3.App2.elm全コード

 以下がApp2.elmの全コードです。

App2.elm
port module App2 exposing (..)

import Html exposing (..)
import Html.Attributes exposing (..)
import Html.Events exposing (onClick, onInput)


-- OUTGOING PORT
port portGetLocalStorage : String -> Cmd msg

-- INCOMING PORT
port portResLocalStorage : ((String, String) -> msg) -> Sub msg



init : ( Model, Cmd Msg )
init =
    (Model "2222") ! []

main : Program Never Model Msg
main =
    Html.program
        { init = init
        , view = view
        , update = update
        , subscriptions = subscriptions
        }

type Msg
  = GetCacheUserId
  | Receive (String, String)

type alias Model =
    {  userId : String
    }

update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
  case msg of
    GetCacheUserId ->
      ( model
      , portGetLocalStorage "userId"
      )
    Receive  (_, val) ->
      {model | userId=val} ! []

view : Model -> Html Msg
view model =
  div []
    [ button [ onClick GetCacheUserId ] [ text "Get Cache userId!" ]
    , div [] [ text <| "current user id = " ++ model.userId ]
    ]

subscriptions : Model -> Sub Msg
subscriptions model =
  portResLocalStorage Receive

3-4.index2.html

 app2.ports.portResLocalStorage.send(res)のresは配列であることに注意してください。

index2.html
<!doctype html>
<html>
    <head>
    </head>
    <body>
        <a href="index.html">top</a>
        <div id="elm-area"></div>
        <script src="app2.js"></script>
        <script>
            const app2 = Elm.App2.embed(document.getElementById("elm-area"));
            app2.ports.portGetLocalStorage.subscribe( (key) => {
            const val = localStorage.getItem(key);
            res = [key, val];
            app2.ports.portResLocalStorage.send(res);
            } )
        </script>
    </body>
</html>

4.プログラムの実行

 適当なディレクトリを作成します。以下のようにして必要なパッケージをインストールします。

elm-package install elm-lang/html

 コンパイルとサーバの実行は以下の通りです。

elm-make App.elm --output app.js
elm-make App2.elm --output app2.js
elm-reactor -a=www.mypress.jp -p=3030

ブラウザでindex.htmlを開いてください。以下のようになっています。「13579」と入力してボタンを押します。入力欄の数字をLocalStorageに書き込みます。

image.png

 次にindex2.htmlを開いてボタンを押してください。LocalStorageの数字を読み込み表示します。

image.png

 これでApp.elmとApp2.elmでuserIdを共有させることができました。さて、これからですね。

(補足)まさに今回のテーマの仕事を行うパッケージがいくつかありました。今回のように自分で書くよりはコード量を少なくできるようですが、パッケージを使う環境を整えるのにそれ以上の労力が必要に思えました。自分でportを使って書いた方が、トータル的に考えてよいと思われます。