О нас Руководства Проекты Контакты
Админка
пожалуйста подождите

Elixir — функциональный конкурентный язык программирования, построенный на Erlang VM (BEAM), известный своей отказоустойчивостью и масштабируемостью. Phoenix — ведущий веб-фреймворк для Elixir, обеспечивающий продуктивность уровня Rails при характеристиках производительности, которые сделали Erlang знаменитым в телеком-системах. Это руководство посвящено созданию готовых к production веб-приложений на Phoenix с точки зрения senior-разработчика.

Почему Elixir и Phoenix

Elixir даёт несколько уникальных преимуществ:

  1. Конкурентность: лёгковесные процессы (не OS threads) позволяют обслуживать миллионы одновременных подключений
  2. Отказоустойчивость: деревья супервизоров автоматически перезапускают упавшие процессы
  3. Горячая перезагрузка кода: развёртывайте обновления без разрыва соединений
  4. Функциональное программирование: неизменяемые структуры данных предотвращают целые классы ошибок
  5. Встроенная поддержка real-time: WebSockets и PubSub являются первоклассными сущностями

Настройка проекта Elixir

Сначала установите Elixir в вашей системе. На CentOS/RHEL:

# Добавьте репозиторий EPEL
sudo dnf install epel-release
sudo dnf install elixir erlang

Установите helper для Phoenix и создайте проект:

mix archive.install hex phx_new
mix phx.new my_app
cd my_app
mix ecto.setup
mix phx.server

Структура проекта Phoenix

Понимание структуры папок критически важно:

my_app/
├── lib/
│ ├── my_app/ # Бизнес-логика (контексты)
│ │ ├── accounts.ex # Контекст Accounts
│ │ └── accounts/
│ │ ├── user.ex # Схема User
│ │ └── credential.ex
│ └── my_app_web/ # Веб-слой
│ ├── controllers/
│ ├── views/
│ ├── templates/
│ ├── channels/ # Каналы WebSocket
│ └── router.ex
├── priv/
│ └── repo/migrations/ # Миграции базы данных
├── assets/ # Фронтенд-ассеты
└── config/ # Файлы конфигурации

Основы синтаксиса Elixir

У Elixir характерный синтаксис, который активно использует pattern matching:

defmodule MyApp.Calculator do
# Pattern matching в заголовках функций
def divide(_, 0) do
{:error, "Division by zero"}
end
def divide(x, y) do
{:ok, x / y}
end
# Pipe operator для связывания функций в цепочку
def process_data(data) do
data
|> String.trim()
|> String.downcase()
|> String.split(",")
end
# Анонимные функции
def map_values(list) do
Enum.map(list, fn x -> x * 2 end)
# Или более короткий синтаксис: Enum.map(list, &(&1 * 2))
end
end

Pattern matching для извлечения значений:

# Извлечь из map
%{id: user_id} = %{id: 123, name: "John"}
# user_id теперь 123
# Извлечь из string
"rows:" <> count = "rows:456"
# count теперь "456"

Создание CRUD-приложения

Определите маршруты

В lib/myappweb/router.ex:

defmodule MyAppWeb.Router do
use MyAppWeb, :router
pipeline :browser do
plug :accepts, ["html"]
plug :fetch_session
plug :fetch_live_flash
plug :put_root_layout, {MyAppWeb.LayoutView, :root}
plug :protect_from_forgery
plug :put_secure_browser_headers
end
scope "/", MyAppWeb do
pipe_through :browser
# RESTful resources
resources "/users", UserController
# Или определите по отдельности:
get "/posts", PostController, :index
get "/posts/new", PostController, :new
post "/posts", PostController, :create
get "/posts/:id/edit", PostController, :edit
put "/posts/:id", PostController, :update
delete "/posts/:id", PostController, :delete
end
end

Создайте миграции

Сгенерируйте миграцию:

mix ecto.gen.migration create_posts

Отредактируйте файл миграции в priv/repo/migrations/:

defmodule MyApp.Repo.Migrations.CreatePosts do
use Ecto.Migration
def change do
create table(:posts) do
add :title, :string, null: false
add :body, :text
add :published, :boolean, default: false
add :user_id, references(:users, on_delete: :delete_all), null: false
timestamps()
end
create index(:posts, [:user_id])
create index(:posts, [:published])
end
end

