mcpField
P
phoenix-liveview

phoenix-liveview

github.com/bobmatnyc开发78421 次安装15 周前更新
代码仓库

关于 phoenix-liveview

介绍 Phoenix、LiveView、Elixir 和 BEAM 的开发技能,适用于构建实时 Web 应用,涵盖服务端交互界面、OTP 监督、领域上下文、数据访问、测试与运维。

主要功能

  1. 01通过 WebSockets 提供服务端 LiveView 界面
  2. 02OTP 监督与隔离进程
  3. 03使用 Ecto 模式和查询组织领域上下文
  4. 04PubSub 更新与 Presence 跟踪
  5. 05Phoenix 和 LiveView 测试模式
  6. 06部署、遥测与性能指南

适用场景

  1. 01以少量客户端代码构建实时界面
  2. 02组织领域逻辑和数据库操作
  3. 03为 Web 应用添加实时更新或在线状态
  4. 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、LiveSessions
  • lib/my_app/ — Context(领域模块)和 Ecto schema
  • test/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 就能发挥出色。

评论

支持 Markdown