技術メモ

プログラミングとか電子工作とか

Phoenix入門 (第3章 Adding Pages)

f:id:ysmn_deus:20190219130922p:plain

どうも、靖宗です。
今回は「Adding Pages」ということで、単純に訳してページを追加していく感じでしょう。
という事で基礎的なルーティング(どんなURLで何を表示するか)もおそらくこの章で触れることになるでしょう。

概要

この項目はざっくり何がどこにあるのか説明されているものなのでよくわかんなくても「あーそういうことね、完全に理解した(分かってない)」程度で十分だと思います。いっぱい使ってれば慣れるはず・・・

まず、Phoenixでプロジェクトを生成した時には下記のようなディレクトリ構造になっています。

├── _build
├── assets
├── config
├── deps
├── lib
│   └── hello_phoenix
│   └── hello_phoenix_web
│   └── hello_phoenix.ex
│   └── hello_phoenix_web.ex
├── priv
├── test

動的ファイル

今回関係してくるのはlib/hello_phoenix_webで、この中は更に様々なファイルが生成されています。

├── channels
│   └── user_socket.ex
├── controllers
│   └── page_controller.ex
├── templates
│   ├── layout
│   │   └── app.html.eex
│   └── page
│       └── index.html.eex
└── views
│   ├── error_helpers.ex
│   ├── error_view.ex
│   ├── layout_view.ex
│   └── page_view.ex
├── endpoint.ex
├── gettext.ex
├── router.ex

最初のハローワールドページを生成していたのは、このうちcontrollerstemplatesviewsの3つのようです。たぶんここら辺をいじったり真似て追加したりしていきます。

静的ファイル

静的ファイルはプロジェクトディレクトリ直下にあるassetsにあります。
ReactとかVue.jsとか入れるならここでしょう。

├── assets
│   ├── css
│   │   └── app.css
│   ├── js
│   │   └── app.js
│   └── static
│   └── node_modules
│   └── vendor

Webの表示などにかかわらないファイル

ウェブの表示などにかかわらないファイル(たとえば、IoTサーバーで温度センサーから温度を取得してDBに記録する、など?)はlib/hello_phoenix/application.exなどに記載していくようです。

A New Route

さてここからが本題です。
あるURLにアクセスしたときに何が表示されるかはルーティングによって決まります。
このルーティングはlib/hello_phoenix_web/router.exに記載されています。

defmodule HelloPhoenixWeb.Router do
  use HelloPhoenixWeb, :router

  pipeline :browser do
    plug :accepts, ["html"]
    plug :fetch_session
    plug :fetch_flash
    plug :protect_from_forgery
    plug :put_secure_browser_headers
  end

  pipeline :api do
    plug :accepts, ["json"]
  end

  scope "/", HelloPhoenixWeb do
    pipe_through :browser

    get "/", PageController, :index
  end

  # Other scopes may use custom stacks.
  # scope "/api", HelloPhoenixWeb do
  #   pipe_through :api
  # end
end

✌('ω')。o(????????????)
まるで今までのElixir入門編をあざ笑うかのごとく、通常の記法ではなくゴリゴリのマクロです。
該当箇所は

  scope "/", HelloPhoenixWeb do
    pipe_through :browser

    get "/", PageController, :index
  end

だと思われますが、scopeなんてしりません。内部実装を知りたい方はPhoenixのリポジトリにscopeが定義されてますのでそちらを参照するのもアリだと思います。
これを思うと、OSSコミッター的ななにかを目指さない限りは「こういうものだ」と割り切って使う方がいい気がしてきました。

めげずに進みますと、おそらくGETメソッドのレスポンスはPageController:indexを返すぜ!って流れだと思います。PageControllerlib/hello_phoenix_web/controllers/page_controller.exに記載されてます。
詳しいルーティングは次回に回すとして、ここではhttp://localhost:4000/helloというページを追加するためにlib/hello_phoenix_web/router.exを修正します。

  scope "/", HelloPhoenixWeb do
    pipe_through :browser

    get "/", PageController, :index
    get "/hello", HelloController, :index
  end

scopeのブロックを増やすのかと思いきやgetを追記していく形でよさそうです。
同じ:indexを返しているようですが、コントローラーが異なっています(HelloController)。

A New Controller

コントローラーは返すページなどのモジュールです。レンダリングと、たぶんデータベースのやりとりとかもココに書くのでしょう。
PageControllerがあるディレクトリにHelloControllerを作って行きます。というかコピペして名前を変えてしまいましょう。
lib/hello_phoenix_web/controllers/hello_controller.exを作成します。

defmodule HelloPhoenixWeb.HelloController do
  use HelloPhoenixWeb, :controller

  def index(conn, _params) do
    render(conn, "index.html")
  end
end

