diff --git a/ru/lessons/specifics/plug.md b/ru/lessons/specifics/plug.md index 7e5046993c..a231fa9a4d 100644 --- a/ru/lessons/specifics/plug.md +++ b/ru/lessons/specifics/plug.md @@ -1,41 +1,53 @@ --- -version: 0.9.0 +version: 1.1.1 title: Plug --- -Если вы знакомы с `Ruby`, то можете думать о `Plug` как о комбинации `Rack` и `Sinatra`. Во-первых, `Plug` это набор договорённостей и спецификаций, позволяющих создавать универсальные компонуемые модули, иcпользуемые в веб-приложениях. Во-вторых, адаптеры соединений для различных веб-серверов на платформе `Erlang VM`. Хотя `Plug` и не является частью ядра `Elixir`, это официальный проект от той же команды. +Если вы знакомы с `Ruby`, то можете думать о `Plug` как о комбинации `Rack` и `Sinatra`. Это набор договорённостей и спецификаций для модулей, используемых в веб-приложениях, а также адаптеры соединений для различных веб-серверов. Хотя `Plug` и не является частью ядра `Elixir`, это официальный проект от той же команды. + +Мы начнем с создания минимального рабочего веб-приложения с использованием `Plug`. После этого мы познакомимся с роутерами и узнаем, как добавить `Plug` к уже существующему приложению. {% include toc.html %} -## Установка +## Перед установкой + +Чтобы следовать инструкциям этого урока, вам понадобятся установленный Elixir версии 1.4 или выше и `mix`. + +Если у вас еще нет проекта, создайте его: + +```shell +mix new example +cd example +``` + +## Зависимости -Plug устанавливается с помощью `mix`. Для установки `Plug` необходимо внести два небольших изменения в наш файл `mix.exs`. Первое, что необходимо сделать, это добавить `Plug` и выбранный веб-сервер (мы будем использовать [`Cowboy`](https://github.com/ninenines/cowboy)) в качестве зависимостей: +Добавлять новые зависимости при помощи `mix` невероятно легко. Чтобы установить `Plug` достаточно сделать пару изменений в файле `mix.exs`. +Для начала добавим в него сам `Plug`, а также веб-сервер (мы будет использовать `Cowboy`). ```elixir defp deps do - [{:cowboy, "~> 1.1.2"}, - {:plug, "~> 1.3.4"}] + [ + {:cowboy, "~> 1.1.2"}, + {:plug, "~> 1.3.4"}, + ] end ``` -Во-вторых, нужно добавить веб-сервер и `Plug` к нашему `OTP` приложению: +Выполните следующую команду в терминале, чтобы `mix` скачал и установил новые зависимости: -```elixir -def application do - [applications: [:cowboy, :logger, :plug]] -end +```shell +$ mix deps.get ``` ## Спецификация Чтобы создавать собственные модули `Plug`, нужно придерживаться спецификации. К счастью, необходимо реализовать всего две функции: `init/1` и `call/2`. -Функция `init/1` используется для инициализации параметров нашего модуля `Plug`, эти параметры передаются в качестве второго аргумента в функцию `call/2`. В дополнение к инициализированным параметрам, функция `call/2` получает структуру `%Plug.Conn` в качества своего первого аргумента, и ожидается, что она также вернёт соединение (структуру того же типа). - Вот пример простого модуля `Plug`, который возвращает "Hello World!": ```elixir -defmodule HelloWorldPlug do +defmodule Example.HelloWorldPlug do import Plug.Conn def init(options), do: options @@ -43,12 +55,117 @@ defmodule HelloWorldPlug do def call(conn, _opts) do conn |> put_resp_content_type("text/plain") - |> send_resp(200, "Hello World!") + |> send_resp(200, "Hello World!\n") end end ``` -## Создание модуля Plug +Сохраним файл как `lib/example/hello_world_plug.ex`. + +Функция `init/1` используется для инициализации параметров нашего модуля `Plug`. Она вызывается супервизором, который мы увидим в следующей секции. Пока что в качестве параметров будет пустой список. + +Значение, возвращаемое `init/1`, передается в качестве второго аргумента в функцию `call/2`. + +Функция `call/2` вызывается для каждого нового запроса, приходящего от веб-сервера — `Cowboy`. +Она получает структуру `%Plug.Conn` в качества своего первого аргумента, и ожидается, что она также вернёт соединение (структуру того же типа). + +## Настройка Application-модуля приложения + +Так как мы создаём Plug-приложение с нуля, нам придется создать еще и Application-модуль. +Добавим в `lib/example.ex` старт веб-сервера `Cowboy`: + +```elixir +defmodule Example do + use Application + require Logger + + def start(_type, _args) do + children = [ + Plug.Adapters.Cowboy.child_spec(:http, Example.HelloWorldPlug, [], port: 8080) + ] + + Logger.info "Started application" + + Supervisor.start_link(children, strategy: :one_for_one) + end +end +``` + +Это запустит `Cowboy` под супервизором, который в свою очередь запустит `HelloWorldPlug` в качестве дочернего процесса. + +В вызове `Plug.Adapters.Cowboy.child_spec/4` третий аргумент будет передан в `Example.HelloWorldPlug.init/1`. + +Но это еще не все. Откроем `mix.exs` снова и найдем там функцию `applications`. +Нужно сделать так, чтобы наше приложение автоматически запускалось. + +Для этого изменим файл следующим образом: + +```elixir +def application do + [ + extra_applications: [:logger], + mod: {Example, []} + ] +end +``` + +Теперь всё готово к запуску нашего первого веб-приложения, созданного на базе `Plug`. В командной строке выполним: + +```shell +$ mix run --no-halt +``` + +Как только все скомпилируется, и выведется сообщение `[info] Started app`, откройте в браузере `127.0.0.1:8080`. Там должно появиться следующее: + +``` +Hello World! +``` + +## Использование Plug.Router + +Для большинства приложений, таких как веб-сайты и REST API, понадобится что-то, что будет перенаправлять запросы к определенным ресурсам на соответствующие обработчики в коде. +Специально для этого в `Plug` существует маршрутизатор (или роутер). Как мы сейчас увидим, фреймворк типа `Sinatra` в `Elixir` не требуется, так как мы получаем его возможности вместе с `Plug` + +Для начала создадим файл `lib/plug/router.ex` и скопируем в него следующий код: + +```elixir +defmodule Example.Router do + use Plug.Router + + plug :match + plug :dispatch + + get "/", do: send_resp(conn, 200, "Welcome") + match _, do: send_resp(conn, 404, "Oops!") +end +``` + +Это самая простая реализация модуля `Router`, её код довольно очевиден. +Мы подключили необходимые макросы с помощью инструкции `use Plug.Router` и задействовали встроенные модули `Plug`: `:match` и `:dispatch`. В коде задано два предопределённых пути маршрутизации: один, для обработки `GET`-запросов к родительскому узлу `'/'`, и второй, для обработки всех остальных запросов, возвращающий сообщение об ошибке `404`. + +Вернемся теперь к `lib/example.ex` и добавим `Example.Router` к дочерним процессам веб-сервера. +Поменяем `Example.HelloWorldPlug` на наш новый роутер: + +```elixir +def start(_type, _args) do + children = [ + Plug.Adapters.Cowboy.child_spec(:http, Example.Router, [], port: 8080) + ] + Logger.info "Started application" + Supervisor.start_link(children, strategy: :one_for_one) +end +``` + +Запустим веб-сервер (в случае, если предыдущий сервер еще работает, его можно остановить, дважды нажав `Ctrl+C`). + +Теперь откроем `127.0.0.1:8080` в браузере. +Мы должны увидеть сообщение `Welcome`. +Попробуем открыть `127.0.0.1:8080/waldo` или любой другой ресурс. +Должна появиться 404 ошибка с текстом `Oops!`. + +## Создание еще одного модуля Plug + +Очень часто Plug-модули используются для обработки всех или части входящих запросов в соответствии с общей логикой. Для примера создадим модуль `Plug`, проверяющий наличие всех заданных параметров у входящего запроса. Реализуя такую проверку в виде модуля `Plug`, мы можем быть уверены, что приложением будут обрабатываться только корректные запросы. Ожидается, что наш модуль будет инициализироваться с двумя аргументами: `:paths` и `:fields`. Первый будет содержать те пути запросов, к которым мы применяем нашу проверку, а второй — наличие каких именно параметров у входящего запроса требуется контролировать. @@ -58,7 +175,6 @@ _Примечание_: модули `Plug` применяются ко всем ```elixir defmodule Example.Plug.VerifyRequest do - import Plug.Conn defmodule IncompleteRequestError do @moduledoc """ @@ -92,30 +208,13 @@ end Последняя часть описываемого модуля Plug — закрытая функция `verify_request!/2`, которая проверяет наличие у запроса всех требуемых параметров из аргумента `:fields`. В случае отсутствия любого из параметров, вызывается исключение `IncompleteRequestError`. -## Использование Plug.Router - -Теперь, когда готов модуль `VerifyRequest`, можно перейти к написанию маршрутизатора. Как мы сейчас увидим, фреймворк типа `Sinatra` в `Elixir` не требуется, так как мы получаем его возможности вместе с `Plug`. +Мы настроили наш модуль `Plug` так, чтобы проверять, что все запросы к пути `/upload` содержат параметры `"content"` и `"mimetype"`. Только в случае прохождения этой проверки может быть выполнен код маршрутизатора, связанный с такими запросами. -Для начала давайте создадим файл `lib/plug/router.ex` и скопируем в него следующий код: +Теперь нужно сообщить маршрутизатору о новом Plug-модуле. +Отредактируем `lib/example/router.ex` следующим образом: ```elixir -defmodule Example.Plug.Router do - use Plug.Router - - plug :match - plug :dispatch - - get "/", do: send_resp(conn, 200, "Welcome") - match _, do: send_resp(conn, 404, "Oops!") -end -``` - -Это самая простая реализация модуля `Router`, её код довольно очевиден. Мы подключили необходимые макросы с помощью инструкции `use Plug.Router` и задействовали встроенные модули `Plug`: `:match` и `:dispatch`. В коде задано два предопределённых пути маршрутизации: один, для обработки `GET`-запросов к родительскому узлу '/', и второй, для обработки всех остальных запросов, возвращающий сообщение об ошибке `404`. - -Давайте добавим созданный нами модуль `Plug` к коду данного маршрутизатора: - -```elixir -defmodule Example.Plug.Router do +defmodule Example.Router do use Plug.Router alias Example.Plug.VerifyRequest @@ -123,34 +222,36 @@ defmodule Example.Plug.Router do plug Plug.Parsers, parsers: [:urlencoded, :multipart] plug VerifyRequest, fields: ["content", "mimetype"], paths: ["/upload"] + plug :match plug :dispatch - get "/", do: send_resp(conn, 200, "Welcome") - post "/upload", do: send_resp(conn, 201, "Uploaded") - match _, do: send_resp(conn, 404, "Oops!") + get "/", do: send_resp(conn, 200, "Welcome\n") + post "/upload", do: send_resp(conn, 201, "Uploaded\n") + match _, do: send_resp(conn, 404, "Oops!\n") end ``` -Вот и всё! Мы настроили наш модуль `Plug` так, чтобы проверять, что все запросы к пути `/upload` содержат параметры `"content"` и `"mimetype"`. Только в случае прохождения этой проверки может быть выполнен код маршрутизатора, связанный с такими запросами. - -На данный момент, наша реализация `/upload` не очень полезна, но мы разобрались как создавать и использовать собственный модуль `Plug`. - -## Запускаем наше веб-приложение +## Делаем HTTP порт конфигурируемым -Перед тем как наше приложение может быть запущено, необходимо установить и настроить веб-сервер, в данном случае это `Cowboy`. Сейчас мы просто внесём все необходимые изменения в последующий код, а с деталями будем разбираться в других уроках. +Когда мы создавали наше приложение, HTTP порт был "зашит" в коде. +Считается хорошим тоном делать порт конфигурируемым при помощи файлов настроек. Начнём с изменения блока `application` в файле `mix.exs` для того, чтобы предоставить среде `Elixir` информацию о нашем приложении и установить для приложения переменную среды `env`. Отредактированный код данного блока будет выглядеть следующим образом: ```elixir def application do - [applications: [:cowboy, :plug], - mod: {Example, []}, - env: [cowboy_port: 8080]] + [ + extra_applications: [:logger], + mod: {Example, []}, + env: [cowboy_port: 8080] + ] end ``` -Далее необходимо обновить файл `lib/example.ex` для запуска и надзора за веб-сервером `Cowboy`: +Непосредственно нашего приложения касается строка `mod: {Example, []}`. Обратите внимание, что мы также запускаем приложения `cowboy`, `logger` и `plug`. + +Далее необходимо добавить в файл `lib/example.ex` чтение номера порта из настроек и передачу его в `Cowboy`: ```elixir defmodule Example do @@ -160,7 +261,7 @@ defmodule Example do port = Application.get_env(:example, :cowboy_port, 8080) children = [ - Plug.Adapters.Cowboy.child_spec(:http, Example.Plug.Router, [], port: port) + Plug.Adapters.Cowboy.child_spec(:http, Example.Router, [], port: port) ] Supervisor.start_link(children, strategy: :one_for_one) @@ -168,6 +269,8 @@ defmodule Example do end ``` +Третий аргумент в `Application.get_env` — это порт по умолчанию на случай, если настройка не объявлена. + > (Необязательно) добавить параметр `:cowboy_port` в файл `config/config.exs` ```elixir @@ -184,16 +287,17 @@ $ mix run --no-halt ## Тестирование модуля Plug -Тестировать модули `Plug` легко благодаря наличию [`Plug.Test`](https://hexdocs.pm/plug/Plug.Test.html). Этот модуль предоставляет множество функций для упрощения тестирования. +Тестировать модули `Plug` легко благодаря наличию `Plug.Test`. +Этот модуль предоставляет множество функций для упрощения тестирования. -Посмотрим, сможете ли вы самостоятельно разобраться с кодом для тестирования маршрутизатора ниже: +Напишем следующий тест в `test/example/router_test.exs`: ```elixir -defmodule RouterTest do +defmodule Example.RouterTest do use ExUnit.Case use Plug.Test - alias Example.Plug.Router + alias Example.Router @content "Hi!" @mimetype "text/html" @@ -227,6 +331,12 @@ defmodule RouterTest do end ``` +И запустим командой: + +```shell +mix test test/example/router_test.exs +``` + ## Доступные модули Plug Много модулей `Plug` доступно для использования сразу "из коробки". Полный список можно найти в документации по `Plug` — [здесь](https://github.com/elixir-lang/plug#available-plugs).