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

Phoenix LiveView обеспечивает насыщенный пользовательский опыт в реальном времени без написания JavaScript. Он поддерживает постоянное WebSocket-соединение между браузером и сервером, отправляя минимальные diff’ы для обновления DOM. В этом руководстве рассматривается создание интерактивных приложений с LiveView с точки зрения senior-разработчика.

Почему LiveView

LiveView предлагает уникальные преимущества:

  1. Server-Rendered: дружественно к SEO, не требуется JavaScript-фреймворк
  2. Real-Time: автоматические обновления через WebSocket
  3. Minimal Data Transfer: по сети передаются только diff’ы DOM
  4. Elixir Ecosystem: используйте OTP для масштабируемости
  5. State on Server: отсутствие сложности управления состоянием на клиенте

Настройка

Создание Phoenix-проекта с LiveView

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

Добавление LiveView в существующий проект

Добавьте в mix.exs:

defp deps do
[
{:phoenix_live_view, "~> 0.19"},
# ...
]
end

Базовый компонент LiveView

Создание LiveView

lib/myappweb/live/counter_live.ex:

defmodule MyAppWeb.CounterLive do
use MyAppWeb, :live_view
@impl true
def mount(_params, _session, socket) do
{:ok, assign(socket, count: 0)}
end
@impl true
def render(assigns) do
~H"""
<div class="counter">
<h1>Count: <%= @count %></h1>
<button phx-click="increment">+</button>
<button phx-click="decrement">-</button>
<button phx-click="reset">Reset</button>
</div>
"""
end
@impl true
def handle_event("increment", _params, socket) do
{:noreply, update(socket, :count, &(&1 + 1))}
end
def handle_event("decrement", _params, socket) do
{:noreply, update(socket, :count, &(&1 - 1))}
end
def handle_event("reset", _params, socket) do
{:noreply, assign(socket, count: 0)}
end
end

Добавление маршрута

lib/myappweb/router.ex:

scope "/", MyAppWeb do
pipe_through :browser
live "/counter", CounterLive
end

Пример списка задач (Todo) с PubSub

Модуль контекста

lib/my_app/todos.ex:

defmodule MyApp.Todos do
import Ecto.Query
alias MyApp.Repo
alias MyApp.Todos.Todo
@topic inspect(__MODULE__)
def subscribe do
Phoenix.PubSub.subscribe(MyApp.PubSub, @topic)
end
defp broadcast_change({:ok, result}, event) do
Phoenix.PubSub.broadcast(MyApp.PubSub, @topic, {__MODULE__, event, result})
{:ok, result}
end
defp broadcast_change({:error, _} = error, _event), do: error
def list_todos do
Repo.all(from t in Todo, order_by: [desc: t.inserted_at])
end
def get_todo!(id), do: Repo.get!(Todo, id)
def create_todo(attrs \\ %{}) do
%Todo{}
|> Todo.changeset(attrs)
|> Repo.insert()
|> broadcast_change([:todo, :created])
end
def update_todo(%Todo{} = todo, attrs) do
todo
|> Todo.changeset(attrs)
|> Repo.update()
|> broadcast_change([:todo, :updated])
end
def delete_todo(%Todo{} = todo) do
todo
|> Repo.delete()
|> broadcast_change([:todo, :deleted])
end
def toggle_todo(%Todo{} = todo) do
update_todo(todo, %{done: !todo.done})
end
end

Компонент LiveView

lib/myappweb/live/todo_live.ex:

defmodule MyAppWeb.TodoLive do
use MyAppWeb, :live_view
alias MyApp.Todos
@impl true
def mount(_params, _session, socket) do
if connected?(socket) do
Todos.subscribe()
end
{:ok, fetch_todos(socket)}
end
@impl true
def render(assigns) do
~H"""
<div class="todo-app">
<h1>Todo List</h1>
<form phx-submit="add" class="add-form">
<input
type="text"
name="title"
placeholder="What needs to be done?"
autofocus
autocomplete="off"
/>
<button type="submit">Add</button>
</form>
<div class="filters">
<button phx-click="filter" phx-value-status="all"
class={if @filter == "all", do: "active"}>
All
</button>
<button phx-click="filter" phx-value-status="active"
class={if @filter == "active", do: "active"}>
Active
</button>
<button phx-click="filter" phx-value-status="completed"
class={if @filter == "completed", do: "active"}>
Completed
</button>
</div>
<ul class="todo-list">
<%= for todo <- @todos do %>
<li class={if todo.done, do: "completed"}>
<input
type="checkbox"
checked={todo.done}
phx-click="toggle"
phx-value-id={todo.id}
/>
<span><%= todo.title %></span>
<button phx-click="delete" phx-value-id={todo.id}>×</button>
</li>
<% end %>
</ul>
<div class="stats">
<%= length(Enum.filter(@todos, &(!&1.done))) %> items left
</div>
</div>
"""
end
@impl true
def handle_event("add", %{"title" => title}, socket) when title != "" do
Todos.create_todo(%{title: title, done: false})
{:noreply, socket}
end
def handle_event("add", _, socket), do: {:noreply, socket}
def handle_event("toggle", %{"id" => id}, socket) do
todo = Todos.get_todo!(id)
Todos.toggle_todo(todo)
{:noreply, socket}
end
def handle_event("delete", %{"id" => id}, socket) do
todo = Todos.get_todo!(id)
Todos.delete_todo(todo)
{:noreply, socket}
end
def handle_event("filter", %{"status" => status}, socket) do
{:noreply, socket |> assign(filter: status) |> fetch_todos()}
end
# Обрабатывать трансляции PubSub
@impl true
def handle_info({Todos, [:todo | _], _}, socket) do
{:noreply, fetch_todos(socket)}
end
defp fetch_todos(socket) do
todos = Todos.list_todos()
filtered = case socket.assigns[:filter] || "all" do
"active" -> Enum.filter(todos, &(!&1.done))
"completed" -> Enum.filter(todos, &(&1.done))
_ -> todos
end
assign(socket, todos: filtered, filter: socket.assigns[:filter] || "all")
end
end

Обработка форм

Компонент формы LiveView

defmodule MyAppWeb.PostLive.FormComponent do
use MyAppWeb, :live_component
alias MyApp.Blog
@impl true
def render(assigns) do
~H"""
<div class="post-form">
<.form
for={@form}
id="post-form"
phx-target={@myself}
phx-change="validate"
phx-submit="save"
>
<div class="form-group">
<.input field={@form[:title]} label="Title" />
</div>
<div class="form-group">
<.input field={@form[:body]} type="textarea" label="Content" />
</div>
<div class="form-group">
<.input field={@form[:published]} type="checkbox" label="Published" />
</div>
<button type="submit" phx-disable-with="Saving...">
Save Post
</button>
</.form>
</div>
"""
end
@impl true
def update(%{post: post} = assigns, socket) do
changeset = Blog.change_post(post)
{:ok,
socket
|> assign(assigns)
|> assign_form(changeset)}
end
@impl true
def handle_event("validate", %{"post" => post_params}, socket) do
changeset =
socket.assigns.post
|> Blog.change_post(post_params)
|> Map.put(:action, :validate)
{:noreply, assign_form(socket, changeset)}
end
def handle_event("save", %{"post" => post_params}, socket) do
save_post(socket, socket.assigns.action, post_params)
end
defp save_post(socket, :edit, post_params) do
case Blog.update_post(socket.assigns.post, post_params) do
{:ok, post} ->
notify_parent({:saved, post})
{:noreply,
socket
|> put_flash(:info, "Post updated successfully")
|> push_patch(to: socket.assigns.patch)}
{:error, %Ecto.Changeset{} = changeset} ->
{:noreply, assign_form(socket, changeset)}
end
end
defp save_post(socket, :new, post_params) do
case Blog.create_post(post_params) do
{:ok, post} ->
notify_parent({:saved, post})
{:noreply,
socket
|> put_flash(:info, "Post created successfully")
|> push_patch(to: socket.assigns.patch)}
{:error, %Ecto.Changeset{} = changeset} ->
{:noreply, assign_form(socket, changeset)}
end
end
defp assign_form(socket, %Ecto.Changeset{} = changeset) do
assign(socket, :form, to_form(changeset))
end
defp notify_parent(msg), do: send(self(), {__MODULE__, msg})
end

