檢視原始碼 Plug
需求:這份指南假定您已閱讀過 請求生命週期指南。
Plug 是 Phoenix HTTP 層的核心,並且 Phoenix 將 Plug 擺在首要位置。我們在請求生命週期的每個步驟都會與 Plug 互動,而 Phoenix 的核心元件(例如端點、路由器和控制器)在內部都只是 Plug。讓我們深入了解 Plug 的特別之處。
Plug 是一種用於 Web 應用程式之間的可組合模組的規範。它同時也是不同 Web 伺服器連接適配器的抽象層。Plug 的基本概念是統一我們操作的「連線」概念。這不同於其他 HTTP 中介層,例如 Rack,在這些中介層堆疊中,請求和回應是分開的。
Plug 規範最基本的層級分成兩種:函式 Plug 和 模組 Plug。
函式 Plug
為了作為一個 Plug,函式需要
- 接受一個連線結構 (
%Plug.Conn{}
) 作為第一個引數,連線選項作為第二個引數; - 回傳一個連線結構。
符合這兩個條件的任何函式都可以。以下是一個範例。
def introspect(conn, _opts) do
IO.puts """
Verb: #{inspect(conn.method)}
Host: #{inspect(conn.host)}
Headers: #{inspect(conn.req_headers)}
"""
conn
end
這個函式會執行下列操作
- 它會接收一個連線和選項(我們不會使用它)
- 它會將一些連線資訊列印到終端機
- 它會回傳連線
相當簡單,對吧?讓我們將這個函式加入在 lib/hello_web/endpoint.ex
中的端點,看看它的實際作用。我們可以在任何地方加入它,因此我們在將請求委派給路由器之前,插入 plug :introspect
defmodule HelloWeb.Endpoint do
...
plug :introspect
plug HelloWeb.Router
def introspect(conn, _opts) do
IO.puts """
Verb: #{inspect(conn.method)}
Host: #{inspect(conn.host)}
Headers: #{inspect(conn.req_headers)}
"""
conn
end
end
函式 Plug 會將函式名稱作為一個原子插入。要試用這個 Plug,請回到瀏覽器並擷取 https://127.0.0.1:4000。您應該會在 shell 終端機中看到列印出類似以下的內容
Verb: "GET"
Host: "localhost"
Headers: [...]
我們的 Plug 會簡單地列印連線資訊。儘管我們的初始 Plug 非常簡單,但您幾乎可以在其中執行任何您想做的事。要深入了解連線中所有可用的欄位,以及所有與它相關的功能,請參閱 Plug.Conn
的文件。
現在讓我們看看另一種 Plug 變體,模組 Plug。
模組外掛程式
模組外掛程式為另一種類型的外掛程式,讓你在模組中定義連線轉換。該模組只需實作兩個函式
為了在實際操作中了解這點,讓我們編寫一個模組外掛程式,其將 :locale
金鑰和值輸入連線,供其他外掛程式、控制器動作,以及我們的檢視在下游使用。將下方的內容置於名為 lib/hello_web/plugs/locale.ex
的檔案中
defmodule HelloWeb.Plugs.Locale do
import Plug.Conn
@locales ["en", "fr", "de"]
def init(default), do: default
def call(%Plug.Conn{params: %{"locale" => loc}} = conn, _default) when loc in @locales do
assign(conn, :locale, loc)
end
def call(conn, default) do
assign(conn, :locale, default)
end
end
為了試試看,讓我們將這個模組外掛程式新增到我們的路由器,將 plug HelloWeb.Plugs.Locale, "en"
附加到 lib/hello_web/router.ex
中的 :browser
管線
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
plug HelloWeb.Plugs.Locale, "en"
end
...
在 init/1
回呼中,如果參數中不存在,則我們傳遞一個預設的區域設定來使用。我們也使用樣式配對來定義多個 call/2
函式表頭,以驗證參數中的區域設定,並在沒有配對的情況下退回 "en"
。 assign/3
是 Plug.Conn
模組的一部份,也是我們在 conn
資料結構中儲存值的方式
為了在實際操作中了解指定,請前往 lib/hello_web/controllers/page_html/home.html.heex
中的範本,並在 </h1>
標籤結束後新增以下程式碼
<p>Locale: <%= @locale %></p>
跳至 https://127.0.0.1:4000/,你應該會看到顯示的區域設定。請拜訪 https://127.0.0.1:4000/?locale=fr,你應該會看到指定變更為 "fr"
。任何人都可以使用這個資訊結合 Gettext 來提供完全國際化的網頁應用程式
Plug 的部分就介紹到這邊。Phoenix 完全採用 Plug 的設計來 COMPOSABLE 所有堆疊中上下的轉換。讓我們看一些範例!
插入位置
Phoenix 中的端點、路由器及控制器接受外掛程式
端點外掛程式
端點整理每個要求共有的所有插入器,並且在使用具有自訂管線的路由器進行調度之前套用這些插入器。我們像這樣,將一個插入器加入端點
defmodule HelloWeb.Endpoint do
...
plug :introspect
plug HelloWeb.Router
預設端點插入器會進行大量的工作。它們的順序如下
Plug.Static
- 提供靜態資產。由於此插入器在記錄器之前,因此不會記錄靜態資產的要求。Phoenix.LiveDashboard.RequestLogger
- 為 Phoenix LiveDashboard 設定 *要求記錄器*,這將允許您選擇傳遞一個查詢參數,以串流要求記錄,或啟用/停用一個從您的資訊面板串流要求記錄的 Cookie。Plug.RequestId
- 為每個要求產生一個唯一的請求 ID。Plug.Telemetry
- 加入工具測量點,以便 Phoenix 能夠記錄請求路徑、狀態碼和請求時間,這是預設行為。Plug.Parsers
- 當有可用的已知剖析器時,剖析請求本體。在預設情況下,此插入器可以處理 URL 編碼、多部分和 JSON 內容 (使用Jason
)。如果無法剖析請求的內容類型,則請求本體將保持不變。Plug.MethodOverride
- 使用一個有效的_method
參數,將請求方法轉換為 POST 要求的 PUT、PATCH 或 DELETE。Plug.Head
- 將 HEAD 要求轉換為 GET 要求。Plug.Session
- 一個用於設定會話管理的插入器。請注意,在使用會話之前,仍然必須明確呼叫fetch_session/2
,因為此插入器僅設定如何擷取會話。
端點中間還有一個條件式區塊
if code_reloading? do
socket "/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket
plug Phoenix.LiveReloader
plug Phoenix.CodeReloader
plug Phoenix.Ecto.CheckRepoStatus, otp_app: :hello
end
此區塊僅在開發環境中執行。它啟用了
- 即時重新載入 - 如果您變更一個 CSS 檔案,它們會在瀏覽器中更新,而不會重新整理頁面;
- 代碼重新載入 - 因此,我們可以在不重新啟動伺服器的情況下,看到應用程式的變更;
- 檢查儲存庫狀態 - 確保我們的資料庫是最新的,否則會產生可讀且可採取行動的錯誤。
路由器插入器
在路由器中,我們可以在管線中宣告插入器
defmodule HelloWeb.Router do
use HelloWeb, :router
pipeline :browser do
plug :accepts, ["html"]
plug :fetch_session
plug :fetch_live_flash
plug :put_root_layout, html: {HelloWeb.LayoutView, :root}
plug :protect_from_forgery
plug :put_secure_browser_headers
plug HelloWeb.Plugs.Locale, "en"
end
scope "/", HelloWeb do
pipe_through :browser
get "/", PageController, :index
end
路由在作用域內定義,並且作用域可以透過多個管線進行串接。當一個路由匹配時,Phoenix 會呼叫與該路由相關的所有管線中定義的所有插入器。例如,存取 "/" 將透過 :browser
管線進行串接,進而呼叫其所有插入器。
如同我們將在 路由指南 中看到,管道本身即為外掛。在指南中,我們也會討論 :browser
管道中的所有外掛。
控制器外掛
最後,控制器也是外掛,所以我們可以做到
defmodule HelloWeb.PageController do
use HelloWeb, :controller
plug HelloWeb.Plugs.Locale, "en"
特別是,控制器外掛會提供一個功能,讓我們僅能在特定動作內執行外掛。例如,你可以這樣做
defmodule HelloWeb.PageController do
use HelloWeb, :controller
plug HelloWeb.Plugs.Locale, "en" when action in [:index]
外掛將僅會在 index
動作中執行。
外掛作為組成
通過遵守外掛合約,我們將應用程式要求轉換為一系列明確的轉換。不只如此。為了真正了解 Plug 的設計有多麼有效,讓我們想像一個場景:我們需要檢查一系列條件,然後在條件失敗時重新導向或暫停。沒有 Plug 的話,我們最終會寫出類似這樣的東西
defmodule HelloWeb.MessageController do
use HelloWeb, :controller
def show(conn, params) do
case Authenticator.find_user(conn) do
{:ok, user} ->
case find_message(params["id"]) do
nil ->
conn |> put_flash(:info, "That message wasn't found") |> redirect(to: ~p"/")
message ->
if Authorizer.can_access?(user, message) do
render(conn, :show, page: message)
else
conn |> put_flash(:info, "You can't access that page") |> redirect(to: ~p"/")
end
end
:error ->
conn |> put_flash(:info, "You must be logged in") |> redirect(to: ~p"/")
end
end
end
注意到只有幾個驗證和授權步驟就需要複雜的巢狀和重複嗎?讓我們用幾個外掛改善一下它。
defmodule HelloWeb.MessageController do
use HelloWeb, :controller
plug :authenticate
plug :fetch_message
plug :authorize_message
def show(conn, params) do
render(conn, :show, page: conn.assigns[:message])
end
defp authenticate(conn, _) do
case Authenticator.find_user(conn) do
{:ok, user} ->
assign(conn, :user, user)
:error ->
conn |> put_flash(:info, "You must be logged in") |> redirect(to: ~p"/") |> halt()
end
end
defp fetch_message(conn, _) do
case find_message(conn.params["id"]) do
nil ->
conn |> put_flash(:info, "That message wasn't found") |> redirect(to: ~p"/") |> halt()
message ->
assign(conn, :message, message)
end
end
defp authorize_message(conn, _) do
if Authorizer.can_access?(conn.assigns[:user], conn.assigns[:message]) do
conn
else
conn |> put_flash(:info, "You can't access that page") |> redirect(to: ~p"/") |> halt()
end
end
end
為了讓這一切奏效,我們轉換了巢狀程式碼區塊,並在到達失敗路徑時使用 halt(conn)
。這時的 halt(conn)
功能至關重要:它告訴 Plug 不應該呼叫下一個外掛。
最後,通過用扁平的外掛轉換系列取代巢狀程式碼區塊,我們可以用更可組合、清晰且可重複利用的方式達成相同的功能。
如需瞭解更多有關外掛的資訊,請參閱 Plug 專案 的文件,其中提供了許多內建外掛和功能。