phoenix-api-channels
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChinesePhoenix APIs, Channels, and Presence (Elixir/BEAM)
Phoenix API、Channels 和 Presence(Elixir/BEAM)
Phoenix excels at REST/JSON APIs and WebSocket Channels with minimal boilerplate, leveraging the BEAM for fault tolerance, lightweight processes, and supervised PubSub/Presence.
Core pillars
- Controllers for JSON APIs with plugs, pipelines, and versioning.
- Contexts own data (Ecto schemas + queries) and expose a narrow API to controllers/channels.
- Channels + PubSub for fan-out real-time updates; Presence for tracking users/devices.
- Auth via plugs (session/cookie for browser, token/Bearer for APIs), with signed params.
Phoenix 在 REST/JSON API 和 WebSocket Channels 方面表现出色,所需样板代码极少,借助 BEAM 实现容错、轻量级进程以及受监管的 PubSub/Presence。
核心支柱
- 用于 JSON API 的控制器,支持插件(plugs)、管道(pipelines)和版本控制。
- 上下文(Contexts)负责数据管理(Ecto 模式 + 查询),并向控制器/Channels 暴露精简的 API。
- Channels + PubSub 用于实时更新的扇出分发;Presence 用于跟踪用户/设备。
- 通过插件实现认证(浏览器使用会话/ Cookie,API 使用令牌/Bearer),支持签名参数。
Project Setup
项目设置
bash
mix phx.new my_api --no-html --no-live
cd my_api
mix deps.get
mix ecto.create
mix phx.serverKey files:
- — plugs, sockets, instrumentation
lib/my_api_web/endpoint.ex - — pipelines, scopes, versioning, sockets
lib/my_api_web/router.ex - — REST/JSON controllers
lib/my_api_web/controllers/* - — contexts + Ecto schemas (ownership of data logic)
lib/my_api/* - — Channel modules
lib/my_api_web/channels/*
bash
mix phx.new my_api --no-html --no-live
cd my_api
mix deps.get
mix ecto.create
mix phx.server关键文件:
- — 插件、套接字、埋点监控
lib/my_api_web/endpoint.ex - — 管道、作用域、版本控制、套接字
lib/my_api_web/router.ex - — REST/JSON 控制器
lib/my_api_web/controllers/* - — 上下文 + Ecto 模式(数据逻辑的归属)
lib/my_api/* - — Channel 模块
lib/my_api_web/channels/*
Routing and Pipelines
路由与管道
Separate browser vs API pipelines; version APIs with scopes.
elixir
defmodule MyApiWeb.Router do
use MyApiWeb, :router
pipeline :api do
plug :accepts, ["json"]
plug :fetch_session
plug :protect_from_forgery
plug MyApiWeb.Plugs.RequireAuth
end
scope "/api", MyApiWeb do
pipe_through :api
scope "/v1", V1, as: :v1 do
resources "/users", UserController, except: [:new, :edit]
post "/sessions", SessionController, :create
end
end
socket "/socket", MyApiWeb.UserSocket,
websocket: [connect_info: [:peer_data, :x_headers]],
longpoll: false
endTips
- Keep pipelines short; push auth/guards into plugs.
- Expose for Channels; restrict transports as needed.
socket "/socket"
分离浏览器与 API 管道;通过作用域实现 API 版本控制。
elixir
defmodule MyApiWeb.Router do
use MyApiWeb, :router
pipeline :api do
plug :accepts, ["json"]
plug :fetch_session
plug :protect_from_forgery
plug MyApiWeb.Plugs.RequireAuth
end
scope "/api", MyApiWeb do
pipe_through :api
scope "/v1", V1, as: :v1 do
resources "/users", UserController, except: [:new, :edit]
post "/sessions", SessionController, :create
end
end
socket "/socket", MyApiWeb.UserSocket,
websocket: [connect_info: [:peer_data, :x_headers]],
longpoll: false
end提示
- 保持管道精简;将认证/守卫逻辑移至插件中。
- 暴露 以支持 Channels;根据需要限制传输方式。
socket "/socket"
Controllers and Plugs
控制器与插件
Controllers stay thin; contexts own the logic.
elixir
defmodule MyApiWeb.V1.UserController do
use MyApiWeb, :controller
alias MyApi.Accounts
action_fallback MyApiWeb.FallbackController
def index(conn, _params) do
users = Accounts.list_users()
render(conn, :index, users: users)
end
def create(conn, params) do
with {:ok, user} <- Accounts.register_user(params) do
conn
|> put_status(:created)
|> put_resp_header("location", ~p\"/api/v1/users/#{user.id}\")
|> render(:show, user: user)
end
end
endFallbackController centralizes error translation ( → 404 JSON).
{:error, :not_found}Plugs
- verifies bearer/session tokens, sets
RequireAuth.current_user - Use -style transforms in pipelines, not controllers.
plug :scrub_params - Avoid heavy work in plugs; they run per-request.
控制器保持精简;数据逻辑由上下文负责。
elixir
defmodule MyApiWeb.V1.UserController do
use MyApiWeb, :controller
alias MyApi.Accounts
action_fallback MyApiWeb.FallbackController
def index(conn, _params) do
users = Accounts.list_users()
render(conn, :index, users: users)
end
def create(conn, params) do
with {:ok, user} <- Accounts.register_user(params) do
conn
|> put_status(:created)
|> put_resp_header("location", ~p\"/api/v1/users/#{user.id}\")
|> render(:show, user: user)
end
end
endFallbackController 集中处理错误转换(例如将 转换为 404 JSON 响应)。
{:error, :not_found}插件
- 验证 Bearer/会话令牌,设置
RequireAuth。current_user - 在管道中使用 这类转换逻辑,而非控制器中。
plug :scrub_params - 避免在插件中执行繁重操作;它们会在每个请求时运行。
Contexts and Data (Ecto)
上下文与数据(Ecto)
Contexts expose only what controllers/channels need.
elixir
defmodule MyApi.Accounts do
import Ecto.Query, warn: false
alias MyApi.{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
endGuidelines
- Keep schema modules free of controller knowledge.
- Validate at the changeset; use for multi-step operations.
Ecto.Multi - Prefer pagination helpers (,
Scrivener) for large lists.Flop
上下文仅暴露控制器/Channels 所需的内容。
elixir
defmodule MyApi.Accounts do
import Ecto.Query, warn: false
alias MyApi.{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指导原则
- 模式模块中不要包含控制器相关逻辑。
- 在 changeset 中进行验证;使用 处理多步骤操作。
Ecto.Multi - 对于大型列表,优先使用分页助手(如 、
Scrivener)。Flop
Channels, PubSub, and Presence
Channels、PubSub 与 Presence
Channel module example:
elixir
defmodule MyApiWeb.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
def handle_in("message:new", %{"body" => body}, socket) do
broadcast!(socket, "message:new", %{user_id: socket.assigns.user_id, body: body})
{:noreply, socket}
end
endPubSub from contexts
elixir
def create_order(attrs) do
with {:ok, order} <- %Order{} |> Order.changeset(attrs) |> Repo.insert() do
Phoenix.PubSub.broadcast(MyApi.PubSub, "orders", {:order_created, order})
{:ok, order}
end
endBest practices
- Authorize in before joining topics.
UserSocket.connect/3 - Limit payload sizes; validate incoming events.
- Use topic partitioning for tenancy ().
"tenant:" <> tenant_id <> ":room:" <> room_id
Channel 模块示例:
elixir
defmodule MyApiWeb.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
def handle_in("message:new", %{"body" => body}, socket) do
broadcast!(socket, "message:new", %{user_id: socket.assigns.user_id, body: body})
{:noreply, socket}
end
end从上下文触发 PubSub
elixir
def create_order(attrs) do
with {:ok, order} <- %Order{} |> Order.changeset(attrs) |> Repo.insert() do
Phoenix.PubSub.broadcast(MyApi.PubSub, "orders", {:order_created, order})
{:ok, order}
end
end最佳实践
- 在加入主题前,在 中完成授权。
UserSocket.connect/3 - 限制 payload 大小;验证传入事件。
- 对于多租户场景,使用主题分区(例如 )。
"tenant:" <> tenant_id <> ":room:" <> room_id
Authentication Patterns
认证模式
- API tokens: Accept ; verify in plug, assign
authorization: Bearer <token>.current_user - Signed params: for short-lived join params.
Phoenix.Token.sign/verify - Rate limiting: Use plugs + ETS/Cachex or reverse proxy (NGINX/Cloudflare).
- CORS: Configure in with
Endpoint.cors_plug
- API 令牌:接收 ;在插件中验证并设置
authorization: Bearer <token>。current_user - 签名参数:使用 生成短期的加入参数。
Phoenix.Token.sign/verify - 速率限制:使用插件 + ETS/Cachex 或反向代理(如 NGINX/Cloudflare)实现。
- CORS:在 中通过
Endpoint配置。cors_plug
Testing
测试
Use generated helpers:
elixir
defmodule MyApiWeb.UserControllerTest do
use MyApiWeb.ConnCase, async: true
test "lists users", %{conn: conn} do
conn = get(conn, ~p\"/api/v1/users\")
assert json_response(conn, 200)["data"] == []
end
endChannel tests:
elixir
defmodule MyApiWeb.RoomChannelTest do
use MyApiWeb.ChannelCase, async: true
test "broadcasts messages" do
{:ok, _, socket} = connect(MyApiWeb.UserSocket, %{"token" => "abc"})
{:ok, _, socket} = subscribe_and_join(socket, "room:123", %{})
ref = push(socket, "message:new", %{"body" => "hi"})
assert_reply ref, :ok
assert_broadcast "message:new", %{body: "hi"}
end
endDataCase: isolates DB per test; use fixtures/factories for setup.
使用生成的助手函数:
elixir
defmodule MyApiWeb.UserControllerTest do
use MyApiWeb.ConnCase, async: true
test "lists users", %{conn: conn} do
conn = get(conn, ~p\"/api/v1/users\")
assert json_response(conn, 200)["data"] == []
end
endChannel 测试:
elixir
defmodule MyApiWeb.RoomChannelTest do
use MyApiWeb.ChannelCase, async: true
test "broadcasts messages" do
{:ok, _, socket} = connect(MyApiWeb.UserSocket, %{"token" => "abc"})
{:ok, _, socket} = subscribe_and_join(socket, "room:123", %{})
ref = push(socket, "message:new", %{"body" => "hi"})
assert_reply ref, :ok
assert_broadcast "message:new", %{body: "hi"}
end
endDataCase:为每个测试隔离数据库;使用 fixtures/factories 进行测试准备。
Telemetry, Observability, and Ops
遥测、可观测性与运维
- events from endpoint, controller, channel, and Ecto queries; export via
:telemetryandOpentelemetryPhoenix.OpentelemetryEcto - Use for request metrics; add logging metadata (request_id, user_id).
Plug.Telemetry - Releases: ; configure runtime in
MIX_ENV=prod mix release.config/runtime.exs - Clustering: + distributed PubSub for multi-node Presence.
libcluster - Assetless APIs: disable unused watchers (esbuild/tailwind) for API-only apps.
- 端点、控制器、Channel 和 Ecto 查询会产生 事件;可通过
:telemetry和OpentelemetryPhoenix导出。OpentelemetryEcto - 使用 收集请求指标;添加日志元数据(如 request_id、user_id)。
Plug.Telemetry - 发布版本:执行 ;在
MIX_ENV=prod mix release中配置运行时参数。config/runtime.exs - 集群部署:使用 + 分布式 PubSub 实现多节点 Presence。
libcluster - 无资产 API:对于仅 API 应用,禁用未使用的监视器(如 esbuild/tailwind)。
Common Pitfalls
常见陷阱
- Controllers doing queries directly instead of delegating to contexts.
- Not authorizing in , leading to topic exposure.
UserSocket.connect/3 - Missing → inconsistent error shapes.
action_fallback - Forgetting to limit event payloads; large messages can overwhelm channels.
- Leaving longpoll enabled when unused; disable to reduce surface area.
Phoenix API + Channels shine when contexts own data, controllers stay thin, and Channels use PubSub/Presence with strict authorization and telemetry. The BEAM handles concurrency and fault tolerance; focus on clear boundaries and real-time experiences.
- 控制器直接执行查询,而非委托给上下文。
- 未在 中进行授权,导致主题暴露。
UserSocket.connect/3 - 缺少 ,导致错误响应格式不一致。
action_fallback - 忘记限制事件 payload 大小;大消息可能导致 Channel 过载。
- 在未使用长轮询时仍保持其启用状态;应禁用以减少攻击面。
当上下文负责数据管理、控制器保持精简,且 Channels 结合严格授权与遥测使用 PubSub/Presence 时,Phoenix API + Channels 的优势将充分发挥。BEAM 负责处理并发与容错;开发者只需专注于清晰的边界划分和实时体验的构建。