JavaScript
Elixir
brunch
Elm
Phoenix

Phoenix+Elmでマルチページ

 Phoenix+Elmでマルチページを実現するための枠組みの基本を考えてみました。SPA(シングルページアプリケーション)ではなくマルチページです。Brunchのなじみが浅い私の個人的な備忘録なので、もっとベターな方法があると思いますが、ご指摘いただければ幸いです。

1. ページ設計

 まず今回の実験のページ設計ですが、以下のように3枚のページ(html)を用意するつもりです。それぞれのパスでアクセスすれば、elmプログラムを含んだページを表示します。目標はシンプルです。

"/"           -- トップページ (Elm無し)
"/page1"      -- ページ1(Hello.elm)
"/page2"      -- ページ2(MyButtons.elm)

2. Plug.Staticの設定

 それではプロジェクトを作成します。

mix phx.new multi_pages --no-ecto

 後で述べますが、Elmプログラムをコンパイルした結果のjsファイルはテンプレートファイルから以下のようにして直接ロードします。

<script src="/js/mybuttons.js"></script>

 そのためPhoenixの設定でstaticファイルを読み込む設定を変えます。onlyをコメントアウトします。許可ファイルを追加しても良いですが、ここではコメントアウトしpriv/staticにあるファイルを全てpublicなものにします。PhoenixでStatic Fileをサーブする - Qiita

lib/multi_pages_web/endpoint.ex
#
  plug Plug.Static,
    at: "/", from: :multi_pages, gzip: false
    # only: ~w(css fonts images js favicon.ico robots.txt)
#

3. routerの設定

上の「1.ページ設計」で示した通りにrouter.exを変更します。

lib/multi_pages_web/router.ex
#
  scope "/", MultiPagesWeb do
    pipe_through :browser # Use the default browser stack
    get "/", PageController, :index
    get "/page1", Page1Controller, :hello
    get "/page2", Page2Controller, :buttons
  end
#

4. トップページの設定

 最初にトップページを変更します。ヘッダーに他のページ(page1, page2)へのリンクを貼ります。デフォルトのレイアウト(app.html.eex)を変更します。

lib/multi_pages_web/templates/layout/app.html.eex
#
      <p class="alert alert-info" role="alert"><%= get_flash(@conn, :info) %></p>
      <p class="alert alert-danger" role="alert"><%= get_flash(@conn, :error) %></p>
      <a href="/page1">ページ1</a> /
      <a href="/page2">ページ2</a>

      <main role="main">
        <%= render @view_module, @view_template, assigns %>
      </main>

    </div> <!-- /container -->

  </body>
</html>

 またこの時以下の行を削除しておきます。トップページではjsやelmは使わないので不要です。

    <script src="<%= static_path(@conn, "/js/app.js") %>"></script>

 またトップページのテンプレートは変更しません。
lib/multi_pages_web/templates/page/index.html.eex

 controllerもviewもデフォルトのままで変更しません。

 以上でトップページは終わりです。

5. ページ1の設定

 ページ1を追加していきます。controllerを以下のように定義します。今回は実験ですのでレイアウトも変更しています。

lib/multi_pages_web/controllers/page1_controller.ex
defmodule MultiPagesWeb.Page1Controller do
  use MultiPagesWeb, :controller

  def hello(conn, _params) do
    conn
    |> put_layout("layout_hello.html")
    |> render("hello.html")
  end
end

 viewも定義します。中身はデフォルトのままです。

lib/multi_pages_web/views/page1_view.ex
defmodule MultiPagesWeb.Page1View do
  use MultiPagesWeb, :view
end

 テンプレートを追加するので、ディレクトリを作成します。

mkdir lib/multi_pages_web/templates/page1

 テンプレートを追加します。elmプログラムのためのプレースフォルダを設定し、elmプログラムを明示的に指定していることに注意してください。まあ、通常のelmの設定ですね。

lib/multi_pages_web/templates/page1/hello.html.eex
<h1>Page1</h1>
<h3>Hello page</h3>

<div id="elm-hello-area"></div>
<script src="/js/vendor/hello.js"></script>
<script>
    Elm.Hello.embed(document.getElementById("elm-hello-area"));
</script>

 レイアウトはトップページのものをコピーし、リンクを以下のように変更します。

lib/multi_pages_web/templates/layout/layout_hello.html.eex
#
      <a href="/">トップページ</a> /
      <a href="/page2">ページ2</a>
#

 以上でページ1の設定は終わりです。

6. ページ2の設定

 ページ2の設定は、ページ1とほぼ同じです。ファイル名やモジュール名、関数名などが異なるだけです。

 controllerです。

lib/multi_pages_web/controllers/page2_controller.ex
defmodule MultiPagesWeb.Page2Controller do
  use MultiPagesWeb, :controller

  def buttons(conn, _params) do
    conn
    |> put_layout("layout_buttons.html")
    |> render("buttons.html")
  end
