Elixir製Webアプリケーションフレームワーク – Phoenixの構造

elixir-flame-i-catch

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

PhoenixはRuby on Railsの影響を受けたMVCのフレームワークで、開発スタイルやディレクトリの構造が非常に似ています。
過去の記事では、環境構築とCRUDアプリケーションを作るところまでを説明しました。

高生産性、高信頼性、高速のElixir製Webアプリケーションフレームワーク、Phoenixを始める
ElixirとPhoenixでCRUDなWebアプリケーションを作る

今回は、アプリケーションの作成ではなくPhoenixの構造やリクエスト処理について調べてみます。

Request Pipeline

Phoenixは他のWebアプリケーションフレームワークと同様にModel-View-Controllerの構造をしています。
Modelはデータにアクセスし、Viewはデータを表示すること、Controllerはリクエストを受け、変換してModelに届ける役割を持ちます。
一方でリクエストを受け付けてからレスポンスを返すまでの処理の流れをプログラム上で明示的に記述します(RailsではRackに移譲することにより暗黙的に実現しています)。
クライアントから送られたリクエストに対して、Phoenixはパイプラインのように一つずつ処理をして加工していきます。
そして、最終的に加工され出来上がったレスポンスをクライアントに返却します。

サーバに届いたリクエストは以下の順に処理されます。

connection
|> endpoint
|> router
|> pipelines
|> controller

それぞれの役割は以下です。

  1. endpointにまず最初にリクエストが届きます
  2. routerでリクエストを正しいコントローラに振り分けます
  3. pipelinesでは共通処理をリクエストに対して行います
  4. controllerは通常のWebアプリケーションと同じ役割です

Plug

このリクエストに対して一つずつ処理を実行する関数をPhoenixのPlugと呼びます。
Plugの定義は「Plug.Conn構造体を入力してPlug.Conn構造体を出力する」です。
説明したroutercontrollerもPhoenixではPlugとして作られています。
つまり、Phoenixで作るWebアプリケーションはPlugのパイプラインで構成されているということです。

ディレクトリ構造

まず、プロジェクト全体のディレクトリ構造を見てみましょう。

hello
└── web
​    ├── channels
​    ├── controllers
​    │   ├── page_controller.ex
​    │   └── hello_controller.ex
​    ├── models
​    ├── static
​    ├── templates
​    │   ├── hello
​    │   │   └── world.html.eex
​    │   ├── layout
​    │   │   └── app.html.eex
​    │   ├── page
​    │   │   └── index.html.eex
​    ├── views
    │   ├── error_view.ex
​    │   ├── layout_view.ex
​    │   ├── page_view.ex
​    │   └── hello_view.ex
​    ├── router.ex
​    └── web.ex
└── lib
​    ├── hello.ex
​    ├── hello
​        ├── endpoint.ex
└── config
​    ├── config.exs
​    ├── dev.exs
​    ├── prod.exs
​    ├── prod.secret.exs
​    ├── test.exs
└── test...

configディレクトリ

config.exsはアプリケーション全体にかかわる設定をするファイルです。

use Mix.Config

# Configures the endpoint
config :hello, Hello.Endpoint,
  url: [host: "localhost"],
  root: Path.dirname(__DIR__),
  secret_key_base: "0W6...vk",
  render_errors: [accepts: ~w(html json)],
  pubsub: [name: Hello.PubSub,
           adapter: Phoenix.PubSub.PG2]

config関数はPhoenixアプリケーションの初期設定を行っています。ここでエンドポイントも設定しています。
それではendpoint.exを見てみましょう、このファイルはlibディレクトリに置かれています。

libディレクトリ

endpoint

endpoint.exの中身は以下のようになっています

lib/endpoint.ex

defmodule Hello.Endpoint do
  use Phoenix.Endpoint, otp_app: :hello
  
  plug Plug.Static, ...
  plug Plug.RequestId
  plug Plug.Logger
  plug Plug.Parsers, ...
  plug Plug.MethodOverride
  plug Plug.Head
  plug Plug.Session, ...
  plug Hello.Router  
end

