ElmでPhoenixSocketを使用したカウンターを作成する(準備編)

はじめに

クライアント側に Elm を用いて、サーバー側のPhoenix.Channelと連携する方法を記述します。
サーバーには、Phoenix LiveViewによる動的サーバーサイドレンダリング2で作成したものをそのまま利用します。
題材としては、上記ページと同じようにカウンターを作成します。
クライアントとサーバー間は、WebSocket を用いて通信を行います。

また、ElmとPhoenixのバージョンはそれぞれ以下です。

項目バージョン
Phoenix1.4.10
Elm0.19

本テーマは、以下の3回に分けて記述します。

  1. ElmでPhoenixSocketを使用したカウンターを作成する(準備編) <- 本記事
  2. ElmでPhoenixSocketを使用したカウンターを作成する(JavaScript編)
  3. ElmでPhoenixSocketを使用したカウンターを作成する(Elm編)

Elmとは

Elm を簡単に説明します。
ElmはJavaScriptにコンパイルされる関数型プログラミング言語です。
以下の特徴を持っています。

  • 関数型言語である
  • Elm 内では副作用を生じるコードは書けない
  • コンパイルされてクライアントサイドで動く
  • 強力な型検査がある
  • コンパイルが通ればランタイム時の例外なし
  • JavaScript と相互運用を行う仕組みがある

言語の雰囲気はAn Introduction to Elmを見ていただくとわかると思います。
こちらには、クライアントのみで動くカウンターが載っています。
このサーバーと連携しないバージョンのカウンターを用いて Elm の概要を説明します。
※文法自体の説明は行いません

まず、カウンターを Elm で表現すると以下のようになります。

module Main exposing (main)

import Browser
import Html exposing (Html, button, div, text)
import Html.Events exposing (onClick)

type alias Model =
    { count : Int }

initialModel : Model
initialModel =
    { count = 0 }

type Msg
    = Increment
    | Decrement

update : Msg -> Model -> Model
update msg model =
    case msg of
        Increment ->
            { model | count = model.count + 1 }

        Decrement ->
            { model | count = model.count - 1 }

view : Model -> Html Msg
view model =
    div []
        [ button [ onClick Increment ] [ text "+1" ]
        , div [] [ text <| String.fromInt model.count ]
        , button [ onClick Decrement ] [ text "-1" ]
        ]

main : Program () Model Msg
main =
    Browser.sandbox
        { init = initialModel
        , view = view
        , update = update
        }

ポイントは以下です。

  • Modelはページの状態を表す
  • update関数で状態更新に関する定義を行う
  • Msg型はアプリケーションで起こるイベントを定義する
  • view関数でDOMの定義を行う
  • Elm Runtimeが全てを結びつける

上記ポイント踏まえ、「ページ表示後ボタンがクリックされた」というケースをシーケンス図に表すと以下のようになります。

viewElm RuntimeupdateModelHtml Msgbutton clickedHtml MsgMsg ModelModel'Model'Html' Msg'merge real DOM & virtual DOMviewElm Runtimeupdate

これを The Elm Architecture と呼んでいます。

このことにより、開発者は以下の事に集中できます。

  • view では与えられた modelHtml に変換する定義を行う
  • update は状態の更新をする定義を行う

このようなシンプルな仕組みでWebアプリケーションが構築できます。
規模が大きくなってもこの仕組みは変わりません。
また、viewが呼ばれる度にDOMを丸ごと再構築しているように見えますが、実際はそうではありません。
viewが返したDOMは、VirtualDOMと呼ばれるメモリ上のDOMに書き出され、ブラウザで表示されるDOMにはその差分が適用されます。
これが Elm の概要です。

手順

それではPhoenix上でElmのカウンターを作る手順を以下に記します。

  1. Phoenix環境へElmの設定
  2. Elmを用いてのクライアントページの作成
  3. Elmとサーバーをつなぐライブラリの作成

Phoenix環境へElmの設定

