技術メモ

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

Phoenix入門 (第8章 Views その2)

f:id:ysmn_deus:20190219130922p:plain

どうも、靖宗です。 前回のViewsの続きです。

The ErrorView

PhoenixにはErrorViewというビューが生成されています。Controllersの2つめの記事で保留にしたやつです。
ファイルはlib/hello_phoenix_web/views/error_view.exが該当します。

defmodule HelloPhoenixWeb.ErrorView do
  use HelloPhoenixWeb, :view

  def template_not_found(template, _assigns) do
    Phoenix.Controller.status_message_from_template(template)
  end
end

ドキュメントとは少々異なりますが無視して進みます。
試しに404のページを叩いてみたい訳ですが、開発モードで存在しないページを参照するとデバッグメッセージが出てしまいます。
なので一旦デバッグメッセージを出ないようにします。config/dev.exsを編集します。

use Mix.Config

config :hello_phoenix, HelloPhoenixWeb.Endpoint,
  http: [port: 4000],
  debug_errors: false, # ここ
  code_reloader: true,
...

再起動してhttp://localhost:4000/such/a/wrong/pathなど、適当に存在しないURLに飛びます。
するときちんとNot Foundと表示されました。どうやらデフォルトではプレーンテキストを返す設定になっているようです。
カスタマイズするにはErrorViewにrender関数を追記してやればいいようです。試しに文言を変えます。

def render("404.html", _assigns) do
  "Page not found"
end

変わりました。
仕組み的には、おそらくルーティング外のリクエストが来たらエンドポイント上でエラーをレンダリングするようです。
Phoenix.Endpoint.RenderErrorsというモジュールでrender/5という関数が定義されており、そこでHelloPhoenixWeb.ErrorViewが利用されているようです。

このErrorViewも普通のビューのようなものなのですが、テンプレートのディレクトlib/hello_phoenix_web/templates/errorが存在しません。
エラーページをカスタマイズするにはこのディレクトリを作成し、404.html.eexを作成する必要があるそうです。作って見ましょう。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta name="description" content="">
    <meta name="author" content="">

    <title>Welcome to Phoenix!</title>
    <link rel="stylesheet" href="/css/app.css">
  </head>

  <body>
    <div class="container">
      <div class="header">
        <ul class="nav nav-pills pull-right">
          <li><a href="https://hexdocs.pm/phoenix/overview.html">Get Started</a></li>
        </ul>
        <span class="logo"></span>
      </div>

      <div class="phx-hero">
        <p>Sorry, the page you are looking for does not exist.</p>
      </div>

      <div class="footer">
        <p><a href="http://phoenixframework.org">phoenixframework.org</a></p>
      </div>

    </div> <!-- /container -->
    <script src="/js/app.js"></script>
  </body>
</html>

もはや普通のHTMLです。
今のままではまだPage not foundレンダリングされるだけなので、def render("404.html", _assigns) doの箇所を削除します。

defmodule HelloPhoenixWeb.ErrorView do
  use HelloPhoenixWeb, :view

  def template_not_found(template, _assigns) do
    Phoenix.Controller.status_message_from_template(template)
  end
end

何も書かないで良さそうです。この辺はPhoenix.Controller.status_message_from_template(template)がよしなにしてくれるようです。
なので、ステータスコード毎にエラーページを作成したい場合は、単純にlib/hello_phoenix_web/templates/errorを作成してエラーコードのテンプレートを放り込んでおくだけで良さそうです。

Rendering JSON

ビューでレンダリングするのはHTMLだけじゃないよ!ってお話です。
コントローラでjson/2なんて方法もありましたが、ビュー側でJSONやその他のフォーマット(CSVXML?)もレンダリングできるみたいです。

なにやらJasonというオープンソースを利用しているようで、基本的にマップからJSONレンダリングをするようです。直感的でよさそう。

コントローラでjson/2することでビューをスキップしてJSONレンダリングできますが、これではビューとコントローラの役割分担が曖昧になってしまうので、基本的にはビュー側で取り扱うのがいいそうです。僕もそう思います。

早速サンプルを見ていきましょう。まずはコントローラ側を編集します。lib/hello_phoenix_web/controllers/page_controller.ex

defmodule HelloPhoenixWeb.PageController do
  use HelloPhoenixWeb, :controller

  def show(conn, _params) do
    page = %{title: "foo"}

    render(conn, "show.json", page: page)
  end

  def index(conn, _params) do
    pages = [%{title: "foo"}, %{title: "bar"}]

    render(conn, "index.json", pages: pages)
  end
