Plug
Исходный кодТребование: предполагается, что вы ознакомились с вводными руководствами и запустили приложение Phoenix в рабочем режиме.
Требование: предполагается, что вы ознакомились с руководством по жизненному циклу запроса.
Plug находится в основе HTTP-уровня Phoenix, и Phoenix уделяет Plug центральное место. Мы взаимодействуем с plug на каждом этапе жизненного цикла запроса, а основные компоненты Phoenix, такие как конечные точки, маршрутизаторы и контроллеры, внутри устроены как plug. Давайте разберёмся, что же делает Plug таким особенным.
Plug — это спецификация для компонуемых модулей, расположенных между веб-приложениями. Это также уровень абстракции для адаптеров соединений различных веб-серверов. Основная идея 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, вернитесь в браузер и откройте http://localhost:4000. В терминале вы должны увидеть примерно следующее:
Verb: "GET" Host: "localhost" Headers: [...]
Наш plug просто выводит сведения из соединения. Хотя наш первоначальный plug очень прост, внутри него можно делать практически всё что угодно. Сведения обо всех доступных полях соединения и связанных с ним возможностях см. в документации по Plug.Conn.
Теперь рассмотрим другой вариант plug — модульные plug.
Модульные plug
Модульные plug позволяют определить преобразование соединения в модуле. Модулю нужно реализовать только две функции:
-
init/1, которая инициализирует аргументы или параметры, передаваемые вcall/2 -
call/2, которая выполняет преобразование соединения.call/2— это функциональный plug, который мы видели ранее
Чтобы увидеть это в действии, напишем модульный plug, который помещает ключ и значение :locale в соединение, чтобы их могли использовать последующие plug, действия контроллеров и наши представления. Поместите приведённое ниже содержимое в файл с именем 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 в маршрутизатор, дописав plug HelloWeb.Plugs.Locale, "en" в конвейер :browser в файле lib/hello_web/router.ex:
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
...
В обратном вызове 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 для полной интернационализации веб-приложения.
Вот и всё о Plug. Phoenix использует дизайн Plug, основанный на компонуемых преобразованиях на всех уровнях стека. Рассмотрим несколько примеров!
Где подключать plug
Конечная точка, маршрутизатор и контроллеры в Phoenix принимают plug.
Plug конечной точки
Конечные точки объединяют все plug, общие для каждого запроса, и применяют их перед передачей запроса маршрутизатору с его пользовательскими конвейерами. Мы добавили plug в конечную точку следующим образом:
defmodule HelloWeb.Endpoint do ... plug :introspect plug HelloWeb.Router
Стандартные plug конечной точки выполняют немало работы. Вот они в порядке вызова:
Plug.Static— обслуживает статические ресурсы. Поскольку этот plug расположен перед журналированием, запросы статических ресурсов не попадают в журнал.Phoenix.LiveDashboard.RequestLogger— настраивает журналирование запросов для Phoenix LiveDashboard, позволяя передавать параметр запроса для потоковой передачи журналов запросов либо включать или отключать cookie, которая передаёт журналы запросов из панели мониторинга.Plug.RequestId— создаёт уникальный идентификатор для каждого запроса.Plug.Telemetry— добавляет точки инструментирования, благодаря которым Phoenix по умолчанию может записывать в журнал путь запроса, код состояния и время обработки запроса.Plug.Parsers— анализирует тело запроса, если доступен соответствующий парсер. По умолчанию этот plug обрабатывает содержимое в форматах URL-encoded, multipart и JSON (с помощьюJason). Если тип содержимого запроса невозможно обработать, тело запроса остаётся без изменений.Plug.MethodOverride— преобразует метод запроса в PUT, PATCH или DELETE для запросов POST с допустимым параметром_method.Plug.Head— преобразует запросы HEAD в запросы GET.Plug.Session— plug, настраивающий управление сеансами. Обратите внимание: перед использованием сеанса по-прежнему необходимо явно вызватьfetch_session/2, поскольку этот plug лишь настраивает способ получения сеанса.
В середине конечной точки также находится условный блок:
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 он обновляется в браузере без перезагрузки страницы;
- перезагрузку кода — изменения приложения становятся видны без перезапуска сервера;
- проверку состояния репозитория — она гарантирует, что база данных обновлена, а в противном случае выводит понятную ошибку с указанием необходимых действий.
Plug маршрутизатора
В маршрутизаторе можно объявлять 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, 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 вызывает все plug, определённые во всех связанных с этим маршрутом конвейерах. Например, при обращении к «/» запрос проходит через конвейер :browser, вызывая все его plug.
Как мы увидим в руководстве по маршрутизации, сами конвейеры также являются plug. Там же мы обсудим все plug конвейера :browser.
Plug контроллера
Наконец, контроллеры тоже являются plug, поэтому можно сделать так:
defmodule HelloWeb.PageController do use HelloWeb, :controller plug HelloWeb.Plugs.Locale, "en"
В частности, plug контроллера позволяют выполнять plug только для определённых действий. Например, можно сделать так:
defmodule HelloWeb.PageController do use HelloWeb, :controller plug HelloWeb.Plugs.Locale, "en" when action in [:index]
В этом случае plug будет выполняться только для действия index.
Композиция с помощью plug
Соблюдая контракт plug, мы превращаем запрос приложения в последовательность явных преобразований. На этом возможности не заканчиваются. Чтобы увидеть, насколько эффективен дизайн 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
Обратите внимание, как даже несколько этапов аутентификации и авторизации приводят к сложной вложенности и дублированию кода. Давайте улучшим это с помощью пары plug.
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 вызывать не нужно.
В итоге, заменив вложенные блоки кода последовательностью преобразований plug, мы можем добиться той же функциональности более компонуемым, понятным и пригодным для повторного использования способом.
Дополнительные сведения о plug см. в документации проекта Plug, который предоставляет множество встроенных plug и функций.
© 2013 Plataformatec
Licensed under the Apache License, Version 2.0.
https://phoenix.hexdocs.pm/plug.html