Elixir Phoenix 指南 - Plug - 翻译

268 阅读7分钟

Plug

Plug 是 Phoenix 中 HTTP 层的核心,重中之重。我们在请求生命周期的每个步骤中都会与 Plug 交互,而核心 Phoenix 组件(如端点、路由器和控制器)在内部都只是 Plug。。让我们一起来看看是什么让 Plug 如此特别。

Plug 是 Web 应用程序之间可组合模块的规范。它也是不同 Web 服务器的连接适配器的抽象层。 Plug 的基本思想是统一 “connection” 的概念。这与其他 HTTP 中间件层(例如 Rack)不同,后者的请求和响应在中间件堆栈中是分开的。

在最简单的层面上,Plug 规范有两种形式:函数 plugs 和 模块 plugs。

注:这一块很抽象,可以回过头再来理解。

函数 plugs

一个函数要成为 函数plug,需要:

  1. 接收一个 连接结构体(%Plug.Conn{}) 作为第一个参数,连接选项作为第二个参数;
  2. 返回一个 连接结构体。

满足这两个条件的,就能成为 函数plug。举个例子:

def introspect(conn, _opts) do
  IO.puts """
  Verb: #{inspect(conn.method)}
  Host: #{inspect(conn.host)}
  Headers: #{inspect(conn.req_headers)}
  """

  conn
end

这个函数做了这些事:

  1. 接收参数为 connection 和 options;
  2. 打印一些请求信息;
  3. 返回 connection;

很简单,对吧?让我们通过将它添加到 lib/hello_web/endpoint.ex 中的 endpoint 来看看这个函数。我们可以在任何地方插入它,所以让我们在将请求委托给路由器之前插入 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。看看效果,请返回浏览器并获取 http://localhost:4000 。你应该会在你的 shell 终端中看到类似这样的打印:

Verb: "GET"
Host: "localhost"
Headers: [...]

我们的 plug 只是打印来自连接的信息。尽管我们最初的插件非常简单,但您几乎可以在其中做任何您想做的事情。要了解连接中可用的所有字段以及与之关联的所有功能,请参阅 Plug.Conn 的文档。

然后来看看模块 plugs。

模块 plugs

模块 plugs 是另一种类型的 plug ,它让我们在模块中定义连接转换。该模块只需要实现两个函数:

  • init/1 初始化要传递给 call/2 的任何参数或选项。
  • call/2 执行 connection 转换。 call/2 只是我们之前看到的一个函数 plug。

为了看到这一点,让我们编写一个模块 plug ,将 :locale 键和值放入 connection assign 中,以供下游在其他plugs 、controller actions 和 views 中使用。将以下内容放入名为 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 放到 router,放到文件 lib/hello_web/router.ex 中的 :browser pipeline 下面,在这里添加 plug HelloWeb.Plugs.Locale, "en" :

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 回调中,如果参数中不存在任何 locale ,我们会传递一个默认 locale 来使用。我们还使用模式匹配来定义多个 call/2 函数头来验证参数中的 locale,如果没有匹配则回退到“en”。 assign/3Plug.Conn 模块的一部分,它是我们在 conn 数据结构中存储值的方式。

要查看实际 assign ,请转到 lib/hello_web/templates/layout/app.html.heex layout,然后在 container 附近添加以下内容:

<main class="container">
  <p>Locale: <%= @locale %></p>

转到 http://localhost:4000/,您应该会看到显示的 locale。 访问 http://localhost:4000/?locale=fr ,您应该看到 locale 变成了“fr”。 可以将这些与 Gettext 一起使用,以提供完全国际化的 Web 应用程序。

使用 plug 的位置

在 Phoenix 中,可以在 endpoint,router 和 controller 中使用 plugs。

Endpoint plugs

endpoint 放置每个请求共用的所有 plugs,,并在将其与自定义管道一起发送到路由器之前应用它们。 我们在 endpoint 添加了一个插件,如下所示:

defmodule HelloWeb.Endpoint do
  ...

  plug :introspect
  plug HelloWeb.Router

