Phoenix: 静的HTMLの配信#
静的HTMLの配信は以下の二通りあります。
endpointで行う方法
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の該当コード行へ飛ぶので確認したい場合はリンク先を参照ください。
リクエストのPlug.Connは endpoint.ex で、Routerに渡される前にPhoenix.LiveReloaderプラグを通ります。
Phoenix.LiveReloader は、 before_send_inject_reloader で register_bofore_send を呼びsend前のコールバックを登録します。
リクエストはその後、ルーターを経てコントローラーの html に渡り send_resp 内の run_before_send にいたり、Enum.reduceでコールバックが実行されコード注入が実現されます。
これにより静的HTMLにコード注入が実現でき、ライブリロードができます。
このTombo NotesはSphinxを用いて静的HTMLを生成しており、ライブリロード機能を利用して記述しています。
以下が動作時の例です。