查看源代码 插头
要求:本指南假设您已经阅读过请求生命周期指南.
插头是 Phoenix HTTP 层的核心,Phoenix 将插头置于首位。我们在请求生命周期的每个步骤中都会与插头交互,而像端点、路由器和控制器这样的核心 Phoenix 组件内部都是插头。让我们深入了解一下,找出是什么让插头如此特别。
插头 是 web 应用程序之间可组合模块的规范。它也是不同 web 服务器连接适配器的抽象层。插头的基本思想是统一我们操作的“连接”概念。这与其他 HTTP 中间件层(如 Rack)不同,在 Rack 中,请求和响应在中间件堆栈中是分开的。
从最简单的层面上讲,插头规范有两种形式:函数插头和模块插头。
函数插头
为了作为一个插头,一个函数需要
- 接受一个连接结构体 (
%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
函数插头通过将函数名作为原子传递来插入。要尝试使用插头,请返回到浏览器并获取 http://localhost:4000。您应该在 shell 终端中看到类似于此的内容
Verb: "GET"
Host: "localhost"
Headers: [...]
我们的插头只是从连接中打印信息。虽然我们的初始插头非常简单,但您可以在其中做几乎任何您想做的事情。要了解连接中所有可用的字段以及与其关联的所有功能,请参阅 有关 Plug.Conn
的文档.
现在让我们看看另一个插头变体,即模块插头。
模块插头
模块插头是另一种类型的插头,它允许我们在一个模块中定义连接转换。该模块只需要实现两个函数
为了看到它的实际操作,让我们编写一个模块插头,它将 :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>
转到 http://localhost:4000/,您应该会看到显示的语言环境。访问 http://localhost:4000/?locale=fr,您应该会看到分配更改为 "fr"
。有人可以使用这些信息以及 Gettext 来提供一个完全国际化的 web 应用程序。
这就是关于插头的一切。Phoenix 采用插头的可组合转换设计,贯穿整个堆栈。让我们看一些例子!
插头的位置
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
)。如果无法解析请求的 content-type,则请求正文将保持不变。Plug.MethodOverride
- 将请求方法转换为 PUT、PATCH 或 DELETE,用于带有有效_method
参数的 POST 请求。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
操作执行。
插头作为组合
通过遵守插头契约,我们将应用程序请求变成一系列显式转换。它不会止步于此。要真正看到插头设计的效果,让我们想象一个需要检查一系列条件,然后在条件失败时重定向或停止的场景。如果没有插头,我们会得到类似于下面的东西
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)
功能在这里至关重要:它告诉插头不应该调用下一个插头。
最终,通过用扁平化的插头转换序列替换嵌套的代码块,我们能够以一种更加可组合、清晰和可重用的方式实现相同的功能。
要了解更多关于插头的知识,请参阅 插头项目 的文档,其中提供了许多内置插头和功能。