mcpField
P
phoenix-liveview

phoenix-liveview

github.com/bobmatnyc開発78421 回インストール15 週間前に更新
リポジトリ

phoenix-liveview について

Phoenix、LiveView、Elixir、BEAM を使う開発スキルで、リアルタイム Web アプリの構築を扱います。サーバー側 UI、OTP 監視、ドメインコンテキスト、データ処理、テスト、運用を解説します。

主な機能

  1. 01WebSocket 経由のサーバー描画 LiveView UI
  2. 02OTP による監視と分離プロセス
  3. 03Ecto スキーマとクエリによるドメインコンテキスト
  4. 04PubSub 更新と Presence トラッキング
  5. 05Phoenix と LiveView のテストパターン
  6. 06デプロイ、テレメトリー、性能のガイド

活用シーン

  1. 01クライアント側コードを抑えてリアルタイム UI を構築
  2. 02ドメインロジックと DB 操作を整理
  3. 03Web アプリにライブ更新やオンライン状態を追加
  4. 04Phoenix のルートと 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、データ、バックグラウンドの各プロセスを分離し、再起動可能にします。
  • コンテキストは、Ecto のスキーマやクエリを中心に、ドメインの境界(例: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、プラグ、ソケット、LiveView の設定
  • lib/my_app_web/router.ex — パイプライン、スコープ、ルート、LiveSessions
  • lib/my_app/ — コンテキスト(ドメインモジュール)と Ecto スキーマ
  • 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 の構成とルーティング

パイプラインとスコープ(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

プラグ:組み合わせ可能なリクエストミドルウェアです。プラグは純粋で短く保ち、横断的な処理にはコントローラープラグよりもパイプラインプラグを優先します。


コンテキストと Ecto

スキーマとチェンジセット

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 のパターン

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 でパラメーターを処理します。ソケットの状態をパラメーターに保存するのは避けてください。

Live コンポーネント

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、チャネル、Presence

コンテキストから変更をブロードキャストする

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 でトピックを認可し、パラメーター/セッション内のユーザートークンを検証し、ペイロードサイズを制限します。


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:サンドボックス化された DB 接続を提供し、データを分離するためにテストをトランザクションでラップします。

フィクスチャ:ExMachina または test/support/fixtures 配下のシンプルなヘルパーモジュールを使ってファクトリーを作成します。


パフォーマンス、運用、デプロイ

  • Telemetry:Phoenix はイベント([: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 で起動します。
  • クラスタリング:水平スケールのために DNS/epmd 戦略を使った libcluster を追加し、分散 PubSub/Presence を使用します。
  • キャッシュ:ホットパスには ETS/Cachex を使用します。短い TTL を優先し、書き込み時に無効化します。
  • バックグラウンドジョブ:再試行/バックオフには Oban を使用し、アプリケーションツリーで監督します。
  • ホットパスの確認::telemetry メトリクスを有効にし、LiveView の差分サイズを確認して、大きな assigns を避けます。streams を優先します。

よくある落とし穴

  • connected?/1 のチェック後に LiveView を PubSub に登録し忘れると、初回レンダリング時のイベントを受け取れません。
  • LiveView のレンダリング内で重い処理を行わないでください。コンテキストに移し、割り当て値を事前に計算してください。
  • 複数ステップの書き込みに Ecto.Multi を使わないと、失敗時に状態が部分的に更新されたままになります。
  • 長時間実行される NIF や重い CPU 処理で BEAM スケジューラーをブロックしないでください。ポートや Oban ジョブにオフロードしてください。
  • 監視や上限なしにグローバル 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 — フォーマットとリントを実行します。
  • mix test --seed 0 --max-failures 1 — 決定的な失敗を再現します。mix test.watch と併用してください。

ドメインロジックをコンテキストに置き、LiveView を薄く保ち、BEAM がすべてのコンポーネントを監視して耐障害性を確保すると、Phoenix + LiveView の強みが発揮されます。

コメント