Запустите миграции:

mix ecto.migrate

Определите схемы (модели)

Создайте lib/my_app/blog/post.ex:

defmodule MyApp.Blog.Post do
use Ecto.Schema
import Ecto.Changeset
# Настройте JSON-сериализацию
@derive {Jason.Encoder, only: [:id, :title, :body, :published, :inserted_at]}
schema "posts" do
field :title, :string
field :body, :string
field :published, :boolean, default: false
belongs_to :user, MyApp.Accounts.User
has_many :comments, MyApp.Blog.Comment
timestamps()
end
def changeset(post, attrs) do
post
|> cast(attrs, [:title, :body, :published])
|> validate_required([:title])
|> validate_length(:title, min: 3, max: 255)
|> assoc_constraint(:user)
end
end

Создайте контекст (бизнес-логика)

Создайте lib/my_app/blog.ex:

defmodule MyApp.Blog do
import Ecto.Query
alias MyApp.Repo
alias MyApp.Blog.Post
def list_posts do
Post
|> order_by(desc: :inserted_at)
|> Repo.all()
|> Repo.preload(:user)
end
def list_published_posts do
Post
|> where(published: true)
|> order_by(desc: :inserted_at)
|> Repo.all()
|> Repo.preload(:user)
end
def get_post!(id) do
Post
|> Repo.get!(id)
|> Repo.preload([:user, :comments])
end
def create_post(user, attrs \\ %{}) do
%Post{}
|> Post.changeset(attrs)
|> Ecto.Changeset.put_assoc(:user, user)
|> Repo.insert()
end
def update_post(%Post{} = post, attrs) do
post
|> Post.changeset(attrs)
|> Repo.update()
end
def delete_post(%Post{} = post) do
Repo.delete(post)
end
end

Реализуйте контроллер

Создайте lib/myappweb/controllers/post_controller.ex:

defmodule MyAppWeb.PostController do
use MyAppWeb, :controller
alias MyApp.Blog
alias MyApp.Blog.Post
def index(conn, _params) do
posts = Blog.list_posts()
render(conn, "index.html", posts: posts)
end
def new(conn, _params) do
changeset = Post.changeset(%Post{}, %{})
render(conn, "new.html", changeset: changeset)
end
def create(conn, %{"post" => post_params}) do
user = conn.assigns.current_user
case Blog.create_post(user, post_params) do
{:ok, post} ->
conn
|> put_flash(:info, "Post created successfully.")
|> redirect(to: Routes.post_path(conn, :show, post))
{:error, changeset} ->
render(conn, "new.html", changeset: changeset)
end
end
def show(conn, %{"id" => id}) do
post = Blog.get_post!(id)
render(conn, "show.html", post: post)
end
def edit(conn, %{"id" => id}) do
post = Blog.get_post!(id)
changeset = Post.changeset(post, %{})
render(conn, "edit.html", post: post, changeset: changeset)
end
def update(conn, %{"id" => id, "post" => post_params}) do
post = Blog.get_post!(id)
case Blog.update_post(post, post_params) do
{:ok, post} ->
conn
|> put_flash(:info, "Post updated successfully.")
|> redirect(to: Routes.post_path(conn, :show, post))
{:error, changeset} ->
render(conn, "edit.html", post: post, changeset: changeset)
end
end
def delete(conn, %{"id" => id}) do
post = Blog.get_post!(id)
{:ok, _post} = Blog.delete_post(post)
conn
|> put_flash(:info, "Post deleted successfully.")
|> redirect(to: Routes.post_path(conn, :index))
end
end

Создайте шаблоны

Создайте lib/myappweb/templates/post/index.html.heex:

<h1>Posts</h1>
<%= for post <- @posts do %>
<div class="post-item">
<h2><%= link post.title, to: Routes.post_path(@conn, :show, post) %></h2>
<p>By <%= post.user.name %> on <%= post.inserted_at %></p>
<div class="actions">
<%= link "Edit", to: Routes.post_path(@conn, :edit, post) %>
<%= link "Delete", to: Routes.post_path(@conn, :delete, post),
method: :delete, data: [confirm: "Are you sure?"] %>
</div>
</div>
<% end %>
<%= link "New Post", to: Routes.post_path(@conn, :new), class: "btn btn-primary" %>