今までは asset の生成に brunch を用いていましたが、Phoenix1.4 から webpack に変更になりました。
そのため、webpackElm をセットアップします。
更にElmで構築したページを表示するための入り口を作ります。

Elmとサーバーをつなぐライブラリの作成

Elm のバージョン0.19では良さそうな WebSocket 用パッケージを見つけることができませんでした。
というわけで、この部分を自作することにします。
自作するライブラリに関しては、複数のプロジェクトで使用できるように意識しながら構築していきます。
このプロジェクトでの実装は必要最小限にして、他のプロジェクトで機能が不足した場合にその時点で追加するようにします。
どのような機能を実装するかに関しては、「Phoenix LiveViewによる動的サーバーサイドレンダリング2」を踏襲します。

Elmを用いてのクライアントページの作成

WebSocketでサーバーと通信を行い、サーバーが状態を持つタイプのカウンターを作ります。
これは、「Phoenix LiveViewによる動的サーバーサイドレンダリング2」のときと同じものです。
違いは、クライアント側をElmで構築するところです。
ポイントとしては以下のとおり。

  • Elmを使用してのページ構築法
  • Portを用いたPhoenix.Channelsとの連携方法

Phoenix環境へのElmの設定

Phoenixでクライアント側をElmで開発するための設定を行います。
以下は全てassetsディレクトリ下で作業します。

Elm用作業ディレクトリの作成

elmディレクトリを作成し、elm.jsonファイルを生成します。

# assets/
$ mkdir elm && cd elm && elm init

srcディレクトリとelm.jsonファイルが作成されます。

srcディレクトリにMain.elmを作成します。
現在のディレクトリ構成は以下になります。

elm
├── elm.json
└── src
    └── Main.elm

ここにクライアント側のコードを記述します。

とりあえずは、動作確認用にダミーのページを作成します。

-- assets/elm/src/Main.elm
module Main exposing (main)
import Html exposing (text)
main =
    text "Hello Elm and Phoenix!"

WebPackプラグインのインストール

Elmのソースコードからトランスパイルできるようにするための環境を作ります。
まずは、elm-web-pack-loader をインストールします。

# assets/
$ npm i --save-dev elm-webpack-loader

webpack.config.js にてパッケージを使用するための設定を行います。
作業ディレクトリは elm.json のあるディレクトリを指定します。

// assets/webpack.config.js
module: {
  rules: [
    ...
  {
      test: /\.elm$/,
      exclude: [/elm-stuff/, /node_modules/],
      use: {
          loader: 'elm-webpack-loader',
          options: {
              cwd: __dirname + '/elm'
          }
      }
  },
    ...
  ]
},

Elm用ページの作成

Phoenix上で実装してます。 /elm-counter が指定されたらElmで作成したページが表示されるようにします。

Router

いつものようにRouterから追加します。

# lib/lv_demo_web/router.ex
scope "/", LvDemoWeb do
  pipe_through :browser
  ...
  get "/elm-counter", ElmCounterController, :index  ...
end

コントローラーの実装

コントローラーを実装します。 Router で指定した、ElmCounterController を作成します。

# /lib/lv_demo_web/controllers/elm-counter-controller.ex
defmodule LvDemoWeb.ElmCounterController do
  use LvDemoWeb, :controller

  def index(conn, _params) do
    render(conn, "index.html", %{val: 0})
  end
end

ViewとTemplateの追加

View を作成します。

# lib/lv_demo_web/views/elm_counter_view.ex
defmodule LvDemoWeb.ElmCounterView do
  use LvDemoWeb, :view
end

クライアント側の要素は Elm で構築するので、Elm 用にIDを指定したdiv要素を一つだけ作ります。

# lib/lv_demo_web/templates/elm_counter/index.html.eex
<div id="elm-main"></div>

この要素に Elm で記述した内容が表示されます。

これで Elm 開発の準備は完了しました。

おわりに

ここまででセットアップは完了しました。
これから JavaScript 側のライブラリと Elm によるページ構築を行っていくことになります。

つづきはこちらへどうぞ。
Elmの方を見たいという方はこちらへ。