Phoenix: 静的HTMLの配信

Published on 2020-06-13 00:00:00

静的HTMLの配信は以下の二通りあります。

  1. endpointで行う方法

  2. router、controllerで行う方法

どちらも Plug.Static を使いますが、
前者はお手軽に配信でき、後者は少し手間ですがライブリロードができます。

endpointで行う方法

mix phx.new直後のendpoint.exには
以下のように静的ファイルを配信する Plug.Static が設定されています。
plug Plug.Static,
  at: "/",
  from: :app_name,
  gzip: false,
  only: ~w(css fonts images js favicon.ico robots.txt)
最も簡単に"/"で配信する場合は、
上記のonlyに静的htmlを格納したディレクトリ名を設定することで配信ができます。
fromに :アプリケーション名 が設定されている場合、
静的ファイルは priv/static にあるものとして扱われます。
"/"以外かつ静的ファイルを priv/static 配下以外で配信する場合は、
以下のように設定します。
plug Plug.Static,
  at: "/static",
  from: {:app_name, "priv/static_html"},
  only: ~w(directory_name or filename)

また、上記2つのソースコードをendpointに同時に書くことも可能です。 これは Plug だからです。

gzip等他のoptsは Plug.Static を参照ください。

router、controllerで行う方法

まず、routerを説明します。
静的html配信のために pipeline と scope を以下のように定義します。
# 既存
pipeline :browser do
  plug :accepts, ["html"]
  plug :fetch_session
  plug :fetch_live_flash
  plug :put_root_layout, {TomboWorksWeb.LayoutView, :root}
  plug :protect_from_forgery
  plug :put_secure_browser_headers
end

# 追加
pipeline :static_html do
  plug :accepts, ["html"]
  plug :fetch_session
  plug :fetch_live_flash
  plug :put_secure_browser_headers
end

# 追加
scope "/static", AppNameWeb do
  pipe_through :static_html
  get "/*path", StaticHTMLController, :index
end

"/static"へのリクエストは :static_html pipelineを通り、"/*path" で捕捉されます。

静的HTMLにはlayout, csrf token が不要なため、
:static_html pipeline は:browser pipeline から:put_root_layoutと:protect_from_forgeryを外したものとしています。
"/*path"は特殊なパスでワイルドカードのように振る舞うパスです。
José Valimが stackoverflow で説明しています。
次に、controllerを説明します。
静的html配信のためにStaticHTMLControllerに Plug.Static と index を以下のように定義します。
plug Plug.Static,
  at: "/static",
  from: {:app_name, "priv/static_html"},
  only: ~w(directory_name or filename)

def index(conn, _params) do
  file =
    ["priv/static_html" | conn.params["path"]]
    |> Enum.join("/")

  case File.read(file) do
    {:ok, data} ->
      html(conn, data)

    _ ->
      conn
      |> put_status(:not_found)
      |> put_view(AppNameWeb.ErrorView)
      |> render(:"404") # render built-in 404.html
  end
end
リクエスト( Plug.Conn )は、まず Plug.Static に渡されレスポンスとなる対象があるか確認されます。
対象があればレスポンスが返り処理は終了します、
対象がなければリクエスト( Plug.Conn )は index に渡されます。
Plug.Static を使えば、atでパスをfromで静的ファイルの位置を定めることができるので、
本来、controllerを用いる必要性はありません。
しかし、ここではあえて Plug.Static とindex に分けるためにcontrollerを使っています。
その理由は、静的HTMLにライブリロードのためのコードを注入するためです。
方法は、Plug.Static ではhtml以外を返すよう設定し、
htmlのみをindexでhtml(conn, data)を介して返させます。

注釈

対象ファイルが読み込めない(存在しない)ときのために、組み込みの404.htmlを返すようにしています。

警告

config/dev.exsでlive_reloadの設定を忘れないよう注意してください。

ライブリロードコード注入の仕組み

html(conn, data)を介すとコード注入できる理由は、elixirforumの Phoenix Live Reload for API で回答されています。
しかし、ぱっとは分からなかったので、調べた要点をかいつまんで説明します。

以下、説明内の各リンクはgithubの該当コード行へ飛ぶので確認したい場合はリンク先を参照ください。

  1. リクエストのPlug.Connは endpoint.ex で、Routerに渡される前にPhoenix.LiveReloaderプラグを通ります。

  2. Phoenix.LiveReloader は、 before_send_inject_reloaderregister_bofore_send を呼びsend前のコールバックを登録します。

  3. リクエストはその後、ルーターを経てコントローラーの html に渡り send_resp 内の run_before_send にいたり、Enum.reduceでコールバックが実行されコード注入が実現されます。

これにより静的HTMLにコード注入が実現でき、ライブリロードができます。

このTombo NotesはSphinxを用いて静的HTMLを生成しており、ライブリロード機能を利用して記述しています。

以下が動作時の例です。

../../../_images/static_html_live_reload.gif