phoenix-framework
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChinesePhoenix Framework Development
Phoenix Framework 开发
This skill activates when working with Phoenix web applications, including setup, development, LiveView, contexts, controllers, and channels.
本技能适用于Phoenix Web应用相关工作场景,包括项目搭建、开发、LiveView实现、上下文设计、控制器开发以及Channels配置等。
When to Use This Skill
适用场景
Activate this skill when:
- Creating or modifying Phoenix applications
- Implementing LiveView components or pages
- Working with Phoenix contexts and business logic
- Building real-time features with channels or LiveView
- Configuring Phoenix routers, plugs, or endpoints
- Troubleshooting Phoenix-specific issues
在以下场景中激活本技能:
- 创建或修改Phoenix应用
- 实现LiveView组件或页面
- 处理Phoenix上下文与业务逻辑
- 基于Channels或LiveView构建实时功能
- 配置Phoenix路由、Plugs或Endpoints
- 排查Phoenix相关问题
Phoenix Project Structure
Phoenix项目结构
Follow Phoenix conventions:
lib/
my_app/ # Business logic and contexts
accounts/ # Domain contexts
repo.ex
my_app_web/ # Web interface
controllers/
live/ # LiveView modules
components/ # Function components
router.ex
endpoint.ex遵循Phoenix的约定:
lib/
my_app/ # Business logic and contexts
accounts/ # Domain contexts
repo.ex
my_app_web/ # Web interface
controllers/
live/ # LiveView modules
components/ # Function components
router.ex
endpoint.exContext-Driven Design
上下文驱动设计
Organize business logic into contexts (bounded domains):
将业务逻辑组织到上下文(限界领域)中:
Creating Contexts
创建上下文
Generate contexts with related schemas:
bash
mix phx.gen.context Accounts User users email:string name:stringStructure contexts to encapsulate business logic:
elixir
defmodule MyApp.Accounts do
@moduledoc """
The Accounts context - manages user accounts and authentication.
"""
alias MyApp.Repo
alias MyApp.Accounts.User
def list_users do
Repo.all(User)
end
def get_user!(id), do: Repo.get!(User, id)
def create_user(attrs \\ %{}) do
%User{}
|> User.changeset(attrs)
|> Repo.insert()
end
def update_user(%User{} = user, attrs) do
user
|> User.changeset(attrs)
|> Repo.update()
end
end生成包含相关Schema的上下文:
bash
mix phx.gen.context Accounts User users email:string name:string结构化上下文以封装业务逻辑:
elixir
defmodule MyApp.Accounts do
@moduledoc """
The Accounts context - manages user accounts and authentication.
"""
alias MyApp.Repo
alias MyApp.Accounts.User
def list_users do
Repo.all(User)
end
def get_user!(id), do: Repo.get!(User, id)
def create_user(attrs \\ %{}) do
%User{}
|> User.changeset(attrs)
|> Repo.insert()
end
def update_user(%User{} = user, attrs) do
user
|> User.changeset(attrs)
|> Repo.update()
end
endContext Best Practices
上下文最佳实践
- Keep contexts focused on a single domain
- Avoid cross-context dependencies when possible
- Use public API functions, not direct Repo access in web layer
- Name contexts after business domains, not technical layers
- 保持上下文聚焦于单一领域
- 尽可能避免跨上下文依赖
- 在Web层使用公开API函数,而非直接访问Repo
- 以业务领域命名上下文,而非技术层
LiveView Development
LiveView开发
LiveView enables rich, real-time experiences without writing JavaScript.
LiveView无需编写JavaScript即可实现丰富的实时交互体验。
LiveView Lifecycle
LiveView生命周期
Understand the mount → handle_event → render cycle:
elixir
defmodule MyAppWeb.UserLive.Index do
use MyAppWeb, :live_view
alias MyApp.Accounts
@impl true
def mount(_params, _session, socket) do
# Runs on initial page load and live connection
{:ok, assign(socket, :users, list_users())}
end
@impl true
def handle_params(params, _url, socket) do
# Runs after mount and on live patch
{:noreply, apply_action(socket, socket.assigns.live_action, params)}
end
@impl true
def handle_event("delete", %{"id" => id}, socket) do
user = Accounts.get_user!(id)
{:ok, _} = Accounts.delete_user(user)
{:noreply, assign(socket, :users, list_users())}
end
@impl true
def render(assigns) do
~H"""
<div>
<.table rows={@users} id="users">
<:col :let={user} label="Name"><%= user.name %></:col>
<:col :let={user} label="Email"><%= user.email %></:col>
<:action :let={user}>
<.button phx-click="delete" phx-value-id={user.id}>Delete</.button>
</:action>
</.table>
</div>
"""
end
defp list_users do
Accounts.list_users()
end
end理解mount → handle_event → render的循环流程:
elixir
defmodule MyAppWeb.UserLive.Index do
use MyAppWeb, :live_view
alias MyApp.Accounts
@impl true
def mount(_params, _session, socket) do
# Runs on initial page load and live connection
{:ok, assign(socket, :users, list_users())}
end
@impl true
def handle_params(params, _url, socket) do
# Runs after mount and on live patch
{:noreply, apply_action(socket, socket.assigns.live_action, params)}
end
@impl true
def handle_event("delete", %{"id" => id}, socket) do
user = Accounts.get_user!(id)
{:ok, _} = Accounts.delete_user(user)
{:noreply, assign(socket, :users, list_users())}
end
@impl true
def render(assigns) do
~H"""
<div>
<.table rows={@users} id="users">
<:col :let={user} label="Name"><%= user.name %></:col>
<:col :let={user} label="Email"><%= user.email %></:col>
<:action :let={user}>
<.button phx-click="delete" phx-value-id={user.id}>Delete</.button>
</:action>
</.table>
</div>
"""
end
defp list_users do
Accounts.list_users()
end
endLiveView Best Practices
LiveView最佳实践
- Use for initial data loading
mount/3 - Handle route changes in
handle_params/3 - Keep renders fast - compute in event handlers, not render
- Use for expensive computations
assign_new/3 - Prefer LiveView over JavaScript for interactive UIs
- Use and
phx-debouncefor frequent eventsphx-throttle
- 使用加载初始数据
mount/3 - 在中处理路由变更
handle_params/3 - 保持渲染速度——在事件处理器中计算,而非渲染阶段
- 使用处理昂贵的计算操作
assign_new/3 - 对于交互式UI,优先使用LiveView而非JavaScript
- 对频繁触发的事件使用和
phx-debouncephx-throttle
Function Components
函数组件
Create reusable components:
elixir
defmodule MyAppWeb.Components.UserCard do
use Phoenix.Component
attr :user, :map, required: true
attr :class, :string, default: ""
def user_card(assigns) do
~H"""
<div class={"card " <> @class}>
<h3><%= @user.name %></h3>
<p><%= @user.email %></p>
</div>
"""
end
endUse with in templates.
<.user_card user={@current_user} />创建可复用组件:
elixir
defmodule MyAppWeb.Components.UserCard do
use Phoenix.Component
attr :user, :map, required: true
attr :class, :string, default: ""
def user_card(assigns) do
~H"""
<div class={"card " <> @class}>
<h3><%= @user.name %></h3>
<p><%= @user.email %></p>
</div>
"""
end
end在模板中通过使用组件。
<.user_card user={@current_user} />Form Handling
表单处理
Use changesets for validation:
elixir
@impl true
def mount(_params, _session, socket) do
changeset = Accounts.change_user(%User{})
{:ok, assign(socket, form: to_form(changeset))}
end
@impl true
def handle_event("validate", %{"user" => user_params}, socket) do
changeset =
%User{}
|> Accounts.change_user(user_params)
|> Map.put(:action, :validate)
{:noreply, assign(socket, form: to_form(changeset))}
end
@impl true
def handle_event("save", %{"user" => user_params}, socket) do
case Accounts.create_user(user_params) do
{:ok, user} ->
{:noreply,
socket
|> put_flash(:info, "User created successfully")
|> push_navigate(to: ~p"/users/#{user}")}
{:error, %Ecto.Changeset{} = changeset} ->
{:noreply, assign(socket, form: to_form(changeset))}
end
end
def render(assigns) do
~H"""
<.form for={@form} phx-change="validate" phx-submit="save">
<.input field={@form[:name]} label="Name" />
<.input field={@form[:email]} label="Email" type="email" />
<.button>Save</.button>
</.form>
"""
end使用Changeset进行验证:
elixir
@impl true
def mount(_params, _session, socket) do
changeset = Accounts.change_user(%User{})
{:ok, assign(socket, form: to_form(changeset))}
end
@impl true
def handle_event("validate", %{"user" => user_params}, socket) do
changeset =
%User{}
|> Accounts.change_user(user_params)
|> Map.put(:action, :validate)
{:noreply, assign(socket, form: to_form(changeset))}
end
@impl true
def handle_event("save", %{"user" => user_params}, socket) do
case Accounts.create_user(user_params) do
{:ok, user} ->
{:noreply,
socket
|> put_flash(:info, "User created successfully")
|> push_navigate(to: ~p"/users/#{user}")}
{:error, %Ecto.Changeset{} = changeset} ->
{:noreply, assign(socket, form: to_form(changeset))}
end
end
def render(assigns) do
~H"""
<.form for={@form} phx-change="validate" phx-submit="save">
<.input field={@form[:name]} label="Name" />
<.input field={@form[:email]} label="Email" type="email" />
<.button>Save</.button>
</.form>
"""
endRouting
路由
Route Organization
路由组织
Structure routes logically:
elixir
defmodule MyAppWeb.Router do
use MyAppWeb, :router
pipeline :browser do
plug :accepts, ["html"]
plug :fetch_session
plug :fetch_live_flash
plug :put_root_layout, html: {MyAppWeb.Layouts, :root}
plug :protect_from_forgery
plug :put_secure_browser_headers
end
pipeline :api do
plug :accepts, ["json"]
end
scope "/", MyAppWeb do
pipe_through :browser
live "/", HomeLive, :index
live "/users", UserLive.Index, :index
live "/users/new", UserLive.Index, :new
live "/users/:id", UserLive.Show, :show
end
scope "/api", MyAppWeb do
pipe_through :api
resources "/users", UserController, except: [:new, :edit]
end
end按逻辑结构配置路由:
elixir
defmodule MyAppWeb.Router do
use MyAppWeb, :router
pipeline :browser do
plug :accepts, ["html"]
plug :fetch_session
plug :fetch_live_flash
plug :put_root_layout, html: {MyAppWeb.Layouts, :root}
plug :protect_from_forgery
plug :put_secure_browser_headers
end
pipeline :api do
plug :accepts, ["json"]
end
scope "/", MyAppWeb do
pipe_through :browser
live "/", HomeLive, :index
live "/users", UserLive.Index, :index
live "/users/new", UserLive.Index, :new
live "/users/:id", UserLive.Show, :show
end
scope "/api", MyAppWeb do
pipe_through :api
resources "/users", UserController, except: [:new, :edit]
end
endLiveView Routes
LiveView路由
Use live actions for modal/overlay states:
elixir
live "/users", UserLive.Index, :index
live "/users/new", UserLive.Index, :new
live "/users/:id/edit", UserLive.Index, :editThen handle in :
handle_params/3elixir
defp apply_action(socket, :edit, %{"id" => id}) do
socket
|> assign(:page_title, "Edit User")
|> assign(:user, Accounts.get_user!(id))
end
defp apply_action(socket, :new, _params) do
socket
|> assign(:page_title, "New User")
|> assign(:user, %User{})
end
defp apply_action(socket, :index, _params) do
socket
|> assign(:page_title, "Listing Users")
|> assign(:user, nil)
end使用Live Actions实现模态框/覆盖层状态:
elixir
live "/users", UserLive.Index, :index
live "/users/new", UserLive.Index, :new
live "/users/:id/edit", UserLive.Index, :edit随后在中处理:
handle_params/3elixir
defp apply_action(socket, :edit, %{"id" => id}) do
socket
|> assign(:page_title, "Edit User")
|> assign(:user, Accounts.get_user!(id))
end
defp apply_action(socket, :new, _params) do
socket
|> assign(:page_title, "New User")
|> assign(:user, %User{})
end
defp apply_action(socket, :index, _params) do
socket
|> assign(:page_title, "Listing Users")
|> assign(:user, nil)
endChannels and PubSub
Channels与PubSub
Phoenix Channels
Phoenix Channels
For custom real-time protocols:
elixir
defmodule MyAppWeb.RoomChannel do
use MyAppWeb, :channel
@impl true
def join("room:" <> room_id, _payload, socket) do
if authorized?(socket, room_id) do
{:ok, assign(socket, :room_id, room_id)}
else
{:error, %{reason: "unauthorized"}}
end
end
@impl true
def handle_in("new_msg", %{"body" => body}, socket) do
broadcast!(socket, "new_msg", %{body: body, user: socket.assigns.user})
{:noreply, socket}
end
end适用于自定义实时协议场景:
elixir
defmodule MyAppWeb.RoomChannel do
use MyAppWeb, :channel
@impl true
def join("room:" <> room_id, _payload, socket) do
if authorized?(socket, room_id) do
{:ok, assign(socket, :room_id, room_id)}
else
{:error, %{reason: "unauthorized"}}
end
end
@impl true
def handle_in("new_msg", %{"body" => body}, socket) do
broadcast!(socket, "new_msg", %{body: body, user: socket.assigns.user})
{:noreply, socket}
end
endPhoenix PubSub
Phoenix PubSub
For LiveView updates and process communication:
elixir
undefined适用于LiveView更新与进程间通信:
elixir
undefinedSubscribe in mount
Subscribe in mount
def mount(_params, _session, socket) do
if connected?(socket) do
Phoenix.PubSub.subscribe(MyApp.PubSub, "users")
end
{:ok, assign(socket, :users, list_users())}
end
def mount(_params, _session, socket) do
if connected?(socket) do
Phoenix.PubSub.subscribe(MyApp.PubSub, "users")
end
{:ok, assign(socket, :users, list_users())}
end
Handle broadcasts
Handle broadcasts
def handle_info({:user_created, user}, socket) do
{:noreply, update(socket, :users, fn users -> [user | users] end)}
end
def handle_info({:user_created, user}, socket) do
{:noreply, update(socket, :users, fn users -> [user | users] end)}
end
Broadcast from context
Broadcast from context
def create_user(attrs) do
with {:ok, user} <- do_create_user(attrs) do
Phoenix.PubSub.broadcast(MyApp.PubSub, "users", {:user_created, user})
{:ok, user}
end
end
undefineddef create_user(attrs) do
with {:ok, user} <- do_create_user(attrs) do
Phoenix.PubSub.broadcast(MyApp.PubSub, "users", {:user_created, user})
{:ok, user}
end
end
undefinedTesting Phoenix Applications
Phoenix应用测试
Controller Tests
控制器测试
elixir
defmodule MyAppWeb.UserControllerTest do
use MyAppWeb.ConnCase, async: true
test "GET /users", %{conn: conn} do
conn = get(conn, ~p"/users")
assert html_response(conn, 200) =~ "Listing Users"
end
endelixir
defmodule MyAppWeb.UserControllerTest do
use MyAppWeb.ConnCase, async: true
test "GET /users", %{conn: conn} do
conn = get(conn, ~p"/users")
assert html_response(conn, 200) =~ "Listing Users"
end
endLiveView Tests
LiveView测试
elixir
defmodule MyAppWeb.UserLiveTest do
use MyAppWeb.ConnCase
import Phoenix.LiveViewTest
test "displays users", %{conn: conn} do
user = insert(:user)
{:ok, view, html} = live(conn, ~p"/users")
assert html =~ user.name
assert has_element?(view, "#user-#{user.id}")
end
test "creates user", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/users/new")
assert view
|> form("#user-form", user: %{name: "Alice", email: "alice@example.com"})
|> render_submit()
assert_patch(view, ~p"/users")
end
endelixir
defmodule MyAppWeb.UserLiveTest do
use MyAppWeb.ConnCase
import Phoenix.LiveViewTest
test "displays users", %{conn: conn} do
user = insert(:user)
{:ok, view, html} = live(conn, ~p"/users")
assert html =~ user.name
assert has_element?(view, "#user-#{user.id}")
end
test "creates user", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/users/new")
assert view
|> form("#user-form", user: %{name: "Alice", email: "alice@example.com"})
|> render_submit()
assert_patch(view, ~p"/users")
end
endChannel Tests
Channel测试
elixir
defmodule MyAppWeb.RoomChannelTest do
use MyAppWeb.ChannelCase
test "broadcasts are pushed to the client", %{socket: socket} do
{:ok, _, socket} = subscribe_and_join(socket, "room:lobby", %{})
broadcast_from!(socket, "new_msg", %{body: "test"})
assert_broadcast "new_msg", %{body: "test"}
end
endelixir
defmodule MyAppWeb.RoomChannelTest do
use MyAppWeb.ChannelCase
test "broadcasts are pushed to the client", %{socket: socket} do
{:ok, _, socket} = subscribe_and_join(socket, "room:lobby", %{})
broadcast_from!(socket, "new_msg", %{body: "test"})
assert_broadcast "new_msg", %{body: "test"}
end
endCommon Patterns
常见模式
Loading Associations
关联数据加载
Preload associations efficiently:
elixir
def list_posts do
Post
|> preload([:author, comments: :author])
|> Repo.all()
end高效预加载关联数据:
elixir
def list_posts do
Post
|> preload([:author, comments: :author])
|> Repo.all()
endPagination
分页
Use Scrivener or custom pagination:
elixir
def list_users(page \\ 1) do
User
|> order_by(desc: :inserted_at)
|> Repo.paginate(page: page, page_size: 20)
end使用Scrivener或自定义分页实现:
elixir
def list_users(page \\ 1) do
User
|> order_by(desc: :inserted_at)
|> Repo.paginate(page: page, page_size: 20)
endFile Uploads
文件上传
Handle uploads in LiveView:
elixir
def mount(_params, _session, socket) do
{:ok,
socket
|> assign(:uploaded_files, [])
|> allow_upload(:avatar, accept: ~w(.jpg .jpeg .png), max_entries: 1)}
end
def handle_event("save", _params, socket) do
uploaded_files =
consume_uploaded_entries(socket, :avatar, fn %{path: path}, _entry ->
dest = Path.join("priv/static/uploads", Path.basename(path))
File.cp!(path, dest)
{:ok, "/uploads/" <> Path.basename(dest)}
end)
{:noreply, update(socket, :uploaded_files, &(&1 ++ uploaded_files))}
end在LiveView中处理文件上传:
elixir
def mount(_params, _session, socket) do
{:ok,
socket
|> assign(:uploaded_files, [])
|> allow_upload(:avatar, accept: ~w(.jpg .jpeg .png), max_entries: 1)}
end
def handle_event("save", _params, socket) do
uploaded_files =
consume_uploaded_entries(socket, :avatar, fn %{path: path}, _entry ->
dest = Path.join("priv/static/uploads", Path.basename(path))
File.cp!(path, dest)
{:ok, "/uploads/" <> Path.basename(dest)}
end)
{:noreply, update(socket, :uploaded_files, &(&1 ++ uploaded_files))}
endPerformance Optimization
性能优化
Database Query Optimization
数据库查询优化
- Use to avoid N+1 queries
preload/2 - Add database indexes for frequently queried fields
- Use to load only needed fields
select/3 - Consider using for large datasets
Repo.stream/2
- 使用避免N+1查询问题
preload/2 - 为频繁查询的字段添加数据库索引
- 使用仅加载所需字段
select/3 - 处理大型数据集时考虑使用
Repo.stream/2
LiveView Performance
LiveView性能优化
- Move expensive computations to or background jobs
handle_event - Use for computed values
assign_new/3 - Implement for async operations after mount
handle_continue/2 - Use temporary assigns for large lists:
assign(socket, :items, temporary: true)
- 将昂贵的计算操作移至事件处理器或后台任务中
- 使用处理计算值
assign_new/3 - 实现以在mount后执行异步操作
handle_continue/2 - 对大型列表使用临时赋值:
assign(socket, :items, temporary: true)
Caching
缓存
Use Cachex or ETS for caching:
elixir
def get_user!(id) do
Cachex.fetch(:users, id, fn ->
{:commit, Repo.get!(User, id)}
end)
end使用Cachex或ETS实现缓存:
elixir
def get_user!(id) do
Cachex.fetch(:users, id, fn ->
{:commit, Repo.get!(User, id)}
end)
endSecurity Best Practices
安全最佳实践
- Always validate and sanitize user input through changesets
- Use CSRF protection (enabled by default)
- Implement rate limiting for APIs
- Use plug
put_secure_browser_headers - Validate file uploads (type, size, content)
- Use prepared statements (Ecto does this automatically)
- Implement proper authentication and authorization
- 始终通过Changeset验证和清理用户输入
- 使用CSRF保护(默认已启用)
- 为API实现速率限制
- 使用插件
put_secure_browser_headers - 验证文件上传的类型、大小和内容
- 使用预编译语句(Ecto会自动处理)
- 实现完善的身份认证与授权机制
Tidewave MCP Dev Tools
Tidewave MCP开发工具
Tidewave connects AI coding assistants to running Phoenix applications via MCP, exposing runtime introspection tools (Ecto schemas, code execution, docs, logs, SQL queries).
Tidewave通过MCP将AI代码助手与运行中的Phoenix应用连接,提供运行时内省工具(Ecto Schema、代码执行、文档、日志、SQL查询等)。
When to Use Tidewave
Tidewave适用场景
- Introspecting a running Phoenix app (schemas, modules, logs)
- Executing Elixir code or SQL queries against a live dev server
- Looking up documentation for project dependencies at runtime
- Debugging LiveView components with source annotations
- 内省运行中的Phoenix应用(Schema、模块、日志)
- 在运行的开发服务器上执行Elixir代码或SQL查询
- 在运行时查找项目依赖的文档
- 通过源码注解调试LiveView组件
Quick Setup
快速搭建
- Add dependency to :
mix.exs
elixir
{:tidewave, "~> 0.5", only: :dev}- Add plug to (before
endpoint.ex):code_reloading?
elixir
if Mix.env() == :dev do
plug Tidewave
end- Connect Claude Code to the MCP server:
bash
claude mcp add --transport http tidewave http://localhost:4000/tidewave/mcp- 在中添加依赖:
mix.exs
elixir
{:tidewave, "~> 0.5", only: :dev}- 在中添加插件(在
endpoint.ex之前):code_reloading?
elixir
if Mix.env() == :dev do
plug Tidewave
end- 将Claude Code连接到MCP服务器:
bash
claude mcp add --transport http tidewave http://localhost:4000/tidewave/mcpKey MCP Tools
核心MCP工具
| Tool | Purpose |
|---|---|
| Execute Elixir code in the running app |
| Run SQL queries against the database |
| List schemas with fields and associations |
| Retrieve module/function documentation |
| Find source file paths and line numbers |
| Access server logs |
| 工具 | 用途 |
|---|---|
| 在运行中的应用中执行Elixir代码 |
| 针对数据库运行SQL查询 |
| 列出包含字段和关联的Schema |
| 获取模块/函数的文档 |
| 查找源文件路径和行号 |
| 访问服务器日志 |
Security
安全注意事项
Tidewave is dev-only. Always guard with and never deploy to production. It only accepts localhost requests by default.
Mix.env() == :devFor full setup details, configuration options, LiveView debug annotations, and troubleshooting, see .
references/tidewave.mdTidewave仅适用于开发环境。始终通过进行防护,切勿部署到生产环境。默认情况下仅接受本地请求。
Mix.env() == :dev如需完整的搭建细节、配置选项、LiveView调试注解以及故障排除指南,请参阅。
references/tidewave.mdKey Principles
核心原则
- Context boundaries: Keep business logic in contexts, not controllers/LiveViews
- LiveView first: Prefer LiveView over JavaScript for interactive features
- Changesets for validation: Always validate through Ecto changesets
- Pub/Sub for communication: Use Phoenix.PubSub for cross-process updates
- Test at boundaries: Test contexts, controllers, and LiveViews separately
- Follow conventions: Use Phoenix generators and follow established patterns
- 上下文边界:将业务逻辑置于上下文中,而非控制器/LiveView中
- 优先使用LiveView:对于交互式功能,优先使用LiveView而非JavaScript
- Changeset验证:始终通过Ecto Changeset进行验证
- Pub/Sub通信:使用Phoenix.PubSub实现跨进程更新
- 边界测试:分别测试上下文、控制器和LiveView
- 遵循约定:使用Phoenix生成器并遵循既定模式