phoenix-liveview by bobmatnyc/claude-mpm-skills
npx skills add https://github.com/bobmatnyc/claude-mpm-skills --skill phoenix-liveviewPhoenix 基于 Elixir 和 BEAM 虚拟机构建,以最少的 JavaScript 提供容错的实时 Web 应用程序。LiveView 将 UI 状态保持在服务器上,同时通过 WebSocket 流式传输 HTML 差异。BEAM 提供轻量级进程、监督树、热代码升级和软实时调度。
核心理念
# 通过 asdf 安装 Erlang + Elixir(推荐)
asdf install erlang 27.0
asdf install elixir 1.17.3
asdf global erlang 27.0 elixir 1.17.3
# 安装 Phoenix 生成器
mix archive.install hex phx_new
# 创建包含 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 — 端点、插件、套接字、LiveView 配置lib/my_app_web/router.ex — 管道、作用域、路由、LiveSession广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
lib/my_app/ — 上下文(领域模块)和 Ecto 模式test/support/{conn_case,data_case}.ex — Ecto + Phoenix 的测试辅助工具监督树 (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)以避免共享状态。管道与作用域 (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
插件 :可组合的请求中间件。保持插件纯粹且简短;在横切关注点上,优先使用管道插件而非控制器插件。
模式 + 变更集
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
上下文 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 模块(服务器上的有状态 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 以最小化差异负载。handle_params/3 中处理参数以驱动 URL 状态;避免将套接字状态存储在参数中。实时组件
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
从上下文广播变更
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 中授权主题,验证参数/会话中的用户令牌,并限制负载大小。
使用 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 测试
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 :提供沙盒化的数据库连接;将测试包装在事务中以隔离数据。
固件 :使用 ExMachina 或 test/support/fixtures 下的简单辅助模块构建工厂。
[:phoenix, :endpoint, ...])。通过 :telemetry_poller、OpentelemetryPhoenix 和 OpentelemetryEcto 导出。mix assets.deploy 运行 npm install、esbuild、tailwind(如果已配置)并进行摘要处理。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。:telemetry 指标,检查 LiveView 差异大小,避免大型赋值;优先使用流。connected?/1 检查后让 LiveView 订阅 PubSub — 初始渲染时将错过事件。Ecto.Multi;失败会留下部分状态。mix phx.routes — 列出路由和 LiveView 路径。mix phx.gen.live Accounts User users email:string confirmed_at:naive_datetime — 生成 LiveView CRUD(之后检查上下文边界)。mix format && mix credo --strict — 格式化和代码检查。mix test --seed 0 --max-failures 1 — 确定性失败;与 mix test.watch 配合使用。当领域逻辑保持在上下文中、LiveView 保持精简、且 BEAM 监督每个组件以实现弹性时,Phoenix + LiveView 表现出色。
每周安装数
91
代码仓库
GitHub 星标数
18
首次出现
2026年1月23日
安全审计
安装于
opencode70
codex69
gemini-cli68
claude-code66
github-copilot64
cursor59
Phoenix builds on Elixir and the BEAM VM to deliver fault-tolerant, real-time web applications with minimal JavaScript. LiveView keeps UI state on the server while streaming HTML diffs over WebSockets. The BEAM provides lightweight processes, supervision trees, hot code upgrades, and soft-realtime scheduling.
Key ideas
# 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
Project layout (key pieces):
lib/my_app/application.ex — OTP supervision tree (Repo, Endpoint, Telemetry, PubSub, Oban, etc.)lib/my_app_web/endpoint.ex — Endpoint, plugs, sockets, LiveView configlib/my_app_web/router.ex — Pipelines, scopes, routes, LiveSessionslib/my_app/ — Contexts (domain modules) and Ecto schemastest/support/{conn_case,data_case}.ex — Testing helpers for Ecto + PhoenixSupervision tree (application.ex) : keep short, isolated children.
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 pattern : wrap stateful services.
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 principles
GenServer.cast/send) to avoid shared state.Pipelines and scopes (router.ex) : keep browser/api concerns separated.
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 : composable request middleware. Keep plugs pure and short; prefer pipeline plugs over controller plugs when cross-cutting.
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
Transactions with 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 module (stateful UI on server)
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 tips
assign_new/3 to lazily compute expensive data only once per connected session.stream/3 for large lists to minimize diff payloads.handle_params/3 for URL-driven state; avoid storing socket state in params.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-driven 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
Broadcast changes from contexts
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 for online/typing indicators
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
Security : authorize topics in join/3, verify user tokens in params/session, and limit payload size.
Use mix test with the generated helpers.
# 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 : provide sandboxed DB connections; wrap tests in transactions to isolate data.
Fixtures : build factories with ExMachina or simple helper modules under test/support/fixtures.
[:phoenix, :endpoint, ...]). Export via :telemetry_poller, OpentelemetryPhoenix, and OpentelemetryEcto.mix assets.deploy runs npm install, esbuild, tailwind (if configured), and digests.MIX_ENV=prod mix release. Configure runtime env in config/runtime.exs. Start with PHX_SERVER=true _build/prod/rel/my_app/bin/my_app start.libcluster with DNS/epmd strategy for horizontal scale; use distributed PubSub/Presence.connected?/1 check — events will be missed on initial render.Ecto.Multi for multi-step writes; failures leave partial state.mix phx.routes — list routes and LiveView paths.mix phx.gen.live Accounts User users email:string confirmed_at:naive_datetime — generate LiveView CRUD (review context boundaries afterward).mix format && mix credo --strict — formatting and linting.mix test --seed 0 --max-failures 1 — deterministic failures; pair with mix test.watch.Phoenix + LiveView excels when domain logic stays in contexts, LiveViews stay thin, and the BEAM supervises every component for resilience.
Weekly Installs
91
Repository
GitHub Stars
18
First Seen
Jan 23, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykPass
Installed on
opencode70
codex69
gemini-cli68
claude-code66
github-copilot64
cursor59
React 组合模式指南:Vercel 组件架构最佳实践,提升代码可维护性
116,600 周安装
:telemetry metrics, check LiveView diff sizes, avoid large assigns; prefer streams.