Жизненный цикл запроса
Исходный кодТребование: предполагается, что вы ознакомились с вводными руководствами и запустили приложение Phoenix, следуя руководству по запуску.
Цель этого руководства — рассказать о жизненном цикле запроса в Phoenix. Мы будем разбираться на практике: добавим в проект Phoenix две новые страницы и по ходу дела обсудим, как взаимодействуют составляющие приложения.
Давайте перейдём к созданию первой новой страницы Phoenix!
Добавление новой страницы
Когда браузер обращается к адресу http://localhost:4000/, он отправляет HTTP-запрос службе, запущенной по этому адресу, — в данном случае нашему приложению Phoenix. HTTP-запрос состоит из метода и пути. Например, следующие запросы браузера соответствуют таким значениям:
| Адрес в адресной строке браузера | Метод | Путь |
|---|---|---|
| http://localhost:4000/ | GET | / |
| http://localhost:4000/hello | GET | /hello |
| http://localhost:4000/hello/world | GET | /hello/world |
Существуют и другие HTTP-методы. Например, при отправке формы обычно используется метод POST.
Веб-приложения обычно обрабатывают запросы, сопоставляя каждую пару «метод/путь» с определённой частью приложения. В Phoenix это сопоставление выполняет маршрутизатор. Например, мы можем сопоставить путь "/articles" с частью приложения, которая показывает все статьи. Поэтому первая задача при добавлении новой страницы — добавить новый маршрут.
Новый маршрут
Маршрутизатор сопоставляет уникальные пары HTTP-метод/путь с парами контроллер/действие, которые будут их обрабатывать. Контроллеры в Phoenix — это просто модули Elixir. Действия — функции, определённые в этих контроллерах.
В новых приложениях Phoenix создаёт для нас файл маршрутизатора по пути lib/hello_web/router.ex. В этом разделе мы будем работать именно с ним.
Маршрут для страницы «Добро пожаловать в Phoenix!» из предыдущего руководства по запуску выглядит так:
get "/", PageController, :home
Разберём, что означает этот маршрут. При переходе по адресу http://localhost:4000/ отправляется HTTP-запрос GET к корневому пути. Все подобные запросы будут обрабатываться функцией home/2 модуля HelloWeb.PageController, определённого в lib/hello_web/controllers/page_controller.ex.
Страница, которую мы собираемся создать, будет выводить «Привет, мир от Phoenix!», когда мы перейдём в браузере по адресу http://localhost:4000/hello.
Сначала нужно добавить маршрут для новой страницы. Откроем 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.Layouts, :root}
plug :protect_from_forgery
plug :put_secure_browser_headers
end
pipeline :api do
plug :accepts, ["json"]
end
scope "/", HelloWeb do
pipe_through :browser
get "/", PageController, :home
end
# Other scopes may use custom stacks.
# scope "/api", HelloWeb do
# pipe_through :api
# end
# ...
end
Пока оставим конвейеры и использование scope без внимания и сосредоточимся на добавлении маршрута. Мы обсудим их в руководстве по маршрутизации.
Добавим в маршрутизатор новый маршрут, который сопоставляет запрос GET для /hello с действием index ещё не созданного HelloWeb.HelloController внутри блока scope "/" do маршрутизатора:
scope "/", HelloWeb do pipe_through :browser get "/", PageController, :home get "/hello", HelloController, :index end
Новый контроллер
Контроллеры — это модули Elixir, а действия — определённые в них функции Elixir. Действия собирают данные и выполняют задачи, необходимые для отображения страницы. В нашем маршруте указано, что нам нужен модуль HelloWeb.HelloController с функцией index/2.
Чтобы действие index заработало, создадим новый файл lib/hello_web/controllers/hello_controller.ex со следующим содержимым:
defmodule HelloWeb.HelloController do
use HelloWeb, :controller
def index(conn, _params) do
render(conn, :index)
end
end
Обсуждение use HelloWeb, :controller оставим для руководства по контроллерам. Пока сосредоточимся на действии index.
Все действия контроллера принимают два аргумента. Первый — conn, структура, содержащая множество данных о запросе. Второй — params, то есть параметры запроса. Здесь мы не используем params, поэтому добавляем перед ним _, чтобы избежать предупреждений компилятора.
Основная часть этого действия — render(conn, :index). Эта инструкция сообщает Phoenix, что нужно отобразить шаблон index. Модули, отвечающие за отображение, называются представлениями. По умолчанию представления Phoenix называются по имени контроллера (HelloController) и формата (в данном случае HTML), поэтому Phoenix ожидает, что существует модуль HelloWeb.HelloHTML, в котором определена функция index/1.
Новое представление
Представления Phoenix отвечают за презентационный слой. Например, ожидается, что результат отображения index будет полноценной HTML-страницей. Чтобы упростить себе задачу, мы часто используем шаблоны для создания таких HTML-страниц.
Создадим новое представление. Создайте lib/hello_web/controllers/hello_html.ex со следующим содержимым:
defmodule HelloWeb.HelloHTML do use HelloWeb, :html end
Шаблоны для этого представления можно определить как функции в модуле или в отдельных файлах.
Начнём с определения функции:
defmodule HelloWeb.HelloHTML do
use HelloWeb, :html
def index(assigns) do
~H"""
Hello!
"""
end
end
Мы определили функцию, принимающую assigns в качестве аргументов, и использовали сигил ~H, чтобы задать содержимое для отображения. Внутри сигила ~H мы использовали язык шаблонов HEEx, название которого расшифровывается как «HTML+EEx». EEx — это библиотека для встраивания Elixir, поставляемая в составе самого Elixir. «HTML+EEx» — расширение EEx от Phoenix, учитывающее HTML и поддерживающее проверку HTML, компоненты и автоматическое экранирование значений. Последнее защищает от уязвимостей безопасности, таких как межсайтовый скриптинг, без дополнительных усилий с вашей стороны. Функцию, которая принимает assigns и возвращает шаблоны, мы называем функциональным компонентом.
Файл шаблона работает таким же образом. Попробуем создать шаблон в отдельном файле. Сначала удалим определённую выше функцию def index(assigns) и заменим её объявлением embed_templates:
defmodule HelloWeb.HelloHTML do use HelloWeb, :html embed_templates "hello_html/*" end
Здесь мы указываем, что хотим встроить все шаблоны .heex из соседнего каталога hello_html в наш модуль в виде функциональных компонентов.
Теперь нужно добавить файлы в каталог lib/hello_web/controllers/hello_html. Файл шаблона имеет следующую структуру: NAME.FORMAT.TEMPLATING_LANGUAGE. В нашем случае создадим файл index.html.heex по пути lib/hello_web/controllers/hello_html/index.html.heex:
<section> <h2>Hello World, from Phoenix!</h2> </section>
Phoenix обнаружит файл шаблона и скомпилирует его в функцию index(assigns), как и в предыдущем примере. Между этими двумя подходами нет различий во время выполнения или производительности.
Обратите внимание, что имя контроллера (HelloController) и представления (HelloHTML), а также соответствующие файлы hello_controller.ex и hello_html.ex следуют одному соглашению об именовании и связаны друг с другом. Каталог можно назвать как угодно, если обновить соответствующую настройку embed_templates, но лучше придерживаться соглашений.
lib/hello_web ├── controllers │ ├── hello_controller.ex │ ├── hello_html.ex │ ├── hello_html | ├── index.html.heex (NEW FILE!)
Теперь, когда у нас есть маршрут, контроллер, представление и шаблон, можно перейти в браузере по адресу http://localhost:4000/hello и увидеть приветствие от Phoenix!
Если вы остановили сервер, перезапустить его можно с помощью задачи mix phx.server. Если сервер всё ещё работает, изменения должны примениться автоматически: Phoenix поддерживает горячую перезагрузку кода!
Макеты
Хотя файл index.html.heex состоит всего из одного тега section, мы получаем полноценный HTML-документ. На самом деле наш шаблон index отображается внутри отдельного макета: lib/hello_web/components/layouts/root.html.heex, содержащего базовый HTML-каркас страницы. Если открыть этот файл, внизу вы увидите строку примерно такого вида:
{@inner_content}
Эта строка вставляет наш шаблон в макет до отправки HTML в браузер. Корневой макет, как следует из названия, — это простой макет, содержащий в основном тег <head> и структуру, общую для всех страниц. Более функциональные элементы, такие как боковая панель, меню и прочее, относятся к макету приложения; мы рассмотрим их ниже.
От конечной точки до представлений
Создав первую страницу, мы уже начинаем понимать, как устроен жизненный цикл запроса. Теперь рассмотрим его целиком.
Все HTTP-запросы начинаются с конечной точки нашего приложения. Это модуль HelloWeb.Endpoint в файле lib/hello_web/endpoint.ex. Открыв файл конечной точки, вы увидите, что, как и маршрутизатор, она содержит множество вызовов plug. Plug — это библиотека и спецификация для объединения веб-приложений. Она играет важную роль в обработке запросов Phoenix; подробно мы обсудим её в следующем руководстве по Plug.
Пока достаточно знать, что каждый plug отвечает за отдельный этап обработки запроса. В файле конечной точки вы найдёте примерно такую основу:
defmodule HelloWeb.Endpoint do use Phoenix.Endpoint, otp_app: :hello plug Plug.Static, ... plug Plug.RequestId plug Plug.Telemetry, ... plug Plug.Parsers, ... plug Plug.MethodOverride plug Plug.Head plug Plug.Session, ... plug HelloWeb.Router end
У каждого из этих plug есть своя задача, с которой мы познакомимся позже. Последний plug — это как раз модуль HelloWeb.Router. Благодаря ему конечная точка передаёт всю дальнейшую обработку запросов маршрутизатору. Как мы уже знаем, основная задача маршрутизатора — сопоставлять пары «метод/путь» с контроллерами. Затем контроллер указывает представлению отобразить шаблон.
Возможно, сейчас вам кажется, что для простого отображения страницы нужно слишком много шагов. Однако по мере усложнения приложения мы увидим, что каждый уровень выполняет отдельную задачу:
конечная точка (
Phoenix.Endpoint) — содержит общую начальную цепочку, через которую проходят все запросы. Если нужно выполнить какое-либо действие для каждого запроса, его следует добавить в конечную точку.маршрутизатор (
Phoenix.Router) — отвечает за перенаправление пар «метод/путь» к контроллерам. Маршрутизатор также позволяет ограничивать область действия функциональности. Например, для доступа к некоторым страницам приложения может потребоваться аутентификация пользователя, а для других — нет.контроллер (
Phoenix.Controller) — получает информацию о запросе, взаимодействует с бизнес-доменом приложения и подготавливает данные для презентационного слоя.представление — обрабатывает структурированные данные от контроллера и преобразует их в формат для показа пользователям. Представления часто называют по формату содержимого, которое они отображают.
Кратко повторим, как взаимодействуют последние три компонента, добавив ещё одну страницу. На этот раз мы воспользуемся дополнительными возможностями, например компонентами макета и интерполяцией содержимого.
Ещё одна новая страница
Добавим приложению немного сложности. Мы создадим новую страницу, которая будет распознавать часть URL, называть её «посланником» и передавать через контроллер в шаблон, чтобы наш посланник мог поздороваться.
Как и в прошлый раз, сначала создадим новый маршрут.
Ещё один новый маршрут
В этом упражнении мы повторно используем HelloController, созданный на предыдущем шаге, и добавим новое действие show. Добавим строку сразу после последнего маршрута:
scope "/", HelloWeb do pipe_through :browser get "/", PageController, :home get "/hello", HelloController, :index get "/hello/:messenger", HelloController, :show end
Обратите внимание на синтаксис :messenger в пути. Phoenix возьмёт значение, указанное в этой позиции URL, и преобразует его в параметр. Например, если перейти в браузере по адресу http://localhost:4000/hello/Frank, значением "messenger" будет "Frank".
Ещё одно новое действие
Запросы к нашему новому маршруту будут обрабатываться действием show контроллера HelloWeb.HelloController. Контроллер уже находится в lib/hello_web/controllers/hello_controller.ex, поэтому нам нужно лишь отредактировать его и добавить действие show. На этот раз нужно извлечь посланника из параметров, чтобы передать его в шаблон. Для этого добавим в контроллер функцию show:
def show(conn, %{"messenger" => messenger}) do
render(conn, :show, messenger: messenger)
end
В теле действия show мы также передаём функции render третий аргумент — пару ключ-значение, где :messenger — ключ, а переменная messenger передаётся в качестве значения.
Если в теле действия нужен доступ ко всей карте параметров, связанной с переменной params, помимо связанной переменной messenger, можно определить show/2 следующим образом:
def show(conn, %{"messenger" => messenger} = params) do
...
end
Важно помнить, что ключи карты params всегда будут строками, а знак равенства означает не присваивание, а утверждение о сопоставлении с образцом.
Ещё один новый шаблон
Для завершения этой головоломки нам понадобится новый шаблон. Поскольку он предназначен для действия show контроллера HelloController, он будет находиться в каталоге lib/hello_web/controllers/hello_html и называться show.html.heex. Он будет удивительно похож на наш шаблон index.html.heex, но в нём нужно будет вывести имя посланника. Запишем новый шаблон, а затем разберём, что он делает:
<Layouts.app flash={@flash}>
<section>
<h2>Hello World, from {@messenger}!</h2>
</section>
</Layouts.app>
Если перейти в браузере по адресу http://localhost:4000/hello/Frank, вы увидите страницу примерно такого вида:
Разберём шаблон по частям. У него расширение .heex, которое означает HTML + Embedded Elixir. В шаблоне выше мы используем три возможности HEEx:
Назначенные значения (assigns), например
@messengerи@flash— значения, которые контроллер передаёт представлению, вместе называются «назначенными значениями». Мы могли бы получить доступ к значению посланника черезassigns.messengerиassigns.flash, но Phoenix предлагает гораздо более лаконичный синтаксис@для использования в шаблонах.Интерполяция содержимого, например
{@messenger}— любой код Elixir между{...}будет выполнен, а полученное значение заменит тег в HTML-выводе.Теги функциональных компонентов, например
<Layouts.app>— шаблоны называются функциональными компонентами, потому что их можно объединять! В этом случае есть функцияHelloWeb.Layouts.app, которая определяет макет приложения. Мы вызываем её, передавая собственное содержимое.
Также обратите внимание, как сочетаются эти три возможности: мы передаём назначенное значение @flash как значение Elixir компоненту <Layouts.app>. Как мы узнаем позже, flash-сообщения используются для отображения пользователю временных уведомлений, например сообщений об успехе или ошибке. Подробнее о них мы узнаем из руководства по компонентам и шаблонам HEEx.
Готово! Можете немного поэкспериментировать. Всё, что вы укажете после /hello/, появится на странице как имя вашего посланника.
© 2014 Chris McCord
Licensed under the MIT License.
https://phoenix.hexdocs.pm/request_lifecycle.html