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
L-- medium_phx_example
+-- application.ex
+-- repo.ex
L-- web
+-- channels
¦ L-- user_socket.ex
+-- controllers
+-- endpoint.ex
+-- gettext.ex
+-- router.ex
+-- views
¦ +-- error_helpers.ex
¦ L-- error_view.ex
L-- 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
L-- medium_phx_example
+-- application.ex
+-- audio
¦ +-- album.ex
¦ +-- audio.ex
¦ L-- song.ex
+-- repo.ex
L-- web
+-- channels
¦ L-- user_socket.ex
+-- controllers
¦ +-- album_controller.ex
¦ +-- fallback_controller.ex
¦ L-- song_controller.ex
+-- endpoint.ex
+-- gettext.ex
+-- router.ex
+-- views
¦ +-- album_view.ex
¦ +-- changeset_view.ex
¦ +-- error_helpers.ex
¦ +-- error_view.ex
¦ L-- song_view.ex
L-- 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 десятка тематических статей, еженедельная рассылка и новости из мира Эликсира. А для вопросов у нас есть чат в Телеграме с отличными участниками.
begemot_sun
Вот разрабатываешь, разрабатываешь.
А потом приходит чел и говорит, все что мы делали — неверно. Давайте делать так.
И спрашивается и где они раньше были? А что нельзя было сразу подумать сделать?
А вот например, значек ~> в версионинровании зависимостей.
Ну нафига они его везде пихают по умолчанию? Ну ведь много раз обжигались, и все должны знать
что версии зависимостей в проекте должны быть прибиты гвоздями и изменятся только человеком?
Ну нет же, жуют эту жвачку. Ну удачи, такое впечатление что школьники.
sl_bug
~> 1.1.0 означает что обновляться будет только минорная версия, в которой обратные совместимости не ломают, если кто-то делает ~> 1.1, то ССЗБ.
jarosluv
Зависимости и прибиты гвоздями в файле `mix.lock`.
Такая стрелочка `~>` помогает легче обновлять библиотеку вручную, непременно с помощью ручного ввода команды на обновление.
А изменения `1.3` не должны убивать приложения на `1.2`, как можно увидеть – изменения вынесены отдельно и являются в некотором роде экспериментальными.