Plug是Phoenix的HTTP层的核心,也是Phoenix的重中之重。在连接的每一个生命周期中我们都有和Plug的交互,Phoenix的核心组件例如 Endpoint,Router 和 Controller 其实内在都是Plug。现在让我们看看为什么Plug是如此的特殊。

Plug 一个为了Web应用设计的技术规范(specification),通过这个规范可以实现可以高组合的模块。同时他也是一个面对不同web服务器的抽象层(类似rack或者是wsgi)。Plug的基本思路就是同一个我们能够处理的链接(connection)的概念。在这点上和其他的HTTP中间件,例如Rack,有所不同,在类似Rack里request和response被分别放在不同的中间件栈里(middleware stack)。

Plug 技术规范

简而言之,Plug的技术规范有两类,函数 plugs 和 模块 plugs

函数 Plugs

一个函数只要接受连接结构体 (%Plug.Conn{}) 和 选项,并且返回连接结构体,就可以视为一个函数类的 Plug。

def put_headers(conn, key_values) do
Enum.reduce key_values, conn, fn {k, v}, conn ->
Plug.Conn.put_resp_header(conn, k, v)
end
end

超简单吧!

在Phoenix里我们就是这样组合一系列动作,转换连接的:

defmodule HelloPhoenix.MessageController do
use HelloPhoenix.Web, :controller

plug: put_headers, %{content_encoding: "gzip", cache_control: "max-age=3600"}
plug: put_layout, "bare.html"
...
end

put_headers/2, put_layout/2 甚至 action/2 都符合Plug的规范, 他们把一个应用的请求转变为一些列的显式的转换。Plug的功能不仅如此, 我们设想一下,有些场景我们需要检查一些条件, 再根据这些条件的满足情况或作重定向,或则终止操作。如果没有 Plug,我们的代码会是这样的:

defmodule HelloPhoenix.MessageController do
use HelloPhoenix.Web, :controller

def show(conn, params) do
case authenticate(conn) do
{:ok, user} ->
case find_message(params["id"]) do
nil ->
conn |> put_flash(:info, "That message was't found") |> redirect(to: "/")
message ->
case authorize_message(conn, params["id"]) do
:ok ->
render conn, :show, page: find_page(params["id"])
:error ->
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

只是为了认证和验证身份就需要如此多的嵌套,如此多的重复代码? 我们看看如果用 Plug 的方式是如何漂亮搞定这个问题的:

defmodule HelloPhoenix.MessageController do
use HelloPhoenix.Web, :controller

plug :authenticate
plug :find_message
plug :authorize_message

def show(conn, params) do
render conn, :show, page: find_page(params["id"])
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 find_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

我们用扁平的一些列的 Plug 变换替代了丑陋的嵌套,这样的代码更加的容易组合,清晰并且容易复用。

现在让我们看看另一类 Plugs, 模块 plugs

模块 Plugs

模块 Plugs 是另一类型的 Plug,让我们能够在模块中定义连接转换(connection transformation)。这类模块只需要实现两个函数:

  • init/1 这个函数会初始化所有给 call/2 的参数
  • call/2 这个方法实现连接转换。 call/2 其实就是我们前面看到的函数Plug

我们看个实战例子。 我们下面实现一个模块Plug, 通过这个模块我们把一个 :locale 键值对放到连接里, 之后的其他plugs、controller或者view都可以使用。

defmodule HelloPhoenix.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


defmodule HelloPhoenix.Router do
use HelloPhoenix.Web, :router

pipline :browser do
plug :accepts, ["html"]
plug :fetch_session
plug :protect_from_forgery
plug :put_secure_browser_headers
plug :HelloPhoenix.Plugs.Locale, "en"
end
end

我们可以把我们定义的这个模块plug以 plug HelloPhoenix.Plugs.Locale, “en” 的形式加入到浏览器 pipeline。在 init/1 回调中, 我们传入了一个默认的locale, 如果没有具体的locale传入的时候, 他会被使用。在定义 call\2 的时候我们也用到了模式匹配定义多个函数,如果没有匹配到就默认使用 en。

这就是Plug的全部啦。Phoenix拥抱Plug对可组合转换的设计逻辑,并在处理连接的从上到下的每个栈里都有用到。这里我们只是简单尝试了一下。在以后的开发中如果你问自己 ”这个我们能放到一个plug里吗?“ 答案通常都是”可以!“

Show your support

Clapping shows how much you appreciated tingwang’s story.