В мире Elixir, Plug представляет собой спецификацию, позволяющую различным фреймворкам общаться с различными web-серверами, работающими в Erlang VM.
Если вы знакомы с Ruby, то можете провести аналогию с Rack: Plug пытается решать те же проблемы, но только другим способом. Понимание основ работы Plug позволит лучше разобраться как с работой Phoenix, так и других web-фреймворков, созданных на языке Elixir.




Роль Plug


Вы можете думать о Plug как о кусочке кода, который получает структуру данных, осуществляет с ней какие-то трансформации, и возвращает ту же структуру данных, но уже частично модифицированную. Та структура данных, с которой работает Plug, обычно называется соединением (connection). В этой структуре хранится всё, что требуется знать о запросе (пер: и об ответе тоже).


Так как любой Plug принимает и возвращает соединение, то можно выстроить цепочку из нескольких таких объектов, которые последовательно будут обрабатывать одно и то же соединение. Такая композиция называется Plug pipeline


Сама структура данных, представляющая соединение — обычная Elixir структура, называемая %Plug.Conn{} (документацию по ней можно найти здесь).



Два различных типа Plug


Существуют два различных типа Plug: Plug-функция и Plug-модуль.


Plug-функция — любая функция, которая в качестве аргумента принимает соединение (это тот самый %Plug.Conn{}), и набор опций, и возвращает соединение.


def my_plug(conn, opts) do
  conn
end

Plug-модуль — это в свою очередь любой модуль, который имеет следующий интерфейс: init/1 и call/2, реализуемый таким образом:


module MyPlug do
  def init(opts) do
    opts
  end

  def call(conn, opts) do
    conn
  end
end

Интерес вызывает тот факт, что функция init/1 вызывается на этапе компиляции, а функция call/2 — во время работы программы.


Простой пример


Перейдём от теории к практике и создадим простейшее приложение, использующее Plug для обработки http запроса.


В начале создадим новый проект с помощью mix:


$ mix new learning_plug
$ cd learning_plug

Отредактируем файл mix.exs, добавив в качестве зависимостей Plug и Cowboy (это web-сервер):


# ./mix.exs

defp deps do
  [{:plug, "~> 1.0"},
   {:cowboy, "~> 1.0"}]
end

Подтянем зависимости:


$ mix deps.get

и мы готовы начинать работу!


Наш первый Plug будет просто возвращать "Hello, World!":


defmodule LearningPlug do
  # The Plug.Conn module gives us the main functions
  # we will use to work with our connection, which is
  # a %Plug.Conn{} struct, also defined in this module.
  import Plug.Conn

  def init(opts) do
    # Here we just add a new entry in the opts map, that we can use
    # in the call/2 function
    Map.put(opts, :my_option, "Hello")
  end

  def call(conn, opts) do
    # And we send a response back, with a status code and a body
    send_resp(conn, 200, "#{opts[:my_option]}, World!")
  end
end

Для использования этого модуля запустим iex с окружением проекта:


$ iex -S mix

и выполним следующие команды:


iex(1)> Plug.Adapters.Cowboy.http(LearningPlug, %{})
{:ok, #PID<0.150.0>}

Мы используем Cowboy в качестве web-сервера, указывая ему использовать наш Plug. Второй аргумент функции http/2 (в данном случае пустой Map %{}) — это тот самый набор опций, который передастся в качестве аргумента функции init/1 в наш Plug.
Web-сервер должен был стартовать на порту 4000, поэтому если вы откроете http://localhost:4000 в браузере, то увидите "Hello, World!". Очень просто!


Попробуем сделать наш Plug чуточку умнее. Пусть он анализирует URL, к которому мы делаем запрос на сервер, и если к примеру мы пытаемся получить доступ к http://localhost:4000/Name мы должны видеть “Hello, Name”.


Так как соединение представляет фигурально всё, что нужно знать о запросе, то оно хранит и его URL. Мы можем просто осуществить сопоставление с образцом этого URL для создания такого ответа, который мы хотим. Немного переделаем call/2 функцию следующим образом:


def call(%Plug.Conn{request_path: "/" <> name} = conn, opts) do
  send_resp(conn, 200, "Hello, #{name}")
end

Вот она, мощь функционального программирования! Мы сопоставляем только ту информацию, которая нам нужна (имя), а затем используем её для генерирования ответа.


Pipeline и как это работает


Plug сам по себе не представляет особого интереса. Вся красота подобной архитектуры раскрывается при попытке композиции множества модулей Plug вместе. Каждый из них делает свою маленькую часть работы, и передаёт соединение дальше.


Phoenix фреймворк использует pipeline везде, и делает это очень умно. По умолчанию, для обработки обычного браузерного запроса pipeline выглядит так:


pipeline :browser do
  plug :accepts, ["html"]
  plug :fetch_session
  plug :fetch_flash
  plug :protect_from_forgery
  plug :put_secure_browser_headers
end

Если, к примеру, нам надо обработать запрос к API, большинство из этих функций нам не нужны. Тогда pipeline значительно упрощается:


pipeline :api do
  plug :accepts, ["json"]
end

Конечно, pipeline макрос из предыдущего примера встроен в Phoenix. Однако и Plug сам по себе предоставляет возможность строить такую pipeline: Plug.Builder.


Вот пример его работы:


defmodule MyPipeline do
  # We use Plug.Builder to have access to the plug/2 macro.
  # This macro can receive a function or a module plug and an
  # optional parameter that will be passed unchanged to the 
  # given plug.
  use Plug.Builder

  plug Plug.Logger
  plug :extract_name
  plug :greet, %{my_option: "Hello"}

  def extract_name(%Plug.Conn{request_path: "/" <> name} = conn, opts) do
    assign(conn, :name, name)
  end

  def greet(conn, opts) do
    conn
    |> send_resp(200, "#{opts[:my_option]}, #{conn.assigns.name}")
  end
end

Тут мы сделали композицию трёх модулей PlugPlug.Logger, extract_name и greet.
extract_name использует assign/3 для того, чтобы поместить значение с определённым ключом в соединение. assign/3 возвращает модифицированную копию соединения, которое затем обрабатывается greet_plug, которое наоборот читает это значение, чтобы затем сгенерировать ответ, который нам нужен.


Plug.Logger поставляется вместе с Plug и, как вы догадались, используется для логирования http запросов. Прямо из коробки доступен определённый набор "батареек", список можно найти тут


Использовать такую pipeline так же просто как и Plug:


Plug.Adapters.Cowboy.http(MyPipeline, %{})

Следует не забывать о том, что модули используются в той же последовательности, в которой они определены в pipeline


Ещё одна фишка: те композиции, которые созданы с помощью Plug.Builder — также реализуют интерфейс Plug. Поэтому, к примеру, можно составить композицию из pipeline и Plug, и продолжать до бесконечности!


Подытожим


Основная идея в том, что и запрос, и ответ представлены в одной общей структуре %Plug.Conn{}, и эта структура передаётся "по цепочке" от функции к функции, частично изменяясь на каждом шагу (пер: изменяется фигурально — данные иммутабельны, поэтому дальше передаётся изменённая копия структуры), до тех пор, пока не получится ответ, который будет послан назад. Plug — это спецификация, определяющая как это всё должно работать и создающая абстракции так, что различные фреймворки могут общаться с различными web-серверами до тех пор, пока они выполняют эту спецификацию.


В "батарейки" к Plug входят различные модули, облегчающие множество разных распространённых задач: создание pipeline, простой роутинг, куки, заголовки и так далее.


А в конце хочется заметить, что в основе Plug лежит сама идея функционального программирования — передача данных по цепочке функций, которые трансформируют эти данные до тех пор, пока не получится нужный результат. Просто в этом случае данные — это http запрос.


Update: на самом деле это перевод, почему-то создался как публикация. Исправляю, приношу извинения. Дополнительно — мелкие орфографические исправления.

Поделиться с друзьями
-->

Комментарии (10)


  1. iqiaqqivik
    25.07.2016 15:14

    «Просто в этом случае данные — это http запрос.» — это, кстати, совершенно не обязательно.


  1. yaBliznyk
    25.07.2016 15:22
    +1

    Отличное начало, спасибо за перевод!
    Elixir с Phoenix займет свою нишу в хайлоаде. Очень жаль, что русскоязычных алхимиков еще так мало.
    Нужно больше полезных статей на русском и укрепить наше малочисленное community)
    Если кому интересно — много полезных новостей в телеграме + наше сообщество.
    proelixir_news — новостная рассылка ботом.
    proelixir — наше сообщество.
    Присоединяйтесь, будем очень рады!


    1. yaBliznyk
      25.07.2016 15:31

      Странно, что ссылки порезались.
      https://telegram.me/proelixir
      https://telegram.me/proelixir_news


    1. iqiaqqivik
      26.07.2016 10:10

      Те, кому интересно, читают twitter’ы ElixirRadar, Хосе Валима и Криса Мак Корда. Откуда эта страсть вырастить домотканный велосипед на ровном месте?


      1. yaBliznyk
        27.07.2016 09:32

        Я сам задавался иногда этим вопросом, но тут есть некая специфика окружения. Телеграм дает нам живое общение, можно быстро решать проблемы, делиться идеями и обсуждать новости. Если проблема более крупная — лучше искать решения в другом месте.
        Но самое важное, нам просто удобно им пользоваться. На всех устройствах, на работе. Дома, пардон, в туалете))) В машине читаю чаты по руби и эликсиру, новостные рассылки типа вот этой https://telegram.me/addmeto. Все очень удобно и в одном месте.

        В новостной рассылке и есть twitter’ы ElixirRadar, Хосе Валима и Криса Мак Корда (утрированно. мы их отрубили, т.к. идет много постороннего шума, а саму суть дают другие RSS источники).


  1. Strain
    25.07.2016 17:24

    философия plug напомнила эту штуку https://github.com/batate/elixir-pipes
    давно использую в продакшне, хороший набор макросов и главное просто в понимании и использовании


  1. Kaer_Morchen
    25.07.2016 21:35

    Я пробовал elixir и phoenix, все очень понравилось. Больше статей интересных и разных!


    1. begemot_sun
      25.07.2016 22:54

      Cами и напишите.


      1. Kaer_Morchen
        26.07.2016 07:31

        Hello world, это не тот уровень с которым нужно статьи писать


  1. UA3MQJ
    26.07.2016 21:26

    Спасибо за статью! Очень вовремя.