phoenix-liveview
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChinesePhoenix + 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
undefinedbash
undefinedErlang + 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)
endGenServer 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
endBEAM principles
- Prefer many small processes; processes are cheap and isolated.
- Supervise everything with clear restart strategies.
- Use message passing (/
GenServer.cast) to avoid shared state.send - 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)
endGenServer 模式:封装有状态服务。
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
endBEAM 原则
- 优先使用大量小型进程;进程开销低且相互隔离。
- 用清晰的重启策略监督所有进程。
- 使用消息传递(/
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
endPlugs: 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
endPlugs:可组合的请求中间件。保持插件简洁纯净;跨领域逻辑优先使用管道插件而非控制器插件。
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
endContext 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
endTransactions 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
endContext 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()
endLiveView 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
endHEEx tips
- Prefer to lazily compute expensive data only once per connected session.
assign_new/3 - Use for large lists to minimize diff payloads.
stream/3 - Handle params in for URL-driven state; avoid storing socket state in params.
handle_params/3
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
endPubSub-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)}
endLiveView 模块(服务器端有状态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
endHEEx 技巧
- 优先使用,仅在每个连接会话中延迟计算一次昂贵数据。
assign_new/3 - 对大型列表使用以最小化差异负载。
stream/3 - 在中处理参数以实现URL驱动的状态;避免在参数中存储套接字状态。
handle_params/3
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
endPubSub 驱动的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)}
endPubSub, 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
endPresence 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
endSecurity: authorize topics in , verify user tokens in params/session, and limit payload size.
join/3从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/3Testing Phoenix + LiveView
测试 Phoenix + LiveView
Use with the generated helpers.
mix testelixir
undefined使用结合生成的助手进行测试。
mix testelixir
undefinedtest/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
```elixiruse MyAppWeb.ConnCase, async: true
test "renders home", %{conn: conn} do
conn = get(conn, "/")
assert html_response(conn, 200) =~ "Welcome"
end
```elixirLiveView 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 (). Export via
[:phoenix, :endpoint, ...],:telemetry_poller, andOpentelemetryPhoenix.OpentelemetryEcto - Assets: runs npm install, esbuild, tailwind (if configured), and digests.
mix assets.deploy - Releases: . Configure runtime env in
MIX_ENV=prod mix release. Start withconfig/runtime.exs.PHX_SERVER=true _build/prod/rel/my_app/bin/my_app start - Clustering: add with DNS/epmd strategy for horizontal scale; use distributed PubSub/Presence.
libcluster - 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 metrics, check LiveView diff sizes, avoid large assigns; prefer streams.
:telemetry
- Telemetry:Phoenix 暴露事件()。可通过
[:phoenix, :endpoint, ...]、:telemetry_poller和OpentelemetryPhoenix导出。OpentelemetryEcto - 资源管理:会运行npm install、esbuild、tailwind(若已配置)和摘要处理。
mix assets.deploy - 发布版本:。在
MIX_ENV=prod mix release中配置运行时环境。通过config/runtime.exs启动。PHX_SERVER=true _build/prod/rel/my_app/bin/my_app start - 集群部署:添加并使用DNS/epmd策略实现水平扩展;使用分布式PubSub/Presence。
libcluster - 缓存:对热点路径使用ETS/Cachex;优先使用短TTL并在写入时失效。
- 后台任务:使用Oban实现重试/退避;在应用树中监督它。
- 热点路径检查:启用指标,检查LiveView差异大小,避免大型赋值;优先使用流。
:telemetry
Common Pitfalls
常见陷阱
- Forgetting to subscribe LiveViews to PubSub after check — events will be missed on initial render.
connected?/1 - Doing heavy work inside LiveView render; move to contexts and precompute assigns.
- Not using for multi-step writes; failures leave partial state.
Ecto.Multi - Blocking BEAM schedulers with long NIFs or heavy CPU work; offload to ports/Oban jobs.
- Overusing global ETS without supervision or limits; leak memory.
- 忘记在检查后让LiveView订阅PubSub —— 初始渲染时会错过事件。
connected?/1 - 在LiveView的render中执行繁重工作;应将其移至Contexts并预先计算赋值。
- 多步骤写入时未使用;失败会导致部分状态残留。
Ecto.Multi - 用长时NIF或繁重CPU工作阻塞BEAM调度器;应将其卸载到端口/Oban任务。
- 过度使用无监督或无限制的全局ETS;会导致内存泄漏。
Reference Commands
常用命令
- — list routes and LiveView paths.
mix phx.routes - — generate LiveView CRUD (review context boundaries afterward).
mix phx.gen.live Accounts User users email:string confirmed_at:naive_datetime - — formatting and linting.
mix format && mix credo --strict - — deterministic failures; pair with
mix test --seed 0 --max-failures 1.mix test.watch
Phoenix + LiveView excels when domain logic stays in contexts, LiveViews stay thin, and the BEAM supervises every component for resilience.
- —— 列出路由和LiveView路径。
mix phx.routes - —— 生成LiveView CRUD(之后需检查领域边界)。
mix phx.gen.live Accounts User users email:string confirmed_at:naive_datetime - —— 格式化与代码检查。
mix format && mix credo --strict - —— 确定性失败排查;搭配
mix test --seed 0 --max-failures 1使用。mix test.watch
当领域逻辑存放在Contexts中、LiveView保持轻量,且BEAM监督每个组件以确保韧性时,Phoenix + LiveView 的优势将得到充分发挥。