Chapter 06

リクエストライフサイクル

koga1020
koga1020
2021.11.24に更新

リクエストライフサイクル

前提: このガイドでは、入門ガイドの内容を理解し、Phoenixアプリケーションを起動していることを前提としています。

このガイドの目的は、Phoenixのリクエストのライフサイクルについて話すことです。このガイドでは、Phoenixプロジェクトに2つの新しいページを追加し、その過程でどのようにしてピースが組み合わされていくのかをコメントするという、実践的なアプローチで学びます。

それでは、最初の新しいPhoenixのページから始めていきましょう!

新しいページを追加する

ブラウザがhttp://localhost:4000/にアクセスすると、そのアドレス上で動作しているサービス、この場合は私たちのPhoenixアプリケーションにHTTPリクエストを送信します。HTTPリクエストは動詞とパスで構成されています。たとえば、以下のブラウザのリクエストは次のように変換されます。

ブラウザのアドレスバー 動詞 パス
http://localhost:4000/ GET /
http://localhost:4000/hello GET /hello
http://localhost:4000/hello/world GET /hello/world

他にもHTTP動詞があります。たとえば、フォームを送信する際には通常POST動詞を使用します。

Webアプリケーションは通常、各動詞/パスのペアをアプリケーションの特定の部分にマッピングすることでリクエストを処理します。Phoenixのこのマッチングはルーターによって行われます。たとえば、"/articles" をすべての記事を表示するアプリケーションの一部にマッピングできます。したがって、新しいページを追加するために、最初のタスクは新しいルートを追加することです。

新しいルート

ルーターは、固有のHTTP動詞/パスのペアを、それらを処理するコントローラー/アクションのペアにマッピングします。Phoenixのコントローラーは単純にElixirモジュールです。アクションは、これらのコントローラー内で定義された関数です。

Phoenixは新しいアプリケーションでは、lib/hello_web/router.ex にルーターファイルを生成してくれます。このセクションではここで作業を行います。

前回の起動ガイドの "Welcome to Phoenix!" のページのルートは次の通りです。

get "/", PageController, :index

このルートが伝えていることを順に理解していきましょう。http://localhost:4000/にアクセスすると、ルートパスへのHTTP GET リクエストが発行されます。このようなリクエストはすべて、lib/hello_web/controllers/page_controller.ex で定義されている HelloWeb.PageController モジュールの index/2 関数で処理されます。

これから作成するページは、ブラウザをhttp://localhost:4000/helloに向けると、"Hello World, from Phoenix!" を返します。

そのページを作成するために、最初にそのページのルートを定義する必要があります。テキストエディターで lib/hello_web/router.ex を開いてみましょう。新しいアプリケーションの場合、次のようになります。

defmodule HelloWeb.Router do
  use HelloWeb, :router

  pipeline :browser do
    plug :accepts, ["html"]
    plug :fetch_session
    plug :fetch_live_flash
    plug :put_root_layout, {HelloWeb.LayoutView, :root}
    plug :protect_from_forgery
    plug :put_secure_browser_headers
  end

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

  scope "/", HelloWeb do
    pipe_through :browser

    get "/", PageController, :index
  end

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

  # ...
end

今のところ、ここではパイプラインと scope の使用は無視して、ルートを追加することに焦点を当てることにします。これらについてはルーティングガイドで説明します。

/hello への GET リクエストを、ルーターの scope "/" do ブロック内にあり、じきに作成する HelloWeb.HelloControllerindex アクションにマップする新しいルートをルーターに追加してみましょう。

scope "/", HelloWeb do
  pipe_through :browser

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

新しいコントローラー

コントローラーはElixirのモジュールで、アクションはその中で定義されたElixirの関数です。アクションの目的は、データを収集し、レンダリングに必要なタスクを実行することです。設定したルートでは、index/2 関数を持つ HelloWeb.HelloController モジュールが必要だと指定しています。

これを実現するために、lib/hello_web/controllers/hello_controller.ex というファイルを新規に作成して、次のようにしてみましょう。

defmodule HelloWeb.HelloController do
  use HelloWeb, :controller

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

use HelloWeb, :controller についての議論は、コントローラーガイドのために取っておくことにします。とりあえず、index のアクションに注目してみましょう。

すべてのコントローラーのアクションは2つの引数をとります。1つ目は conn で、リクエストに関する大量のデータを保持する構造体です。2つ目は params で、これはリクエストのパラメーターです。ここでは params を使用しておらず、先頭に _ のprefixを記述することでコンパイラの警告を回避しています。

