关于 phoenix-liveview
介绍 Phoenix、LiveView、Elixir 和 BEAM 的开发技能,适用于构建实时 Web 应用,涵盖服务端交互界面、OTP 监督、领域上下文、数据访问、测试与运维。
主要功能
- 01通过 WebSockets 提供服务端 LiveView 界面
- 02OTP 监督与隔离进程
- 03使用 Ecto 模式和查询组织领域上下文
- 04PubSub 更新与 Presence 跟踪
- 05Phoenix 和 LiveView 测试模式
- 06部署、遥测与性能指南
适用场景
- 01以少量客户端代码构建实时界面
- 02组织领域逻辑和数据库操作
- 03为 Web 应用添加实时更新或在线状态
- 04测试 Phoenix 路由和 LiveView 交互
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ bobmatnyc/ claude-mpm-skills --skill phoenix-liveview安装后在 Claude Code 中输入/phoenix-liveview确认已加载。
附带文件metadata.json
详细介绍
Phoenix + LiveView(Elixir/BEAM)
Phoenix 基于 Elixir 和 BEAM VM 构建,能够以极少的 JavaScript 实现容错的实时 Web 应用。LiveView 将 UI 状态保留在服务器上,同时通过 WebSocket 流式传输 HTML 差异。BEAM 提供轻量级进程、监督树、热代码升级和软实时调度。
核心理念
- OTP 监督机制让 Web、数据和后台进程彼此隔离,并可在故障后重新启动。
- Context 以 Ecto schema 和查询为基础,定义领域边界(例如 Accounts、Billing)。
- LiveView 在服务器上渲染 HTML,通过 WebSocket 同步 UI 状态,只需极少的客户端代码。
- PubSub + Presence 支持扇出更新、状态跟踪和协作功能。
环境和项目设置
# Erlang + Elixir via asdf (recommended)
asdf install erlang 27.0
asdf install elixir 1.17.3
asdf global erlang 27.0 elixir 1.17.3
# Install Phoenix generator
mix archive.install hex phx_new
# Create project with LiveView + Ecto + esbuild
mix phx.new my_app --live
cd my_app
mix deps.get
mix ecto.create
mix phx.server
项目布局(主要部分):
lib/my_app/application.ex— OTP 监督树(Repo、Endpoint、Telemetry、PubSub、Oban 等)lib/my_app_web/endpoint.ex— Endpoint、plugs、sockets、LiveView 配置lib/my_app_web/router.ex— Pipelines、scopes、routes、LiveSessionslib/my_app/— Context(领域模块)和 Ecto schematest/support/{conn_case,data_case}.ex— Ecto + Phoenix 测试辅助工具
BEAM + OTP 基础知识
监督树(application.ex):保持精简,并隔离各个子进程。
def start(_type, _args) do
children = [
MyApp.Repo,
{Phoenix.PubSub, name: MyApp.PubSub},
MyAppWeb.Endpoint,
{Oban, Application.fetch_env!(:my_app, Oban)},
MyApp.Metrics
]
Supervisor.start_link(children, strategy: :one_for_one, name: MyApp.Supervisor)
end
GenServer 模式:封装有状态服务。
defmodule MyApp.Counter do
use GenServer
def start_link(initial \\ 0), do: GenServer.start_link(__MODULE__, initial, name: __MODULE__)
def increment(), do: GenServer.call(__MODULE__, :inc)
@impl true
def handle_call(:inc, _from, state) do
new_state = state + 1
{:reply, new_state, new_state}
end
end
BEAM 原则
- 优先使用多个小进程;进程开销低且相互隔离。
- 使用明确的重启策略监督所有进程。
- 使用消息传递(
GenServer.cast/send)来避免共享状态。 - 使用 ETS/Cachex 实现内存缓存,并确保其受监督。
Phoenix 架构和路由
Pipelines 和 scopes(router.ex):将浏览器和 API 相关关注点分开。
defmodule MyAppWeb.Router do
use MyAppWeb, :router
pipeline :browser do
plug :accepts, ["html"]
plug :fetch_session
plug :fetch_live_flash
plug :protect_from_forgery
plug :put_secure_browser_headers
plug :fetch_current_user
end
pipeline :api do
plug :accepts, ["json"]
end
scope "/", MyAppWeb do
pipe_through :browser
live "/", HomeLive
resources "/users", UserController
end
scope "/api", MyAppWeb do
pipe_through :api
resources "/users", Api.UserController, except: [:new, :edit]
end
end
Plugs:可组合的请求中间件。让 plugs 保持纯粹且简短;处理横切关注点时,优先使用 pipeline plugs,而非 controller plugs。
Context 和 Ecto
Schema + changeset
defmodule MyApp.Accounts.User do
use Ecto.Schema
import Ecto.Changeset
schema "users" do
field :email, :string
field :hashed_password, :string
field :confirmed_at, :naive_datetime
timestamps()
end
def registration_changeset(user, attrs) do
user
|> cast(attrs, [:email, :password])
|> validate_required([:email, :password])
|> validate_format(:email, ~r/@/)
|> validate_length(:password, min: 12)
|> unique_constraint(:email)
|> put_password_hash()
end
defp put_password_hash(%{valid?: true} = changeset),
do: put_change(changeset, :hashed_password, Argon2.hash_pwd_salt(get_change(changeset, :password)))
defp put_password_hash(changeset), do: changeset
end
Context API
defmodule MyApp.Accounts do
import Ecto.Query, warn: false
alias MyApp.{Repo, Accounts.User}
def list_users, do: Repo.all(User)
def get_user!(id), do: Repo.get!(User, id)
def register_user(attrs) do
%User{}
|> User.registration_changeset(attrs)
|> Repo.insert()
end
end
使用 Ecto.Multi 进行事务处理
alias Ecto.Multi
def register_and_welcome(attrs) do
Multi.new()
|> Multi.insert(:user, User.registration_changeset(%User{}, attrs))
|> Multi.run(:welcome_email, fn _repo, %{user: user} ->
MyApp.Mailer.deliver_welcome(user)
{:ok, :sent}
end)
|> Repo.transaction()
end
LiveView 模式
LiveView 模块(服务器上的有状态 UI)
defmodule MyAppWeb.CounterLive do
use MyAppWeb, :live_view
def mount(_params, _session, socket) do
{:ok, assign(socket, count: 0)}
end
def handle_event("inc", _params, socket) do
{:noreply, update(socket, :count, &(&1 + 1))}
end
def render(assigns) do
~H"""
<div class="space-y-4">
<p class="text-lg">Count: <%= @count %></p>
<button phx-click="inc" class="btn">Increment</button>
</div>
"""
end
end
HEEx 提示
- 优先使用
assign_new/3,在每个已连接会话中仅延迟计算一次开销较大的数据。 - 对于大型列表,使用
stream/3,以尽量减小差异数据量。 - 对于由 URL 驱动的状态,在
handle_params/3中处理参数;避免将 socket 状态存储在参数中。
Live Components
defmodule MyAppWeb.NavComponent do
use MyAppWeb, :live_component
def render(assigns) do
~H"""
<nav>
<%= for item <- @items do %>
<.link navigate={item.href}><%= item.label %></.link>
<% end %>
</nav>
"""
end
end
由 PubSub 驱动的 LiveView
@impl true
def mount(_params, _session, socket) do
if connected?(socket), do: Phoenix.PubSub.subscribe(MyApp.PubSub, "orders")
{:ok, assign(socket, orders: [])}
end
@impl true
def handle_info({:order_created, order}, socket) do
{:noreply, update(socket, :orders, fn orders -> [order | orders] end)}
end
PubSub、Channels 和 Presence
从 Context 广播变更
def create_order(attrs) do
with {:ok, order} <- %Order{} |> Order.changeset(attrs) |> Repo.insert() do
Phoenix.PubSub.broadcast(MyApp.PubSub, "orders", {:order_created, order})
{:ok, order}
end
end
使用 Presence 显示在线/正在输入状态
defmodule MyAppWeb.RoomChannel do
use Phoenix.Channel
alias Phoenix.Presence
def join("room:" <> room_id, _payload, socket) do
send(self(), :after_join)
{:ok, assign(socket, :room_id, room_id)}
end
def handle_info(:after_join, socket) do
Presence.track(socket, socket.assigns.user_id, %{online_at: System.system_time(:second)})
push(socket, "presence_state", Presence.list(socket))
{:noreply, socket}
end
end
安全性:在 join/3 中授权 topic,在参数/会话中验证用户令牌,并限制负载大小。
测试 Phoenix + LiveView
使用生成的辅助工具运行 mix test。
# test/support/conn_case.ex
use MyAppWeb.ConnCase, async: true
test "renders home", %{conn: conn} do
conn = get(conn, "/")
assert html_response(conn, 200) =~ "Welcome"
end
# LiveView test
use MyAppWeb.ConnCase, async: true
import Phoenix.LiveViewTest
test "counter increments", %{conn: conn} do
{:ok, view, _html} = live(conn, "/counter")
view |> element("button", "Increment") |> render_click()
assert render(view) =~ "Count: 1"
end
DataCase:提供沙盒化的数据库连接;将测试包装在事务中,以隔离数据。
Fixtures:使用 ExMachina 构建工厂,或在 test/support/fixtures 下创建简单的辅助模块。
性能、运维和部署
- Telemetry:Phoenix 提供事件(
[:phoenix, :endpoint, ...])。使用:telemetry_poller、OpentelemetryPhoenix和OpentelemetryEcto导出。 - Assets:
mix assets.deploy会运行 npm install、esbuild、tailwind(如果已配置)并生成摘要。 - Releases:
MIX_ENV=prod mix release。在config/runtime.exs中配置运行时环境。使用PHX_SERVER=true _build/prod/rel/my_app/bin/my_app start启动。 - 集群:添加
libcluster并采用 DNS/epmd 策略以实现水平扩展;使用分布式 PubSub/Presence。 - 缓存:对热点路径使用 ETS/Cachex;优先采用较短的 TTL,并在写入时使缓存失效。
- 后台作业:使用 Oban 实现重试/退避;在 application 树中监督它。
- 热点路径检查:启用
:telemetry指标,检查 LiveView 差异大小,避免使用大型 assigns;优先使用 streams。
常见陷阱
- 在检查
connected?/1后忘记让 LiveView 订阅 PubSub——初始渲染时会错过事件。 - 在 LiveView 渲染过程中执行繁重的工作;应将其移至上下文中,并预先计算 assigns。
- 多步写入时未使用
Ecto.Multi;发生故障会留下部分状态。 - 让长时间运行的 NIF 或繁重的 CPU 工作阻塞 BEAM 调度器;应将其转移到 ports/Oban jobs。
- 过度使用全局 ETS,却没有监督机制或限制;会导致内存泄漏。
参考命令
mix phx.routes— 列出路由和 LiveView 路径。mix phx.gen.live Accounts User users email:string confirmed_at:naive_datetime— 生成 LiveView CRUD(之后检查上下文边界)。mix format && mix credo --strict— 格式化和 lint。mix test --seed 0 --max-failures 1— 确定性地复现故障;与mix test.watch搭配使用。
当领域逻辑留在上下文中、LiveView 保持精简,并由 BEAM 监督每个组件以确保弹性时,Phoenix + LiveView 就能发挥出色。