
Phoenix Framework всегда был классным. Но он никогда не был таким классным, как с новым релизом 1.3 (который сейчас находится в стадии RC2).
Произошло много значительных изменений. Крис МакКорд написал полный путеводитель по изменениям. Так же доступна его речь с LonestarElixir, где он подробно рассказывает про ключевые моменты. Вдохновленный его трудами, в своей статье я постараюсь рассказать вам про самые важные изменения в проекте Phoenix.
Давайте начнем!
Перевод выполнен самим автором оригинальной статьи Никитой Соболевым.
Существующие проблемы
Phoenix – новый фреймворк. И, естественно, у него есть некоторые проблемы. Основная команда работала очень старательно, чтобы решить некоторые из самых важных. Итак, каковы эти проблемы?
Директория web — чистая магия
При работе над проектом с использованием Phoenix у вас есть два места для исходного кода: lib/ и web/. Концепция такова:
- Поместите всю свою бизнес-логику и утилиты внутрь
lib/. - Поместите всё, что связано с вашим веб-интерфейсом (контроллеры, представления, шаблоны) внутрь веб-каталога
web/.
Но понятно ли это разработчикам? Я так не думаю.
Откуда появился этот веб-каталог? Это особенность Phoenix? Или другие фреймворки тоже используют его? Должен ли я использовать lib/ с Phoenix-проектами или он зарезервирован для некоторой глубинной магии? Все эти вопросы появились у меня после моей первой встречи с Phoenix.
До версии 1.2 только директория web/ автоматически перезагружалась. Итак, зачем мне создавать какие-либо файлы внутри lib/ и перезапускать сервер, когда я могу поместить их где-то внутри web/ для быстрой перезагрузки?
Это приводит нас к еще более важным вопросам: относятся ли мои файлы-модели (назовем их моделями в этом конкретном контексте) к web-части приложения или к основной логике? Можно ли разделить логику на разные домены или приложения (например, как в Django)?
Эти вопросы остаются без ответа.
Бизнес-логика в контроллерах
Более того, код шаблона, который идет в Phoenix, предполагает другой способ. Можно получить следующий код в новом проекте:
defmodule Example.UserController do use Example.Web, :controller # ... def update(conn, %{"id" => id, "user" => user_params}) do user = Repo.get!(User, id) changeset = User.changeset(user, user_params) case Repo.update(changeset) do {:ok, user} -> render(conn, Example.UserView, "show.json", user: user) {:error, changeset} -> conn |> put_status(:unprocessable_entity) |> render(Example.ChangesetView, "error.json", changeset: changeset) end end end
Что должен делать разработчик, когда пользователю после успешного обновления должно быть отправлено электронное письмо? Контроллер так и просится, чтобы его расширили. Просто поставьте еще одну строку кода перед render/4, что может пойти не так? Но. Только что Phoenix сам подтолкнул нас к неправильному использованию своей кодовой базы: мы пишем бизнес логику в контроллере!
На самом деле, одна дополнительная строка в контроллере это нормально. Все проблемы возникают, когда приложение растет. Таких строк становится много, приложение становится неустойчивым, неподъемным и повторяет само себя.
Схемы не являются моделями
В какой-то момент без особых причин схемы Ecto стали называться «моделями». В чем разница между «моделью» и «схемой»? Схема — это всего лишь способ определить структуру — структуру базы данных в данном конкретном случае. Модели как концепция намного сложнее схем. Модели должны обеспечивать способ управления данными и выполнять различные действия, как модели в Django или Rails. Elixir как функциональный язык не подходит для концепции «модели», поэтому они были упразднены в проекте Ecto.
Файлы внутри models/ не были организованы. По мере своего роста ваше приложение становится хаотичным. Как эти файлы связаны между собой? В каком контексте мы используем их? Это было трудно понять.
Кроме того, директория models/ рассматривалась как еще одно место для размещения вашей бизнес-логики, что нормально для других языков и фреймворков. Существует уже знакомая концепция «fat models». Но такая концепция, опять же, не подходит для Phoenix по уже названным причинам.
Решения
С момента последнего крупного релиза многое изменилось. Самый простой способ показать все изменения — на примере.
Требования
В этом руководстве предполагается, что у вас есть elixir-1.4, и он работает. Нет? Значит, установите его!
Установка
Для начала вам нужно будет установить новую версию Phoenix:
mix archive.install https://github.com/phoenixframework/archives/raw/master/phx_new.ez
Создание нового проекта
По завершению установки надо проверить, всё ли на месте. mix help вернет вам что-то вроде этого:
mix phoenix.new # Creates a new Phoenix v1.1.4 application mix phx.new # Creates a new Phoenix v1.3.0-rc.1 application using the experimental generators
Вот тут и проявляется первое изменение: новые генераторы. Старые генераторы назывались phoenix, а новые — просто phx. Теперь нужно меньше печатать. И, что более важно, новое сообщение разработчикам: эти генераторы новые, они будут делать что-то новое для вашего проекта.
Затем нужно создать структуру нового проекта, запустив:
mix phx.new medium_phx_example --no-html --no-brunch
Прежде чем мы увидим какие-либо результаты этой команды, давайте обсудим параметры. --no-html удаляет некоторые компоненты для работы с html, поэтому phx.gen.html больше не будет работать. Но мы строим json API, и нам не нужен html. Аналогично --no-brunch означает: не создавайте brunch-файл для работы со статикой.
Изменения
Веб-директория
Глядя на ваши новые файлы, вы можете задаться вопросом: где находится веб-директория? Ну, вот и второе изменение. И довольно большое. Теперь ваша веб-директория находится внутри lib/. Она была особенной, многие люди неправильно поняли его главную цель, которая состояла в содержании веб-интерфейса для вашего приложения. Это не место для вашей бизнес-логики. Теперь все ясно. Поместите всё внутрь lib/. И оставьте только свои контроллеры, шаблоны и представления внутри новой web-директории. Вот как это выглядит:
lib └── medium_phx_example ├── application.ex ├── repo.ex └── web ├── channels │ └── user_socket.ex ├── controllers ├── endpoint.ex ├── gettext.ex ├── router.ex ├── views │ ├── error_helpers.ex │ └── error_view.ex └── web.ex
Где medium_phx_example — имя текущего приложения. Приложений может быть много. Итак, теперь весь код живет в одной и той же директории.
Третье изменение откроется вскоре после просмотра файла web.ex:
defmodule MediumPhxExample.Web do def controller do quote do use Phoenix.Controller, namespace: MediumPhxExample.Web import Plug.Conn # Before 1.3 it was just: # import MediumPhxExample.Router.Helpers import MediumPhxExample.Web.Router.Helpers import MediumPhxExample.Web.Gettext end end # Some extra code: # ... end
Phoenix теперь создает пространство имен .Web, которое очень хорошо сочетается с новой файловой структурой.
Создание схемы
Это четвертое и моё любимое изменение. Раньше у нас была директория web/models/, которая использовалась для хранения схем. Теперь концепция моделей полностью мертва. Внедрена новая философия:
- схема представляет структуру данных;
- контекст используется для хранения нескольких схем;
- контекст используется для предоставления публичного внешнего API. Другими словами, он определяет, что можно сделать с вашими данными.
Наше приложение будет содержать только один контекст: Audio. Начнем с создания Audio контекста с двумя схемами Album и Song:
mix phx.gen.json Audio Album albums name:string release:utc_datetime mix phx.gen.json Audio Song songs album_id:references:audio_albums name:string duration:integer
Синтаксис этого генератора также изменился. Теперь требуется, чтобы имя контекста было первым аргументом. Также обратите внимание на audio_albums, схемы теперь содержат префикс с именем контекста. И вот что происходит со структурой проекта после запуска двух генераторов:
lib └── medium_phx_example ├── application.ex ├── audio │ ├── album.ex │ ├── audio.ex │ └── song.ex ├── repo.ex └── web ├── channels │ └── user_socket.ex ├── controllers │ ├── album_controller.ex │ ├── fallback_controller.ex │ └── song_controller.ex ├── endpoint.ex ├── gettext.ex ├── router.ex ├── views │ ├── album_view.ex │ ├── changeset_view.ex │ ├── error_helpers.ex │ ├── error_view.ex │ └── song_view.ex └── web.ex
Каковы основные изменения в структурах по сравнению с предыдущей версией?
- Теперь схемы не принадлежат
web/, а директорияmodels/вообще исчезла. - Схемы теперь разделены контекстом, который определяет, как они связаны друг с другом.
И схемы прямо сейчас являются не более чем описанием таблицы. Чем и должна быть схема в первую очередь. Вот как выглядят наши схемы:
defmodule MediumPhxExample.Audio.Album do use Ecto.Schema schema "audio_albums" do field :name, :string field :release, :utc_datetime timestamps() end end
defmodule MediumPhxExample.Audio.Song do use Ecto.Schema schema "audio_songs" do field :duration, :integer field :name, :string field :album_id, :id timestamps() end end
Всё за исключением самой схемы исчезло. Нет обязательных полей, никаких функций changeset/2 или каких-либо других. Генератор теперь даже не создает belongs_to для вас. Вы сами управляете связями ваших схем.
Итак, теперь это довольно ясно: схема — не место для вашей бизнес-логики. Всё это обрабатывается контекстом, который выглядит следующим образом:
defmodule MediumPhxExample.Audio do @moduledoc """ The boundary for the Audio system. """ import Ecto.{Query, Changeset}, warn: false alias MediumPhxExample.Repo alias MediumPhxExample.Audio.Album def list_albums do Repo.all(Album) end def get_album!(id), do: Repo.get!(Album, id) def create_album(attrs \\ %{}) do %Album{} |> album_changeset(attrs) |> Repo.insert() end # ... defp album_changeset(%Album{} = album, attrs) do album |> cast(attrs, [:name, :release]) |> validate_required([:name, :release]) end alias MediumPhxExample.Audio.Song def list_songs do Repo.all(Song) end def get_song!(id), do: Repo.get!(Song, id) def create_song(attrs \\ %{}) do %Song{} |> song_changeset(attrs) |> Repo.insert() end # ... defp song_changeset(%Song{} = song, attrs) do song |> cast(attrs, [:name, :duration]) |> validate_required([:name, :duration]) end end
Сам вид контекста отправляет ясный посыл: вот место, где нужно поместить свой код! Но будьте осторожны, файлы контекста могут разрастись. Разделите их на несколько модулей в таком случае.
Использование контроллера
Раньше у нас было много кода в контроллере по-умолчанию и разработчику было легко расширить шаблонный код. Здесь появляется пятое изменение. Начиная с нового выпуска, шаблонный код в контроллере был уменьшен и реорганизован:
defmodule MediumPhxExample.Web.AlbumController do use MediumPhxExample.Web, :controller alias MediumPhxExample.Audio alias MediumPhxExample.Audio.Album action_fallback MediumPhxExample.Web.FallbackController # ... def update(conn, %{"id" => id, "album" => album_params}) do album = Audio.get_album!(id) with {:ok, %Album{} = album} <- Audio.update_album(album, album_params) do render(conn, "show.json", album: album) end end # ... end
В действии update/2 теперь есть только три осмысленные строчки кода.
В настоящее время контроллеры используют контексты напрямую, что делает их очень тонким слоем в приложении. Очень трудно найти место для дополнительной логики в контроллере. Что и было основной задачей при реорганизации.
Контроллеры даже не обрабатывают ошибки. Для работы с ошибками предназначен специальный новый fallback_controller. Эта новая концепция — шестое изменение. Оно позволяет иметь все обработчики ошибок и коды ошибок в одном месте:
defmodule MediumPhxExample.Web.FallbackController do @moduledoc """ Translates controller action results into valid `Plug.Conn` responses. See `Phoenix.Controller.action_fallback/1` for more details. """ use MediumPhxExample.Web, :controller def call(conn, {:error, %Ecto.Changeset{} = changeset}) do conn |> put_status(:unprocessable_entity) |> render(MediumPhxExample.Web.ChangesetView, "error.json", changeset: changeset) end def call(conn, {:error, :not_found}) do conn |> put_status(:not_found) |> render(MediumPhxExample.Web.ErrorView, :"404") end end
Что происходит, когда результат из Audio.update_album(album, album_params) не соответствует {:ok, %Album{} = album}? В этой ситуации вызывается контроллер, определенный в action_fallback. И будет выбран правильный call/2, что в свою очередь возвращает правильный ответ. Легко и приятно. Никаких обработок исключений в контроллере.
Заключение
Внесенные изменения весьма интересны. Их много, они все сфокусированы на том, чтобы загубить старые привычки программистов, которые пришли из других языков программирования. И новые изменения стараются пополнить философию Phoenix-Way новыми практиками. Надеюсь, эта статья была полезна и побудила вас использовать Phoenix Framework по максимуму. Заходите ко мне на GitHub.
Благодарим Никиту за подготовку перевода своей собственной оригинальной статьи и с радостью публикуем материал на Хабре. Никита представляет сообщество ElixirLangMoscow, которое организует митапы по Эликсиру в Москве, а также является активным контрибьютером в опенсорс и вносит значительный вклад в наше сообщество Вунш. На сайте вас ждут 3 десятка тематических статей, еженедельная рассылка и новости из мира Эликсира. А для вопросов у нас есть чат в Телеграме с отличными участниками.