詳しいことは別のタイミングでやります。兎に角いまは付け焼き刃でガンガン実装していきます。
ここのindex/2は、一個目の引数がおそらく接続に必要な情報(ヘッダとか?)で、_paramsがPOSTパラメータなどでしょう。今は特にパラメータが渡されることは無い筈なので、変数名に_が着いてます。

ここで、重要な箇所のrender/2という関数ですが、この関数はテンプレートを呼び出してHTMLデータを作成する関数のようです。
PhoenixはこのHelloControllerという名前のController以前の箇所名を参照してテンプレートを取得するようです。
なのでlib/hello_phoenix_web/templates/helloフォルダを作成し、index.html.eexを作成します。lib/hello_phoenix_web/templates/pageの物と同様でいいとおもいます。

A New View

コントローラーからデータを受け取ってレンダリング前に加工するのにビューが用いられるそうです。
例えば、苗字と名前のデータがそれぞれ別にあって、フルネームがレンダリングされる場合はフルネーム=苗字+名前の処理はビューでやるそうです。
(この辺は宗教な気はしますが)
ただし、どのみちビューのファイルは必要なようです。lib/hello_phoenix_web/views/hello_view.exを作成します。

defmodule HelloPhoenixWeb.HelloView do
  use HelloPhoenixWeb, :view
end

Phoenixは結構名前にうるさいようです。基本モジュール名はキャメルケース(CamelCase)で、ファイル名はスネークケース(snake_case)にするようです。そしてコントローラー名をHogeControllerにした時、ビューはHogeViewにするという、ある程度命名規則が決まっているようです。
不自由に見えて実はこの辺はフレームワークの仕様として決めてしまえば明示的に誰が見ても機能が保証される気がするのでいい気がします。

A New Template

lib/hello_phoenix_web/templates/hello/index.html.eexHelloControllerrenderレンダリングされます。
まったく同じ物というのでは全体像がつかみにくいのでやはり編集します。

<div class="phx-hero">
  <h2>Hello World, from Phoenix!</h2>
</div>

シンプル!
完全なHTMLではなく、一部が挿入されるようです。
とりあえずコレで大体が完了したようなのでhttp://localhost:4000/helloにアクセスしてチェックします。
mix phx.serverを終了していなければ、追加変更は検知してコンパイルしてくれるそうです。(地味に便利!)
特に何もせずにアクセスしてみましょう。

f:id:ysmn_deus:20190225224611p:plain

どうやらヘッダ部分(Phoenix Frameworkと書いてある箇所)からして共通のテンプレートがありそうです。
lib/hello_phoenix_web/templates/layout/app.html.eexがこのテンプレートに該当するようです。
このapp.html.eex

<%= render(@view_module, @view_template, assigns) %>

という箇所に先ほど修正したテンプレートが入るという流れのようです。
これで一応荒削りながら新しいページを追加することができました。

Another New Page

とはいえもう少しWebアプリケーションっぽくしたいところです。
そこで、URLから動的にページをレンダリングさせてみます。

A New Route

基本的には先ほどの流れとほぼ同様。ルーティングファイルを修正していきます。
lib/hello_phoenix_web/router.exに追記します。

...
    get "/hello", HelloController, :index
    get "/hello/:messenger", HelloController, :show # この行を追記
...

URLにアトムを使用すると、その箇所がマップとしてコントローラーに渡されるようです。
この場合、http://localhost:4000/hello/Frankとアクセスすると、引数のマップに"messenger"というキーで"Frank"が取り出せるマップが渡されます。

A New Action

追記した箇所は先ほどと同じHelloControllerを使用しているのでHelloControllershow関数を追記します。
lib/hello_phoenix_web/controllers/hello_controller.exを修正します。

...
  def show(conn, %{"messenger" => messenger}) do
    render(conn, "show.html", messenger: messenger)
  end
...

index_paramsだった箇所にマップの型が入ってます。これは関数が呼び出されたときにパターンマッチでmessengerという変数を抽出しています。
もし、paramsの他の値が必要になる場合は

  def show(conn, %{"messenger" => messenger} = params) do
...
  end

としておけば良いそうです。
極力セーフな感じに設計するならパターンマッチで抽出してる方がきちんとエラー処理できそうです。

A New Template

ここまでくれば後はテンプレートを追加するだけです。
(ビューは特に何もしてないのでlib/hello_phoenix_web/views/hello_view.exが存在していればOK)
lib/hello_phoenix_web/templates/hello/show.html.eexを作成していきます。

<div class="phx-hero">
  <h2>Hello World, from <%= @messenger %>!</h2>
</div>

<%= @messenger %>の箇所はEEx独自のタグだそうで、レンダリングする時に変数などを代入できます。

特に間違ってなければ、この時点で既にhttp://localhost:4000/hello/hogehogeでアクセスできる筈です。

f:id:ysmn_deus:20190225231355p:plain

命名規則が若干ヤヤコシイですが、慣れればどうということは無さそうです。