技術メモ

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

Phoenix入門 (第4章 Routing その3)

f:id:ysmn_deus:20190219130922p:plain

どうも、靖宗です。
Plugの説明の記事を挟みましたが、引きつづぎRoutingの章を進めて行きます。

Pipelines

スルーしてきたpipe_through :browserの説明です。

Plugのスタックってパイプラインに似てると思わへんか?とのこと。確かにrouter.ex

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

は、接続情報をリレーして行ってる感じなので

    conn
    |> plug :accepts, ["html"]
    |> plug :fetch_session
    |> plug :fetch_flash
    |> plug :protect_from_forgery
    |> plug :put_secure_browser_headers

みたいに見えなくも無いです。
名前からしてもイメージ的にはパイプラインなのでしょう。

作成したファイルにデフォルトで記載されているpipe_through:browser:apiがあります。が、この説明をしていくまえにEndpoint plugsの解説が必要なようです。

The Endpoint Plugs

Endpointsは全Plugに放り込まれる共通の処理を担当しており、ルーティングされる前にこの共通処理を行うPlugのようです。
主に担当している機能は下記の通り。

結構な量がありますが、確かにHTTPリクエストの度にこれだけの処理は要る気はします。
その辺をEndpointsがよしなにしてくれているのでしょう。ありがたや。

The :browser and :api Pipelines

さて、ここに来てようやく:browser:apiのパイプラインの説明です。
先ほどのEndpointsに加えて、ブラウザで表示する場合に必要なPlugとAPIとして活用するときに必要なPlugが別れるのでしょう。

  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

pipelineというマクロを使ってパイプラインを定義していくようです。
その後、利用するスコープ(scope "/", HelloPhoenixWeb ...など)でpipe_through パイプライン名という形でパイプラインを呼び出しているようです。
結局この辺の処理もcaseなどでネストしないで済むような工夫な気はしますね。

:browserパイプラインは下記のPlugの処理が走ります。

  • plug :accepts, ["html"] リクエストのフォーマットがHTMLのやつのみ受入
  • plug :fetch_session セッションデータ作成(EndpointのPlug.Sessionで準備したやつ)
  • plug :fetch_flash Flashメッセージの取り出し
  • plug :protect_from_forgery 下の奴とセットでCSRF対策?
  • plug :put_secure_browser_headers 

一方:apiパイプラインは一個だけです。

  • plug :accepts, ["json"] JSON形式のやつだけ受入

パイプラインはscopeマクロでスコープされている中だけに適応されますが、そもそもscopeマクロがない場合は全てのリクエスト処理に適応されるそうです。
また、ネストしたスコープの中で使う場合は、親スコープでpipe_throughにて適応されたパイプラインはネストした中にも影響を及ぼすそうです。これは直感的なので「せやな」ってところでしょう。

そんなことよりコードで理解しろ!ってことでサンプルを載せてくれてます。Phoenixプロジェクトを作成した直後のrouter.ex:apiのスコープを追記した感じです。

defmodule HelloWeb.Router do
  use HelloWeb, :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 "/", HelloWeb do
    pipe_through :browser

    get "/", PageController, :index
  end

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

    resources "/reviews", ReviewController
  end
end

Phoenixはリクエストを受け取るとまずEndpointにリクエストを送るそうです。これは上記でも説明してますね。
次にどのPlugやパイプラインに渡すかはscopeによって判定されているようです。複数scopeがある場合は上からチェック。

ということで一番最初のscopeで判定されますが、http://localhost:400/にアクセスすれば/がマッチします。そのscopeではpipe_through :browserと書いてあるので:browserパイプラインを通過します。
処理の内容は上記で説明したとおり、pipeline :browserで書いたPlugが実行されます。そのあとで、PageController:indexが実行される、という流れです。

同様にhttp://localhost:400/api/hogehogeなど、scope "/api"にマッチするリクエストはpipe_through :apiの記載がありますので:apiパイプラインを通過します。

scope無しのパイプライン

もしAPIの挙動が要らなく、ブラウザでの表示のみを想定している場合は、scope無しで下記のように書けます。

defmodule HelloWeb.Router do
  use HelloWeb, :router

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

  pipe_through :browser

  get "/", HelloWeb.PageController, :index

  resources "/reviews", HelloWeb.ReviewController