Аутентификация с использованием sessions

Реализуйте аутентификацию пользователей, следуя паттернам Phoenix.

Создайте схему учётных данных

mix phx.gen.context Accounts Credential credentials email:string:unique password_hash:string user_id:references:users

Session Controller

Создайте lib/myappweb/controllers/session_controller.ex:

defmodule MyAppWeb.SessionController do
use MyAppWeb, :controller
alias MyApp.Accounts
def new(conn, _params) do
render(conn, "new.html")
end
def create(conn, %{"user" => %{"email" => email, "password" => password}}) do
case Accounts.authenticate_by_email_password(email, password) do
{:ok, user} ->
conn
|> put_flash(:info, "Welcome back!")
|> put_session(:user_id, user.id)
|> configure_session(renew: true)
|> redirect(to: "/")
{:error, :unauthorized} ->
conn
|> put_flash(:error, "Invalid email or password")
|> redirect(to: Routes.session_path(conn, :new))
end
end
def delete(conn, _params) do
conn
|> configure_session(drop: true)
|> put_flash(:info, "Signed out successfully")
|> redirect(to: "/")
end
end

Authentication Plug

Добавьте в router для защищённых маршрутов:

defmodule MyAppWeb.Router do
# ...
pipeline :authenticate do
plug :require_authenticated_user
end
scope "/admin", MyAppWeb.Admin do
pipe_through [:browser, :authenticate]
resources "/posts", PostController
end
defp require_authenticated_user(conn, _opts) do
case get_session(conn, :user_id) do
nil ->
conn
|> put_flash(:error, "Please sign in to access this page")
|> redirect(to: Routes.session_path(conn, :new))
|> halt()
user_id ->
assign(conn, :current_user, Accounts.get_user!(user_id))
end
end
end

Plugs (Middleware)

Plugs — это компонуемые модули, которые преобразуют соединение:

defmodule MyAppWeb.Plugs.LoadCurrentUser do
import Plug.Conn
import Phoenix.Controller
alias MyApp.Accounts
def init(opts), do: opts
def call(conn, _opts) do
case get_session(conn, :user_id) do
nil ->
assign(conn, :current_user, nil)
user_id ->
user = Accounts.get_user!(user_id)
assign(conn, :current_user, user)
end
end
end

Запуск в production с Docker

Создайте Dockerfile:

# Этап сборки
FROM elixir:1.15-alpine AS builder
RUN apk add --no-cache build-base git
WORKDIR /app
ENV MIX_ENV=prod
# Установите hex и rebar
RUN mix local.hex --force && mix local.rebar --force
# Скопируйте файлы зависимостей
COPY mix.exs mix.lock ./
RUN mix deps.get --only prod
RUN mix deps.compile
# Скопируйте файлы приложения
COPY lib lib
COPY priv priv
COPY assets assets
COPY config config
# Скомпилируйте ассеты
RUN mix assets.deploy
# Создайте release
RUN mix release
# Этап runtime
FROM alpine:3.18 AS runtime
RUN apk add --no-cache libstdc++ openssl ncurses-libs
WORKDIR /app
COPY --from=builder /app/_build/prod/rel/my_app ./
ENV HOME=/app
ENV MIX_ENV=prod
CMD ["bin/my_app", "start"]

Ключевые выводы

  1. Контексты организуют бизнес-логику: держите web-слой тонким, выносите логику в контексты
  2. Pattern matching повсюду: используйте его в заголовках функций для более чистого кода
  3. Pipe operator для читаемости: естественно выстраивайте цепочки преобразований
  4. Plugs — это middleware: компонуйте пайплайны обработки запросов
  5. Деревья супервизоров для надёжности: процессы автоматически перезапускаются при сбоях
  6. Ecto для слоя данных: changesets обеспечивают явную валидацию данных

Phoenix сочетает элегантность функционального программирования с практичными паттернами веб-разработки, что делает его отличным выбором для создания масштабируемых и сопровождаемых приложений.

 
 
 
Языки
Темы
Copyright © 1999 — 2026
Зетка Интерактив