どうも、靖宗です。 前回の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やその他のフォーマット(CSVやXML?)もレンダリングできるみたいです。
なにやら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/3
とrender_one/3
という怪しげな関数が出てきています。
これはどうやら第二引数にモジュール、第三引数に該当するrender
関数の第一引数(今回で言う"page.json"
を引数にとるrender関数)を指定すると、第一引数の情報を利用して別のレンダリング関数を利用したレンダリング情報を自身のレスポンスに組み込める関数のようです。
文章ではなかなか伝わりにくいと思いますのでサンプルを見ていきましょう。まず
def render("page.json", %{page: page}) do %{title: page.title} end
は純粋にJSON情報を返すだけのレンダリング関数です。例えば、page
に%{title: "hoge"}
が渡されてこいつが呼び出されると
{ "title": "foo" }
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
の項目は、PhoenixをAPIアプリケーションとして利用することを想定しているならマストな気がします。
とはいえPhoenixフレームワーク自体かなり分かりやすく設計されているので、さらっと流して分からなければ再読という感じでいい気がします。
マクロでブラックボックス化されている箇所が多い気がしますが、そこさえ許容してしまえば可読性と生産性を両立した便利なフレームワークなのではないでしょうか。