名称: phoenix-controllers 用户可调用: false 描述: 使用 Phoenix 控制器处理 HTTP 请求,包括动作、参数、渲染、flash 消息和重定向 允许工具: [Bash, Read]
Phoenix 控制器
Phoenix 控制器是 Phoenix 应用程序中路由器和视图之间的中间模块。它们处理 HTTP 请求、处理参数、与上下文交互,并决定向客户端发送什么响应。控制器是无状态的,并接收一个表示当前 HTTP 请求的连接结构(conn)。
基本控制器结构
最简单的 Phoenix 控制器使用 HelloWeb, :controller 宏,并定义接收连接和参数的函数作为动作:
defmodule HelloWeb.PageController do
use HelloWeb, :controller
def home(conn, _params) do
render(conn, :home, layout: false)
end
end
每个控制器动作接收:
conn- 表示 HTTP 请求/响应的Plug.Conn结构params- 一个包含来自 URL、查询字符串和请求体的请求参数的映射
控制器动作和参数处理
提取特定参数
使用模式匹配从 params 映射中提取特定参数:
defmodule HelloWeb.HelloController do
use HelloWeb, :controller
def show(conn, %{"messenger" => messenger}) do
render(conn, :show, messenger: messenger)
end
end
访问完整参数映射
要访问特定参数和完整 params 映射,使用 = 运算符进行模式匹配:
def show(conn, %{"messenger" => messenger} = params) do
# 同时访问 messenger 和完整 params 映射
render(conn, :show, messenger: messenger)
end
忽略参数
当动作不需要参数时,在变量前加下划线以避免编译器警告:
def index(conn, _params) do
render(conn, :home)
end
重命名动作
可以独立于模板名称自定义动作名称:
defmodule HelloWeb.PageController do
use HelloWeb, :controller
def index(conn, _params) do
# 渲染 :home 模板,但动作命名为 :index
render(conn, :home)
end
end
相应更新路由器:
get "/", PageController, :index
渲染响应
渲染 HTML 模板
使用 render/3 函数通过 Phoenix 视图渲染 HTML 模板:
defmodule HelloWeb.HelloController do
use HelloWeb, :controller
def show(conn, %{"messenger" => messenger}) do
render(conn, :show, messenger: messenger)
end
end
控制器和视图必须共享根名称(例如,HelloController 和 HelloHTML)。
渲染无布局
为特定动作禁用布局:
def home(conn, _params) do
render(conn, :home, layout: false)
end
分配值到模板
使用 assign/3
使用 assign/3 将数据传递给模板:
def show(conn, %{"messenger" => messenger}) do
conn
|> assign(:messenger, messenger)
|> render(:show)
end
链式多个分配
链式多个分配以使代码更清晰:
def show(conn, %{"messenger" => messenger}) do
conn
|> assign(:messenger, messenger)
|> assign(:receiver, "Dweezil")
|> render(:show)
end
直接将分配传递给 render/3
为简洁语法,直接将分配传递给 render/3:
def show(conn, %{"messenger" => messenger}) do
render(conn, :show, messenger: messenger, receiver: "Dweezil")
end
配置控制器格式
在控制器配置中配置支持的响应格式:
def controller do
quote do
use Phoenix.Controller,
formats: [:html, :json]
...
end
end
这允许控制器根据请求以不同格式响应。
Flash 消息
设置 Flash 消息
使用 put_flash/3 设置临时消息:
defmodule HelloWeb.PageController do
use HelloWeb, :controller
def home(conn, _params) do
conn
|> put_flash(:error, "让我们假装有一个错误。")
|> render(:home, layout: false)
end
end
Flash 消息类型:
:info- 信息性消息:error- 错误消息:warning- 警告消息(自定义):success- 成功消息(自定义)
清除 Flash 消息
从连接中移除所有 flash 消息:
clear_flash(conn)
带重定向的 Flash
将 flash 消息与重定向结合,以在导航后提供上下文:
def home(conn, _params) do
conn
|> put_flash(:error, "让我们假装有一个错误。")
|> redirect(to: ~p"/redirect_test")
end
重定向
基本重定向
使用已验证的路由语法重定向到另一条路由:
def create(conn, params) do
# ... 创建逻辑
redirect(conn, to: ~p"/posts")
end
带参数的重定向
在重定向中包含动态参数:
def create(conn, params) do
# ... 创建帖子
redirect(conn, to: ~p"/posts/#{post}")
end
外部重定向
重定向到外部 URL:
def external(conn, _params) do
redirect(conn, external: "https://example.com")
end
动作回退
动作回退控制器通过集中错误处理逻辑优雅地处理错误情况:
defmodule HelloWeb.MyController do
use Phoenix.Controller
action_fallback HelloWeb.MyFallbackController
def show(conn, %{"id" => id}, current_user) do
with {:ok, post} <- fetch_post(id),
:ok <- authorize_user(current_user, :view, post) do
render(conn, :show, post: post)
end
end
end
回退控制器处理非 ok 元组:
defmodule HelloWeb.MyFallbackController do
use Phoenix.Controller
def call(conn, {:error, :not_found}) do
conn
|> put_status(:not_found)
|> put_view(HelloWeb.ErrorHTML)
|> render(:"404")
end
def call(conn, {:error, :unauthorized}) do
conn
|> put_status(403)
|> put_view(HelloWeb.ErrorHTML)
|> render(:"403")
end
end
测试控制器
HTML 控制器测试
使用 ConnCase 测试控制器动作:
defmodule HelloWeb.PostControllerTest do
use HelloWeb.ConnCase
import Hello.BlogFixtures
@create_attrs %{body: "一些内容", title: "一些标题"}
@update_attrs %{body: "一些更新内容", title: "一些更新标题"}
@invalid_attrs %{body: nil, title: nil}
describe "index" do
test "列出所有帖子", %{conn: conn} do
conn = get(conn, ~p"/posts")
assert html_response(conn, 200) =~ "帖子列表"
end
end
describe "show" do
test "显示单个帖子", %{conn: conn} do
post = post_fixture()
conn = get(conn, ~p"/posts/#{post}")
assert html_response(conn, 200) =~ post.title
end
end
end
测试重定向
在测试中断言重定向:
test "创建后重定向", %{conn: conn} do
conn = post(conn, ~p"/posts", post: @create_attrs)
assert %{id: id} = redirected_params(conn)
assert redirected_to(conn) == ~p"/posts/#{id}"
end
测试 Flash 消息
验证 flash 消息是否正确设置:
test "错误时设置 flash 消息", %{conn: conn} do
conn = post(conn, ~p"/posts", post: @invalid_attrs)
assert get_flash(conn, :error) == "无法创建帖子"
end
何时使用此技能
在需要以下情况时使用此技能:
- 创建新的控制器动作以处理 HTTP 请求
- 处理和验证传入的请求参数
- 使用动态数据渲染 HTML 模板
- 通过 flash 消息实现用户反馈
- 处理表单提交或操作后的重定向
- 测试控制器行为和响应
- 使用动作回退实现错误处理
- 构建 RESTful API 端点
- 将控制器动作与 Phoenix 上下文集成
- 管理会话数据和用户身份验证流程
- 处理文件上传和多部分表单
- 为列表视图实现分页
- 创建自定义响应格式(JSON、XML 等)
- 调试请求/响应周期
最佳实践
- 保持控制器精简 - 将业务逻辑移动到上下文,保持控制器专注于 HTTP 相关事务
- 使用模式匹配 - 仅从 params 映射中提取所需的参数
- 早期验证 - 在传递给上下文之前在控制器级别验证参数
- 使用动作回退 - 使用动作回退控制器集中错误处理
- 利用管道 - 使用 plugs 进行常见操作,如身份验证和授权
- 明确分配 - 仅传递必要数据到模板,以避免暴露敏感信息
- 使用已验证的路由 - 始终使用
~p符号表示路由路径,以便在编译时捕获错误 - 全面测试 - 为所有控制器动作和边缘情况编写全面的测试
- 优雅处理错误 - 提供有意义的错误消息和适当的 HTTP 状态码
- 明智使用 flash 消息 - 在操作后向用户提供清晰、可操作的反馈
- 避免业务逻辑 - 控制器应协调,而不是实现业务规则
- 早期返回 - 使用保护子句和早期返回以使代码更清晰
- 一致的命名 - 遵循 Phoenix 的动作命名约定(index、show、new、create、edit、update、delete)
- 记录复杂动作 - 为非显而易见的控制器逻辑添加注释
- 使用结构体而非映射 - 尽可能使用来自上下文的结构体,而不是原始参数映射
常见陷阱
- 将业务逻辑放入控制器 - 这使代码更难测试和重用
- 未验证参数 - 始终验证和清理用户输入
- 过度使用分配 - 传递整个上下文模块或大型数据结构到视图
- 忽略错误情况 - 未正确处理来自上下文函数的错误
- 硬编码路径 - 使用字符串路径而不是已验证的路由(
~p) - 未使用动作回退 - 在多个动作中重复错误处理逻辑
- 测试实现细节 - 测试行为,而不是内部实现
- 暴露内部错误 - 向用户泄露堆栈跟踪或内部错误
- 未设置状态码 - 忘记为错误设置适当的 HTTP 状态码
- 突变参数 - 尝试修改 params 映射而不是使用分配
- 跳过 CSRF 保护 - 在不理解影响的情况下禁用安全功能
- 大型控制器文件 - 创建整体控制器而不是拆分关注点
- 未使用 with 语句 - 错过使用 with 语句干净地链式操作的机会
- 忘记内容协商 - 未适当处理不同的响应格式
- 混淆关注点 - 在同一控制器中处理 API 和 HTML 响应而没有适当分离