end

ほぼ同じです。レンダリングするテンプレートの指定をshow.jsonのようにjsonを指定してあげるぐらいです。
続きましてはビューの方。lib/hello_phoenix_web/views/page_view.exです。

defmodule HelloPhoenixWeb.PageView do
  use HelloPhoenixWeb, :view

  def render("index.json", %{pages: pages}) do
    %{data: render_many(pages, HelloWeb.PageView, "page.json")}
  end

  def render("show.json", %{page: page}) do
    %{data: render_one(page, HelloWeb.PageView, "page.json")}
  end

  def render("page.json", %{page: page}) do
    %{title: page.title}
  end
end

コントローラ側でrender(conn, "hoge.json", マップ)を実行すると、ビュー側のrender("hoge.json", マップ)が呼び出される仕組みです。

ここでrender_many/3render_one/3という怪しげな関数が出てきています。
これはどうやら第二引数にモジュール、第三引数に該当するrender関数の第一引数(今回で言う"page.json"を引数にとるrender関数)を指定すると、第一引数の情報を利用して別のレンダリング関数を利用したレンダリング情報を自身のレスポンスに組み込める関数のようです。
文章ではなかなか伝わりにくいと思いますのでサンプルを見ていきましょう。まず

  def render("page.json", %{page: page}) do
    %{title: page.title}
  end

は純粋にJSON情報を返すだけのレンダリング関数です。例えば、page%{title: "hoge"}が渡されてこいつが呼び出されると

{
    "title": "foo"
}

というJSONレンダリングされます。これを踏まえて

  def render("index.json", %{pages: pages}) do
    %{data: render_many(pages, HelloWeb.PageView, "page.json")}
  end

はざっくり言えば

{
    "data": ## render_many(pages, HelloWeb.PageView, "page.json")の実行結果 ##
}

という形になるはずです。
render_many(pages, HelloWeb.PageView, "page.json")render("page.json", %{page: page})を第一引数のpagesの要素数だけ実行されるようで、いわば

Enum.map(pages, fn page -> render("page.json", page) end)

と同じ様な働きのようです。よって、render("index.json", %{pages: pages})の実行結果は

{
  "data": [
    {
     "title": "foo"
    },
    {
     "title": "bar"
    },
 ]
}

となります。

同じ様に

  def render("show.json", %{page: page}) do
    %{data: render_one(page, HelloWeb.PageView, "page.json")}
  end

{
    "data": ## render_one(page, HelloWeb.PageView, "page.json")の実行結果 ##
}

となり、このrender_one/3の実行結果はrender("page.json", %{page: page})のマップの部分にそのままpageを渡した

{
  "data": {
    "title": "foo"
  }
}

となります。
一見しちめんどくさい処理の様に感じますが、pageのモデルが変わった際などには修正がしやすくミスが少ない仕組みのように思えます。
構造化されているレンダリング対象から、データベースのリレーションを意識してビューを作る際などはこれらをうまく活用すると生産性が上がりそうです。

例えば一つのページに対して著者が複数いる多対一の関係性であれば、下記のように書けます。

defmodule HelloPhoenixWeb.PageView do
  use HelloPhoenixWeb, :view
  alias HelloPhoenixWeb.AuthorView

  def render("page_with_authors.json", %{page: page}) do
    %{title: page.title,
      authors: render_many(page.authors, AuthorView, "author.json")}
  end

  def render("page.json", %{page: page}) do
    %{title: page.title}
  end
end

AuthorViewの想定ですがおそらく

  def render("author.json", %{author: author}) do
    %{author: author.name}
  end

といったところでしょう。別のビューからレンダリング関数を呼び出してくるなど拡張性は高そうです。
もし、AuthorViewの引数のパターンマッチが%{writer: writer}であれば

def render("page_with_authors.json", %{page: page}) do
  %{title: page.title,
    authors: render_many(page.authors, AuthorView, "author.json", as: :writer)}
end

ともできるそうです。

わりとRendering JSONの項目は、PhoenixAPIアプリケーションとして利用することを想定しているならマストな気がします。
とはいえPhoenixフレームワーク自体かなり分かりやすく設計されているので、さらっと流して分からなければ再読という感じでいい気がします。
マクロでブラックボックス化されている箇所が多い気がしますが、そこさえ許容してしまえば可読性と生産性を両立した便利なフレームワークなのではないでしょうか。