phoenix-liveview について
Phoenix、LiveView、Elixir、BEAM を使う開発スキルで、リアルタイム Web アプリの構築を扱います。サーバー側 UI、OTP 監視、ドメインコンテキスト、データ処理、テスト、運用を解説します。
主な機能
- 01WebSocket 経由のサーバー描画 LiveView UI
- 02OTP による監視と分離プロセス
- 03Ecto スキーマとクエリによるドメインコンテキスト
- 04PubSub 更新と Presence トラッキング
- 05Phoenix と LiveView のテストパターン
- 06デプロイ、テレメトリー、性能のガイド
活用シーン
- 01クライアント側コードを抑えてリアルタイム UI を構築
- 02ドメインロジックと DB 操作を整理
- 03Web アプリにライブ更新やオンライン状態を追加
- 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— パイプライン、スコープ、ルート、LiveSessionslib/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 の強みが発揮されます。