phoenix-liveview

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Phoenix + LiveView (Elixir/BEAM)

Phoenix + LiveView (Elixir/BEAM)

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
  • OTP supervision keeps web, data, and background processes isolated and restartable.
  • Contexts encode domain boundaries (e.g., Accounts, Billing) around Ecto schemas and queries.
  • LiveView renders HTML on the server, syncing UI state over WebSockets with minimal client code.
  • PubSub + Presence enable fan-out updates, tracking, and collaboration features.

Phoenix 基于 Elixir 和 BEAM 虚拟机,可构建容错性强、实时的Web应用,且所需JavaScript代码极少。LiveView 将UI状态保留在服务器端,同时通过WebSocket流式传输HTML差异。BEAM 提供轻量级进程、监督树、热代码升级和软实时调度能力。
核心概念
  • OTP 监督机制可将Web、数据和后台进程隔离开,并支持重启。
  • Contexts 围绕Ecto模式和查询定义领域边界(例如:Accounts、Billing)。
  • LiveView 在服务器端渲染HTML,通过WebSocket同步UI状态,仅需极少客户端代码。
  • PubSub + Presence 支持扇出更新、跟踪和协作功能。

Environment and Project Setup

环境与项目搭建

bash
undefined
bash
undefined

Erlang + Elixir via asdf (recommended)

通过asdf安装Erlang + Elixir(推荐方式)

asdf install erlang 27.0 asdf install elixir 1.17.3 asdf global erlang 27.0 elixir 1.17.3
asdf install erlang 27.0 asdf install elixir 1.17.3 asdf global erlang 27.0 elixir 1.17.3

Install Phoenix generator

安装Phoenix生成器

mix archive.install hex phx_new
mix archive.install hex phx_new

Create project with LiveView + Ecto + esbuild

创建包含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 config
- `lib/my_app_web/router.ex` — Pipelines, scopes, routes, LiveSessions
- `lib/my_app/` — Contexts (domain modules) and Ecto schemas
- `test/support/{conn_case,data_case}.ex` — Testing helpers for Ecto + Phoenix

---
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` — 管道、作用域、路由、Live会话
- `lib/my_app/` — Contexts(领域模块)和Ecto模式
- `test/support/{conn_case,data_case}.ex` — Ecto + Phoenix的测试助手

---

BEAM + OTP Essentials

BEAM + OTP 核心要点

Supervision tree (application.ex): keep short, isolated children.
elixir
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.
elixir
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
  • Prefer many small processes; processes are cheap and isolated.
  • Supervise everything with clear restart strategies.
  • Use message passing (
    GenServer.cast
    /
    send
    ) to avoid shared state.
  • Use ETS/Cachex for in-memory caches; keep them supervised.

监督树(application.ex):保持子进程简洁、独立。
elixir
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 模式:封装有状态服务。
elixir
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 Anatomy and Routing

Phoenix 架构与路由

Pipelines and scopes (router.ex): keep browser/api concerns separated.
elixir
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.

管道与作用域(router.ex):分离浏览器/API相关逻辑。
elixir
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:可组合的请求中间件。保持插件简洁纯净;跨领域逻辑优先使用管道插件而非控制器插件。

Contexts and Ecto

Contexts 与 Ecto

Schema + changeset
elixir
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
elixir
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
elixir
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

模式 + 变更集
elixir
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
elixir
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处理事务
elixir
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 Patterns

LiveView 模式

LiveView module (stateful UI on server)
elixir
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
  • Prefer
    assign_new/3
    to lazily compute expensive data only once per connected session.
  • Use
    stream/3
    for large lists to minimize diff payloads.
  • Handle params in
    handle_params/3
    for URL-driven state; avoid storing socket state in params.
Live Components
elixir
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
elixir
@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

LiveView 模块(服务器端有状态UI)
elixir
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驱动的状态;避免在参数中存储套接字状态。
Live 组件
elixir
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
elixir
@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, and Presence

PubSub、Channels 与 Presence

Broadcast changes from contexts
elixir
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
elixir
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.

从Contexts广播变更
elixir
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实现在线/输入状态指示器
elixir
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
中授权主题,验证参数/会话中的用户令牌,并限制负载大小。

Testing Phoenix + LiveView

测试 Phoenix + LiveView

Use
mix test
with the generated helpers.
elixir
undefined
使用
mix test
结合生成的助手进行测试。
elixir
undefined

test/support/conn_case.ex

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

```elixir
use MyAppWeb.ConnCase, async: true
test "renders home", %{conn: conn} do conn = get(conn, "/") assert html_response(conn, 200) =~ "Welcome" end

```elixir

LiveView test

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**: provide sandboxed DB connections; wrap tests in transactions to isolate data.

**Fixtures**: build factories with `ExMachina` or simple helper modules under `test/support/fixtures`.

---
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`下的简单助手模块构建工厂。

---

Performance, Ops, and Deployment

性能、运维与部署

  • Telemetry: Phoenix exposes events (
    [:phoenix, :endpoint, ...]
    ). Export via
    :telemetry_poller
    ,
    OpentelemetryPhoenix
    , and
    OpentelemetryEcto
    .
  • Assets:
    mix assets.deploy
    runs npm install, esbuild, tailwind (if configured), and digests.
  • Releases:
    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
    .
  • Clustering: add
    libcluster
    with DNS/epmd strategy for horizontal scale; use distributed PubSub/Presence.
  • Caching: use ETS/Cachex for hot paths; prefer short TTLs and invalidate on write.
  • Background jobs: Oban for retries/backoff; supervise it in application tree.
  • Hot path checks: enable
    :telemetry
    metrics, check LiveView diff sizes, avoid large assigns; prefer streams.

  • 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
    启动。
  • 集群部署:添加
    libcluster
    并使用DNS/epmd策略实现水平扩展;使用分布式PubSub/Presence。
  • 缓存:对热点路径使用ETS/Cachex;优先使用短TTL并在写入时失效。
  • 后台任务:使用Oban实现重试/退避;在应用树中监督它。
  • 热点路径检查:启用
    :telemetry
    指标,检查LiveView差异大小,避免大型赋值;优先使用流。

Common Pitfalls

常见陷阱

  • Forgetting to subscribe LiveViews to PubSub after
    connected?/1
    check — events will be missed on initial render.
  • Doing heavy work inside LiveView render; move to contexts and precompute assigns.
  • Not using
    Ecto.Multi
    for multi-step writes; failures leave partial state.
  • Blocking BEAM schedulers with long NIFs or heavy CPU work; offload to ports/Oban jobs.
  • Overusing global ETS without supervision or limits; leak memory.

  • 忘记在
    connected?/1
    检查后让LiveView订阅PubSub —— 初始渲染时会错过事件。
  • 在LiveView的render中执行繁重工作;应将其移至Contexts并预先计算赋值。
  • 多步骤写入时未使用
    Ecto.Multi
    ;失败会导致部分状态残留。
  • 用长时NIF或繁重CPU工作阻塞BEAM调度器;应将其卸载到端口/Oban任务。
  • 过度使用无监督或无限制的全局ETS;会导致内存泄漏。

Reference Commands

常用命令

  • 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.
  • 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
    使用。
当领域逻辑存放在Contexts中、LiveView保持轻量,且BEAM监督每个组件以确保韧性时,Phoenix + LiveView 的优势将得到充分发挥。