このアクションの中核は render(conn, "index.html") です。これはPhoenixに "index.html" をレンダリングするように指示します。レンダリングを担当するモジュールはビューと呼ばれます。デフォルトでは、Phoenixのビューはコントローラーの名前が付けられているので、Phoenixは HelloWeb.HelloView が存在し、"index.html" を処理してくれることを期待しています。

注意: アトムをテンプレート名として使用すると、render(conn, :index) も動作します。これらの場合、テンプレートはAcceptヘッダーに基づいて選択されます。

新しいビュー

Phoenixのビューは、プレゼンテーションレイヤーとして機能します。たとえば、"index.html" をレンダリングしたときの出力は、完全なHTMLページになることを期待しています。実装を楽にするために、これらのHTMLページを作成するためにテンプレートを使用することがよくあります。

新しいビューを作成してみましょう。lib/hello_web/views/hello_view.ex を作成し、次のようにします。

defmodule HelloWeb.HelloView do
  use HelloWeb, :view
end

このビューにテンプレートを追加するには、lib/hello_web/templates/hello ディレクトリにファイルを追加する必要があります。コントローラー名(HelloController)、ビュー名(HelloView)、テンプレートディレクトリ(hello)はすべて同じ命名規則にしたがっており、それぞれにちなんで命名されていることに注意してください。

テンプレートファイルは NAME.FORMAT.TEMPLATING_LANGUAGE という構造になっています。ここでは、 lib/hello_web/templates/hello/index.html.heexindex.html.heex というファイルを作成します。".heex" は HTML+EEx の略で、 EEx はElixir自体の一部として組み込まれている、Elixirを埋め込むためのライブラリです。"HTML+EEx "は、HTMLを意識したEExのPhoenix拡張で、HTMLのバリデーション、コンポーネント、値の自動エスケープをサポートしています。後者はクロスサイトスクリプティングのようなセキュリティ上の脆弱性から、余分な作業をすることなく保護します。

lib/hello_web/templates/hello/index.html.heex を作成し、次のようにします。

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

これで、ルート、コントローラー、ビュー、テンプレートができたので、ブラウザをhttp://localhost:4000/helloに向けて、Phoenixからの挨拶を見ることができるはずです!(途中でサーバーを停止してしまった場合、サーバーを再起動するタスクは mix phx.server です。)

今行ったことについて、いくつか興味深いことがあります。これらの変更を行っている間、サーバーを停止したり再起動したりする必要はありませんでした。そう、Phoenixにはホットコードのリロード機能があります!また、index.html.heex ファイルは単一の div タグだけで構成されていましたが、得られるページは完全なHTMLドキュメントです。インデックステンプレートはアプリケーションのレイアウト lib/hello_web/templates/layout/app.html.heex にレンダリングされます。これを開くと、次のような行が表示されます。

<%= @inner_content %>

これは、HTMLがブラウザへ送信される前にレイアウトにテンプレートを注入します。

ホットコードのリロードについての注意点: 自動リンターを搭載しているエディタによっては、ホットコードのリロードが動作しない場合があります。それがうまくいかない場合は、この問題の議論を参照してください。

エンドポイントからビューへ

最初のページを構築していくうちに、リクエストのライフサイクルがどのようにまとめられているかを理解することができました。では、より全体的に見てみましょう。

すべてのHTTPリクエストはアプリケーションのエンドポイントから始まります。エンドポイントは lib/hello_web/endpoint.ex の中にある HelloWeb.Endpoint というモジュールで見つけることができます。エンドポイントファイルを開くと、ルーターと同じように、エンドポイントが plug をたくさん呼び出していることがわかるでしょう。Plug はウェブアプリケーションをつなぎ合わせるためのライブラリであり仕様です。これはPhoenixがどのようにリクエストを処理するかの重要な部分であり、詳細についてはプラグガイドを参照してください。

今のところ、各plugはリクエスト処理の断片を定義しているだけと言えば十分です。エンドポイントの中には、およそこのようなスケルトンがあります。

defmodule HelloWeb.Endpoint do
  use Phoenix.Endpoint, otp_app: :hello

  plug Plug.Static, ...
  plug Plug.RequestId
  plug Plug.Telemetry, ...
  plug Plug.Parsers, ...
  plug Plug.MethodOverride
  plug Plug.Head
  plug Plug.Session, ...
  plug HelloWeb.Router
end

