
В мире Вы можете думать о Так как любой Сама структура данных, представляющая Существуют два различных типа Интерес вызывает тот факт, что функция Перейдём от теории к практике и создадим простейшее приложение, использующее В начале создадим новый проект с помощью Отредактируем файл Подтянем зависимости: и мы готовы начинать работу! Наш первый Для использования этого модуля запустим и выполним следующие команды: Мы используем Попробуем сделать наш Так как Вот она, мощь функционального программирования! Мы сопоставляем только ту информацию, которая нам нужна (имя), а затем используем её для генерирования ответа. Если, к примеру, нам надо обработать запрос к API, большинство из этих функций нам не нужны. Тогда Конечно, Вот пример его работы: Тут мы сделали композицию трёх модулей Использовать такую Следует не забывать о том, что модули используются в той же последовательности, в которой они определены в Ещё одна фишка: те композиции, которые созданы с помощью Основная идея в том, что и запрос, и ответ представлены в одной общей структуре В "батарейки" к А в конце хочется заметить, что в основе Update: на самом деле это перевод, почему-то создался как публикация. Исправляю, приношу извинения. Дополнительно — мелкие орфографические исправления.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
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
Plug
— Plug.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
запрос.