end

scopeがない場合は地に書かれたマクロが適応されるので、せやな。といったところです。
ただし、Phoenixはどちらかと言えばビューのみを担当することが無さそうなのであまりこの書き方はしない気がします。

複数のパイプライン

上記の例では:browser:apiの二者択一でしたが、両方適応したいときとかはどうするんでしょうか?並べる?

defmodule HelloWeb.Router do
  use HelloWeb, :router

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

  scope "/reviews" do
    pipe_through [:browser, :review_checks, :other_great_stuff]

    resources "/", HelloWeb.ReviewController
  end
end

pipe_throughにパイプライン名のリストを投げれば良いようです。

defmodule HelloWeb.Router do
  use HelloWeb, :router

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

  scope "/", HelloWeb do
    pipe_through :browser

    resources "/posts", PostController
  end

  scope "/reviews", HelloWeb do
    pipe_through [:browser, :review_checks]

    resources "/", ReviewController
  end
end

同じコントローラー使ってても認証とかチェックとか挟むときはscopeかき分けた方がいいかも。

Creating New Pipelines

パイプラインの作り方。もうサンプルで散々出てきてるので大丈夫そう。
pipelineマクロを使います。

defmodule HelloWeb.Router do
  use HelloWeb, :router

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

  pipeline :review_checks do
    plug :ensure_authenticated_user
    plug :ensure_user_owns_review
  end

  scope "/reviews", HelloWeb do
    pipe_through :review_checks

    resources "/", ReviewController
  end
end

Channel Routes

Channelというものがあります。おそらくリアルタイムのチャットなどwebsocketを使う手段でしょう。
Channelの設定はlib/hello_phoenix_web/endpoint.exに記載があります。この箇所でwebsocketのハンドラをマウントしているようです。

defmodule HelloPhoenixWeb.Endpoint do
  use Phoenix.Endpoint, otp_app: :hello_phoenix

  socket "/socket", HelloPhoenixWeb.UserSocket,
    websocket: true,
    longpoll: false
  ...
end

websocketを有効にするかロングポーリングを有効にするかの設定があります。webscoket使えるならロングポーリングは要らないでしょう。

先ほどマウントしたハンドラを見てみます。lib/hello_phoenix_web/channels/user_socket.exにファイルがあります。

defmodule HelloPhoenixWeb.UserSocket do
  use Phoenix.Socket

  ## Channels
  # channel "room:*", HelloPhoenixWeb.RoomChannel

  # Socket params are passed from the client and can
  # be used to verify and authenticate a user. After
  # verification, you can put default assigns into
  # the socket that will be set for all channels, ie
  #
  #     {:ok, assign(socket, :user_id, verified_user_id)}
  #
  # To deny connection, return `:error`.
  #
  # See `Phoenix.Token` documentation for examples in
  # performing token verification on connect.
  def connect(_params, socket, _connect_info) do
    {:ok, socket}
  end

  # Socket id's are topics that allow you to identify all sockets for a given user:
  #
  #     def id(socket), do: "user_socket:#{socket.assigns.user_id}"
  #
  # Would allow you to broadcast a "disconnect" event and terminate
  # all active sockets and channels for a given user:
  #
  #     HelloPhoenixWeb.Endpoint.broadcast("user_socket:#{user.id}", "disconnect", %{})
  #
  # Returning `nil` makes this socket anonymous.
  def id(_socket), do: nil
end

ほぼなんも書かれてません。
コメントアウトされている箇所の

  # channel "room:*", HelloPhoenixWeb.RoomChannel

でチャンネルモジュールを呼び出すようです。上記の例ではroom:*というトピック名の通信を呼び出しているようです。
room:*と書かれているのはroom:subtopicというサブトピックもHelloPhoenixWeb.RoomChannelでハンドリングするという書き方なのでしょう。
詳しくはChannelsの章があるのでその際に見ていきたいと思います。
(まだwebsocket分かってないマンなのでPhoenixでwebsocketを理解できればとは思います。)

系統立ててなかなか学習できてませんが、とりあえず進める精神で他の項目もやっていきます┗(⌒)(╬´◓ω◔`╬)(⌒)┛