Spec-Zone.ru › Phoenix

Plug

Исходный код

Требование: предполагается, что вы ознакомились с вводными руководствами и запустили приложение Phoenix в рабочем режиме.

Требование: предполагается, что вы ознакомились с руководством по жизненному циклу запроса.

Plug находится в основе HTTP-уровня Phoenix, и Phoenix уделяет Plug центральное место. Мы взаимодействуем с plug на каждом этапе жизненного цикла запроса, а основные компоненты Phoenix, такие как конечные точки, маршрутизаторы и контроллеры, внутри устроены как plug. Давайте разберёмся, что же делает Plug таким особенным.

Plug — это спецификация для компонуемых модулей, расположенных между веб-приложениями. Это также уровень абстракции для адаптеров соединений различных веб-серверов. Основная идея Plug — унифицировать понятие «соединения», с которым мы работаем. Это отличается от других уровней промежуточного ПО HTTP, таких как Rack, где запрос и ответ разделены в цепочке промежуточного ПО.

В самом простом виде спецификация Plug представлена в двух вариантах: функциональные plug и модульные plug.

Функциональные plug

Чтобы функция могла работать как plug, она должна:

  1. принимать структуру соединения (%Plug.Conn{}) в качестве первого аргумента, а параметры соединения — в качестве второго;
  2. возвращать структуру соединения.

Подойдёт любая функция, удовлетворяющая этим двум условиям. Вот пример.

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

  conn
end

Эта функция выполняет следующие действия:

  1. Получает соединение и параметры (которые мы не используем)
  2. Выводит сведения о соединении в терминал
  3. Возвращает соединение

Всё просто, правда? Давайте посмотрим на эту функцию в действии, добавив её в конечную точку в 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 и функций.

← Предыдущая страница Жизненный цикл запроса
Следующая страница → Маршрутизация

Пакет Hex Предварительный просмотр Hex (текущий файл) Скачать версию ePub

Создано с помощью ExDoc (v0.40.3) для языка программирования Elixir

© 2013 Plataformatec
Licensed under the Apache License, Version 2.0.
https://phoenix.hexdocs.pm/plug.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API