Elixir: починаємо роботу з Plug


У світі
Elixir
,
Plug
представляє собою специфікацію, що дозволяє різним фреймворкам спілкуватися з різними web-серверами, працюють в
Erlang VM
.
Якщо ви знайомі з
Ruby
, то можете провести аналогію з
Rack
:
Plug
намагається вирішувати ті ж проблеми, але тільки іншим способом. Розуміння основ роботи
Plug
дозволить краще розібратися як з роботою
Phoenix
, так і інших веб-фреймворків, створених на мові
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, "Привіт")
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 to the unchanged 
# given plug.
use Plug.Builder

plug Plug.Logger
plug :extract_name
plug :greet, %{my_option: "Привіт"}

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
запит.
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.