これらのプラグのそれぞれには、後ほど説明する特定の責務があります。最後のプラグは HelloWeb.Router モジュールです。これにより、エンドポイントはさらに先のすべてのリクエスト処理をルーターに委譲できます。今知っているように、このモジュールの主な役割は、動詞とパスのペアをコントローラーにマッピングすることです。コントローラーはビューにテンプレートをレンダリングするように指示します。

この時点では、単にページをレンダリングするために多くのステップが必要だと思うかもしれません。しかし、アプリケーションが複雑になるにつれて、それぞれのレイヤーが異なる目的を果たすことがわかります。

  • エンドポイント (Phoenix.Endpoint) - エンドポイントには、すべてのリクエストが通過する共通の初期パスが含まれます。すべてのリクエストに何かを実行させたい場合は、エンドポイントに記述します

  • ルーター (Phoenix.Router) - ルーターはコントローラーへの動詞/パスのディスパッチを担当します。ルーターは機能をスコープすることもできます。たとえば、アプリケーションの中にはユーザー認証が必要なページもあれば、そうでないページもあります。

  • コントローラー (Phoenix.Controller) - コントローラーの仕事は、リクエスト情報を取得し、ビジネスドメインと対話し、プレゼンテーション層のデータを準備することです。

  • ビュー (Phoenix.View) - ビューはコントローラーからの構造化データを処理し、それをユーザーに表示するためのプレゼンテーションに変換します。

最後の3つのコンポーネントがどのように機能するのか、別のページを追加して簡単に復習してみましょう。

別の新しいページ

アプリケーションに少し複雑さを加えてみましょう。新しいページを追加して、URLの一部を認識し、それを "messenger" としてラベルを付け、コントローラーを介してテンプレートに渡し、メッセンジャーがこんにちはと言えるようにします。

前回と同じく、まずは新しいルートを作成します。

別の新しいルート

今回は、先ほど作成した HelloController を再利用して、新しい show アクションを追加します。最後のルートのすぐ下に次のような行を追加します。

scope "/", HelloWeb do
  pipe_through :browser

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

パスの中で :messenger 構文を使用していることに注意してください。PhoenixはURLのその位置にある値をすべて受け取り、それをパラメーターに変換します。たとえば、ブラウザで http://localhost:4000/hello/Frank を指すと、"messenger" の値は "Frank" になります。

別の新しいアクション

新しいルートへのリクエストは HelloWeb.HelloControllershow アクションで処理されます。すでに lib/hello_web/controllers/hello_controller.ex にコントローラーがあるので、このファイルを編集して show アクションを追加するだけです。今回は、パラメーターからメッセンジャーを抽出してテンプレートに渡す必要があります。そのために、このshow関数をコントローラーに追加します。

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

show アクションのボディ内では、レンダー関数に第3引数を渡します。ここでは :messenger がキーで、変数 messenger が値として渡されます。

アクションの本体が、バインドされたメッセンジャー変数に加えてparams変数にバインドされたパラメーターのフルマップにアクセスする必要がある場合、次のように show/2 を定義できます。

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

params マップのキーは常に文字列であり、等号は代入を表すものではなく、代わりにパターンマッチのアサーションであることを覚えておくと良いでしょう。

別の新しいテンプレート

このパズルの最後のピースとして、新しいテンプレートが必要です。これは HelloControllershow アクション用なので、lib/hello_web/templates/hello ディレクトリにある show.html.heex という名前になります。メッセンジャーの名前を表示する必要があることを除けば、見た目は驚くほど index.html.heex テンプレートと似ています。

そのために、Elixir式を実行するための特別なEExタグ <%= %> を使用します。最初のタグには、<%= のような等号が付いていることに注意してください。 これは、これらのタグの間を通過するElixirコードはすべて実行され、結果として得られる値がタグを置き換えることを意味します。等号がない場合でも、コードは実行されますが、その値はページに表示されません。

そして、テンプレートは次のようになります。

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

メッセンジャーは @messenger という名前で表示されます。コントローラーからビューに渡された値を "assigns" と呼びます。これは、assigns.messenger の略で、メタプログラムされた特殊な構文です。その結果、見栄えが良くなり、テンプレートでの作業が格段に楽になりました。

これで終わりです。ブラウザをhttp://localhost:4000/hello/Frankに向けると、このようなページが表示されるはずです。

image

少し遊んでみてください。/hello/ の後につけたものが、あなたのメッセンジャーとしてページに表示されます。