リクエストに対して処理をしていくPlugが設定されています。
ここに書いてあるPlugを上から順に処理していきます。アプリケーションに共通の処理を足したい場合は、共通処理を書いたPlugをここに設定すれば良いだけです。非常にわかりやすいですね。

それ以外のファイル

endpoint.ex以外にlibディレクトリにはsupervision treeにより起動するプロセス、長く起動し続けるプロセスを記述したファイルを置きます。

webディレクトリ

もう一度webディレクトリの中身を見てみましょう。

hello
└── web
​    ├── channels
​    ├── controllers
​    │   ├── page_controller.ex
​    │   └── hello_controller.ex
​    ├── models
​    ├── static
​    ├── templates
​    │   ├── hello
​    │   │   └── world.html.eex
​    │   ├── layout
​    │   │   └── app.html.eex
​    │   ├── page
​    │   │   └── index.html.eex
​    ├── views
    │   ├── error_view.ex
​    │   ├── layout_view.ex
​    │   ├── page_view.ex
​    │   └── hello_view.ex
​    ├── router.ex
​    └── web.ex

このディレクトリにはおなじみのControllerやModel、Viewのファイルが配置されています。
また、他のFWでは見たことのないchannelsディレクトリがあります。ここにはPhoenixがサポートするリアルタイム通信をするためのファイルを配置します。

router

続いてrouter.exを見ていきましょう

web/router.ex

defmodule Hello.Router do
  use Hello.Web, :router

  # ひとまとまりのpipelineとして定義
  pipeline :browser do
    plug :accepts, ["html"]
    plug :fetch_session
    plug :fetch_flash
    plug :protect_from_forgery
    plug :put_secure_browser_headers
    plug Hello.Auth, repo: Hello.Repo
  end

  pipeline :api do
    plug :accepts, ["json"]
  end
  
  scope "/" Hello do
    pipe_through :browser # 上で定義したplugのまとまりをこのルートで処理させる
    get "/", PageController, :index
  end
end

routerはRailsと同じようにURLと処理するコントローラのルーティングを設定します。
それ以外にここでは、HTMLを返却するアプリケーションとAPIアプリケーションごとにそれぞれ共通して処理するPlug関数を設定しています。
pipe_through :browserの記述がありますが、これは上で定義したpipeline :browserの中で設定したplug関数を全て処理するようにしています。
endopoint.exrouter.exを見ればリクエストに対してPhoenixが処理することがすべて分かります。

Railsとの違い

ここまではPhoenixアプリケーションのリクエスト処理の仕組みやディレクトリ構造について調べました。
最後にRuby on Railsとの違いについて整理します。

起動処理

プロジェクトのルートディレクトリに配置されているmix.exを見ると、起動時にアプリケーションが呼び出しているモジュールが分かります。

mix.ex

def application do
  [mod: {Hello, []},
   applications: [:phoenix, :phoenix_html, :cowboy, :logger, :gettext,
                  :phoenix_ecto, :postgrex]]
end

これはPhoenix独自の仕組みではなく、すべてのElixirアプリケーションで共通する仕組みです。
Railsとは違い、Phoenixは一つのElixirアプリケーションとして作られています。

見通しの良さ

起動処理やリクエストに対する共通処理はendpoint.exrouter.exを見ればすぐに理解でき、非常に見通しの良い造りになっています。
endpoint, controller, routerなどを全てPlugとして定義し抽象化したことによる成果です。

アセット

これは重要なことではないですが、PhoenixはCoffeeScriptではなく、ES2015です。

まとめ

PhoenixがRailsで成功した素晴らしい仕組みを取り入れつつ、Elixir、関数型の強みを生かした独自の構造をしていることを書いてきました。
個人的にアプリケーションを書いていて印象に残ったことは、Railsに近い生産性を持ちつつ全体の仕組みが非常に理解しやすいことです。
そして、今回は書いていませんがPhoenixにはChannelというリアルタイム通信をサポートする機能があります(Rails5もActionCableでサポートされました)。

今後のWebの成長とともに、成功する可能性の非常に大きいフレームワークです。

参考資料

Programming Phoenix