默认的 endpoint plugs 做了很多工作。它们按顺序排列如下:

  • Plug.Static - 服务静态资源。由于此 plug 位于 logger 之前,因此不会记录对静态资源的请求。
  • Phoenix.LiveDashboard.RequestLogger - 为 Phoenix LiveDashboard 设置 Request Logger,这将允许您选择将查询参数传递给流式请求日志或启用/禁用从仪表板流式传输请求日志的 cookie。
  • Plug.RequestId - 为每个请求生成一个唯一的请求ID。
  • Plug.Telemetry - 添加检测点,以便 Phoenix 可以默认记录请求路径、状态代码和请求时间。
  • Plug.Parsers - 当解析器可用时解析请求 body。 默认情况下,此 plug 可以处理 URL 编码、multipart 和 JSON 内容(使用 Jason)。 如果无法解析请求的 content-type ,则请求正文保持不变。
  • Plug.MethodOverride - 对带有有效 _method 参数的 POST 请求,将请求方法转换为PUT、PATCH或DELETE。
  • Plug.Head - 将HEAD请求转换为GET请求并剥离响应主体。
  • Plug.Session - 一个设置会话管理的插件。 请注意 fetch_session/2 在使用会话之前仍然必须显式调用,因为这个 plug 只是设置了会话的获取方式。

在 endpoint 中间,还有一个条件块:

  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 文件,它们会在浏览器中更新而不刷新页面;
  • 代码重新加载 - 这样我们就可以在不重新启动服务器的情况下看到对应用程序的更改;
  • 检查 repo 状态 - 确保我们的数据库是最新的,否则会引发可读性和可操作性方面的错误。
路由 plugs

在路由中,我们可以声明 pipeline 的 plug:

defmodule HelloWeb.Router do
  use HelloWeb, :router

  pipeline :browser do
    plug :accepts, ["html"]
    plug :fetch_session
    plug :fetch_live_flash
    plug :put_root_layout, {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

路由是在 scope 内定义的,scope 内可以使用多个管道。 一旦路由匹配成功,Phoenix 就会调用与该路由关联的所有管道中定义的所有插件。 例如,访问“/”将通过 :browser 管道,从而调用它的所有 plugs。

正如我们将在路由指南中看到的,管道本身就是 plug。 在那里,我们还将讨论 :browser 管道中的所有 plugs。

Controller plugs

最后,controller 也是 plug,所以我们可以这样做:

defmodule HelloWeb.PageController do
  use HelloWeb, :controller

  plug HelloWeb.Plugs.Locale, "en"

特别是,controller plugs 提供了一项功能,允许我们仅在某些 actions 中执行插件。 例如,您可以这样做:

defmodule HelloWeb.PageController do
  use HelloWeb, :controller

  plug HelloWeb.Plugs.Locale, "en" when action in [:index]

这个 plug 就只会在 index 下执行。

Plugs as composition 可组合性

通过遵守 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: "/")
          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: "/")
            end
        end
      :error ->
        conn |> put_flash(:info, "You must be logged in") |> redirect(to: "/")
    end
  end
end

请注意,仅仅几个身份验证和授权步骤就需要复杂的嵌套和重复?让我们用几个 plugs 来改进这一点。

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: "/") |> 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: "/") |> 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: "/") |> halt()
    end
  end
end

为了使这一切正常工作,我们转换了嵌套的代码块,并在遇到故障路径时使用了 halt(conn)halt(conn) 功能在这里是必不可少的:它告诉 Plug 不应该调用下一个 Plug。

归根结底,通过用一系列扁平化的插件转换替换嵌套的代码块,我们能够以更加可组合、更清晰和可重用的方式实现相同的功能。

要了解有关插件的更多信息,请参阅 Plug 项目的文档,该项目提供了许多内置插件和功能。

这个 plug 可以翻译为插头,可以作为拦截器使用。

这个翻译仅是为了学习。

不会英语的怎么写代码哦,会英语的又不需要看翻译。