Live-навигация

Паттерн модального окна

defmodule MyAppWeb.PostLive.Index do
use MyAppWeb, :live_view
alias MyApp.Blog
@impl true
def mount(_params, _session, socket) do
{:ok, stream(socket, :posts, Blog.list_posts())}
end
@impl true
def handle_params(params, _url, socket) do
{:noreply, apply_action(socket, socket.assigns.live_action, params)}
end
defp apply_action(socket, :edit, %{"id" => id}) do
socket
|> assign(:page_title, "Edit Post")
|> assign(:post, Blog.get_post!(id))
end
defp apply_action(socket, :new, _params) do
socket
|> assign(:page_title, "New Post")
|> assign(:post, %Blog.Post{})
end
defp apply_action(socket, :index, _params) do
socket
|> assign(:page_title, "Posts")
|> assign(:post, nil)
end
@impl true
def render(assigns) do
~H"""
<.header>
Posts
<:actions>
<.link patch={~p"/posts/new"}>
<.button>New Post</.button>
</.link>
</:actions>
</.header>
<.table id="posts" rows={@streams.posts}>
<:col :let={{_id, post}} label="Title"><%= post.title %></:col>
<:col :let={{_id, post}} label="Status"><%= post.status %></:col>
<:action :let={{_id, post}}>
<.link patch={~p"/posts/#{post}/edit"}>Edit</.link>
</:action>
</.table>
<.modal :if={@live_action in [:new, :edit]} id="post-modal" show on_cancel={JS.patch(~p"/posts")}>
<.live_component
module={MyAppWeb.PostLive.FormComponent}
id={@post.id || :new}
title={@page_title}
action={@live_action}
post={@post}
patch={~p"/posts"}
/>
</.modal>
"""
end
end

Обновления в реальном времени

Отслеживание Presence

Отслеживайте пользователей, просматривающих страницу:

defmodule MyAppWeb.Presence do
use Phoenix.Presence,
otp_app: :my_app,
pubsub_server: MyApp.PubSub
end

В LiveView:

defmodule MyAppWeb.RoomLive do
use MyAppWeb, :live_view
alias MyAppWeb.Presence
@impl true
def mount(%{"id" => room_id}, session, socket) do
topic = "room:#{room_id}"
if connected?(socket) do
Phoenix.PubSub.subscribe(MyApp.PubSub, topic)
{:ok, _} = Presence.track(self(), topic, session["user_id"], %{
online_at: DateTime.utc_now()
})
end
users = Presence.list(topic)
{:ok, assign(socket, room_id: room_id, users: users)}
end
@impl true
def handle_info(%Phoenix.Socket.Broadcast{event: "presence_diff"}, socket) do
users = Presence.list("room:#{socket.assigns.room_id}")
{:noreply, assign(socket, users: users)}
end
end

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

  1. mount/3 для настройки: инициализируйте состояние при запуске LiveView
  2. handle_event/3 для взаимодействий: реагируйте на действия пользователя
  3. handle_info/2 для внешних событий: PubSub, таймеры и т. д.
  4. PubSub для синхронизации между пользователями: транслируйте изменения между соединениями
  5. assign для состояния: всё состояние хранится в assigns сокета
  6. Streams для больших списков: эффективные обновления DOM для коллекций

LiveView предоставляет интерактивность SPA, при этом сохраняя сложность на стороне сервера, что делает его идеальным для команд, уверенно работающих с Elixir и желающих реализовать функции реального времени без JavaScript-фреймворков.

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