end

 viewです。

lib/multi_pages_web/views/page2_view.ex
defmodule MultiPagesWeb.Page2View do
  use MultiPagesWeb, :view
end

 テンプレートディレクトリを作成します。

mkdir lib/multi_pages_web/templates/page2

 テンプレートです。

lib/multi_pages_web/templates/page2/buttons.html.eex
<h1>Page2</h1>
<h3>My Buttons page</h3>

<div id="elm-buttons-area"></div>
<script src="/js/vendor/mybuttons.js"></script>
<script>
    Elm.MyButtons.embed(document.getElementById("elm-buttons-area"));
</script>

 レイアウトです。

lib/multi_pages_web/templates/layout/layout_buttons.html.eex
#
      <a href="/">トップページ</a> /
      <a href="/page1">ページ1</a>
#

 以上でページ2の設定を終わります。

7. Elmクライアントの設定

 Brunchの設定をしていきます。

cd assets/
npm install --save-dev elm-brunch

 brunch-config.jsを変更します。今回の肝の部分です。

assets/brunch-config.js
#
  // Phoenix paths configuration
  paths: {
    // Dependencies and current project directories to watch
    // (1)"elm"を追加
    watched: ["static", "css", "js", "elm", "vendor"],
    // Where to compile files to
    public: "../priv/static"
  },

  // Configure your plugins
  plugins: {
    babel: {
      // Do not use ES6 compiler in vendor code
      ignore: [/vendor/]
    },
    // (2)elmBrunchを追加
    elmBrunch: {
      elmFolder: "elm",
      mainModules: ["Hello.elm", "MyButtons.elm"], // Elmプログラム
      outputFolder: "../static/js/vendor"  // Elmの実行形をstaticに吐き出す
    }
  },
#

(※重要)outputFolderにはvendorサブディレクトリが指定されていることに注意してください。brunchにおいてはvendorにあるfileが優先的にロードされるようです。つまりElmのjsファイルが安全にロードされます。単にoutputFolder を "../static/js"と vendor抜きで指定すると、かなりの確率でElmのjsファイルのロードに失敗してしまいます。

 今回は2つのelmプログラムを使います。page1でHello.elmを、page2でMyButtons.elmを走らせます。elmプログラムをmainModulesで指定し、assets/static/jsにコンパイルするように指定しました。brunchはassets/static/ 下のファイルををそのままpriv/static/にコピーします。priv/static/は「2.Plug.Staticの設定」での設定によりpublicとなっており、テンプレートから以下のように指定することができます。

<script src="/js/vendor/hello.js"></script>
<script src="/js/vendor/mybuttons.js"></script>

 次にassets/static/jsディレクトリとasstes/elmディレクトリを作成し、必要なelmパッケージをインストールしておきます。

mkdir static/js
mkdir elm
cd elm
elm-package install elm-lang/html

 Hello.elmがpage1で走らせるelmプログラムです。

assets/elm/Hello.elm
module Hello exposing (..)
import Html exposing (text)

main =
  text "Hello, World!"

 MyButtons.elmがpage2で走らせるelmプログラムです。これはMyButtonsBody.elmをimportしています。動作を確認するためにわざとElmプログラムを分割してみました。そもそもelmコンパイラは、複数の分割ファイルをリンクしてくれますので、この点ではbrunchに頼る必要がありません。複数ファイルを一つの実行形にまとめるのはelmコンパイラであり、brunchではありません。ちなみにbrunchがまとめたapp.jsファイルは使わないで無視します。

assets/elm/MyButtons.elm
module MyButtons exposing (..)

import Html
import MyButtonsBody exposing (model, update, view)

main =
  Html.beginnerProgram { model = model, view = view, update = update }

 MyButtons.elmはMyButtonsBody.elmをimportしています。

assets/elm/MyButtonsBody.elm
module MyButtonsBody exposing (model, update, view)

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

-- MODEL
type alias Model = Int

model : Model
model =
  0

-- UPDATE
type Msg = Increment | Decrement

update : Msg -> Model -> Model
update msg model =
  case msg of
    Increment ->
      model + 1

    Decrement ->
      model - 1

-- VIEW
view : Model -> Html Msg
view model =
  div []
    [ button [ onClick Decrement ] [ text "-" ]
    , div [] [ text (toString model) ]
    , button [ onClick Increment ] [ text "+" ]
    ]

 以上でクライアント側のElmの設定は終わりです。

8. プログラムの実行

 トップページです。レイアウトを変更してページ1とページ2のリンクを設けました。

image.png

 ページ1です。リンクを変更しました。「Hello world!」はelmプログラムが走った結果です。

image.png

 ページ2です。リンクを変更しました。カウンターを増減させるボタンが表示されています。これもelmプログラムです。

image.png

 以上です。