控制器基础 #

一、控制器概述 #

1.1 什么是控制器 #

控制器是Phoenix MVC架构中的C,负责处理HTTP请求并返回响应。每个控制器是一个Elixir模块,包含多个动作函数。

text
Request → Router → Controller → View → Response

1.2 控制器结构 #

elixir
defmodule HelloWeb.UserController do
  use HelloWeb, :controller

  def index(conn, _params) do
    users = Hello.Accounts.list_users()
    render(conn, :index, users: users)
  end

  def show(conn, %{"id" => id}) do
    user = Hello.Accounts.get_user!(id)
    render(conn, :show, user: user)
  end
end

二、创建控制器 #

2.1 手动创建 #

elixir
defmodule HelloWeb.PostController do
  use HelloWeb, :controller

  def index(conn, _params) do
    posts = Hello.Blog.list_posts()
    render(conn, :index, posts: posts)
  end

  def show(conn, %{"id" => id}) do
    post = Hello.Blog.get_post!(id)
    render(conn, :show, post: post)
  end

  def new(conn, _params) do
    changeset = Hello.Blog.change_post(%Hello.Blog.Post{})
    render(conn, :new, changeset: changeset)
  end

  def create(conn, %{"post" => post_params}) do
    case Hello.Blog.create_post(post_params) do
      {:ok, post} ->
        conn
        |> put_flash(:info, "Post created successfully")
        |> redirect(to: ~p"/posts/#{post.id}")

      {:error, changeset} ->
        render(conn, :new, changeset: changeset)
    end
  end

  def edit(conn, %{"id" => id}) do
    post = Hello.Blog.get_post!(id)
    changeset = Hello.Blog.change_post(post)
    render(conn, :edit, post: post, changeset: changeset)
  end

  def update(conn, %{"id" => id, "post" => post_params}) do
    post = Hello.Blog.get_post!(id)

    case Hello.Blog.update_post(post, post_params) do
      {:ok, post} ->
        conn
        |> put_flash(:info, "Post updated successfully")
        |> redirect(to: ~p"/posts/#{post.id}")

      {:error, changeset} ->
        render(conn, :edit, post: post, changeset: changeset)
    end
  end

  def delete(conn, %{"id" => id}) do
    post = Hello.Blog.get_post!(id)
    {:ok, _post} = Hello.Blog.delete_post(post)

    conn
    |> put_flash(:info, "Post deleted successfully")
    |> redirect(to: ~p"/posts")
  end
end

2.2 使用生成器 #

bash
mix phx.gen.html Blog Post posts title:string body:text
mix phx.gen.json Blog Post posts title:string body:text
mix phx.gen.live Blog Post posts title:string body:text

三、Conn结构 #

3.1 Conn概述 #

conn是Plug.Conn结构体,包含请求的所有信息:

elixir
%Plug.Conn{
  host: "localhost",
  method: "GET",
  path_info: ["users", "1"],
  request_path: "/users/1",
  params: %{"id" => "1"},
  req_headers: [{"content-type", "text/html"}],
  resp_headers: [],
  status: 200,
  assigns: %{},
  cookies: %{},
  session: %{}
}

3.2 常用Conn操作 #

elixir
def show(conn, params) do
  conn
  |> assign(:user, user)
  |> put_session(:user_id, user.id)
  |> put_flash(:info, "Welcome!")
  |> put_resp_header("x-custom", "value")
  |> put_status(:created)
  |> render(:show, user: user)
end

四、渲染响应 #

4.1 渲染模板 #

elixir
def index(conn, _params) do
  users = Hello.Accounts.list_users()
  render(conn, :index, users: users)
end

4.2 渲染JSON #

elixir
def show(conn, %{"id" => id}) do
  user = Hello.Accounts.get_user!(id)
  json(conn, %{id: user.id, name: user.name, email: user.email})
end

4.3 渲染文本 #

elixir
def health(conn, _params) do
  text(conn, "OK")
end

4.4 发送文件 #

elixir
def download(conn, %{"id" => id}) do
  file = Hello.Files.get_file!(id)
  send_download(conn, {:file, file.path}, filename: file.name)
end

4.5 自定义响应 #

elixir
def custom(conn, _params) do
  conn
  |> put_resp_content_type("text/plain")
  |> send_resp(200, "Custom response")
end

五、重定向 #

5.1 基本重定向 #

elixir
def create(conn, params) do
  case Hello.Accounts.create_user(params) do
    {:ok, user} ->
      redirect(conn, to: ~p"/users/#{user.id}")

    {:error, changeset} ->
      render(conn, :new, changeset: changeset)
  end
end

5.2 外部重定向 #

elixir
def external(conn, _params) do
  redirect(conn, external: "https://example.com")
end

5.3 返回上一页 #

elixir
def delete(conn, %{"id" => id}) do
  {:ok, _} = Hello.Accounts.delete_user(id)

  conn
  |> put_flash(:info, "User deleted")
  |> redirect(to: conn.assigns[:return_to] || ~p"/users")
end

六、Flash消息 #

6.1 设置Flash #

elixir
def create(conn, params) do
  case Hello.Accounts.create_user(params) do
    {:ok, user} ->
      conn
      |> put_flash(:info, "User created successfully")
      |> redirect(to: ~p"/users/#{user.id}")

    {:error, changeset} ->
      conn
      |> put_flash(:error, "Failed to create user")
      |> render(:new, changeset: changeset)
  end
end

6.2 Flash类型 #

elixir
put_flash(conn, :info, "Information message")
put_flash(conn, :error, "Error message")
put_flash(conn, :warning, "Warning message")
put_flash(conn, :success, "Success message")

七、状态码 #

7.1 设置状态码 #

elixir
def create(conn, params) do
  case Hello.Accounts.create_user(params) do
    {:ok, user} ->
      conn
      |> put_status(:created)
      |> render(:show, user: user)

    {:error, changeset} ->
      conn
      |> put_status(:unprocessable_entity)
      |> render(:new, changeset: changeset)
  end
end

7.2 常用状态码 #

原子 代码 说明
:ok 200 成功
:created 201 已创建
:no_content 204 无内容
:bad_request 400 错误请求
:unauthorized 401 未授权
:forbidden 403 禁止访问
:not_found 404 未找到
:unprocessable_entity 422 无法处理
:internal_server_error 500 服务器错误

八、响应头 #

8.1 设置响应头 #

elixir
def show(conn, %{"id" => id}) do
  user = Hello.Accounts.get_user!(id)

  conn
  |> put_resp_header("x-request-id", generate_request_id())
  |> put_resp_header("cache-control", "max-age=3600")
  |> render(:show, user: user)
end

8.2 内容类型 #

elixir
def export(conn, %{"format" => "csv"}) do
  csv_data = generate_csv()

  conn
  |> put_resp_content_type("text/csv")
  |> send_resp(200, csv_data)
end

九、Assigns #

9.1 设置Assigns #

elixir
def index(conn, _params) do
  conn
  |> assign(:users, Hello.Accounts.list_users())
  |> assign(:page_title, "Users List")
  |> render(:index)
end

9.2 合并Assigns #

elixir
def show(conn, %{"id" => id}) do
  user = Hello.Accounts.get_user!(id)
  posts = Hello.Blog.list_user_posts(user.id)

  conn
  |> assign(:user, user)
  |> assign(:posts, posts)
  |> render(:show)
end

9.3 在模板中使用 #

heex
defmodule HelloWeb.Layouts do
  use HelloWeb, :html

  embed_templates "layouts/*"
end

十、错误处理 #

10.1 404处理 #

elixir
def show(conn, %{"id" => id}) do
  case Hello.Accounts.get_user(id) do
    nil ->
      conn
      |> put_status(:not_found)
      |> put_view(HelloWeb.ErrorHTML)
      |> render("404.html")

    user ->
      render(conn, :show, user: user)
  end
end

10.2 使用get_user! #

elixir
def show(conn, %{"id" => id}) do
  user = Hello.Accounts.get_user!(id)
  render(conn, :show, user: user)
end

get_user!会在找不到时抛出Ecto.NotFoundError,Phoenix会自动返回404。

10.3 自定义错误 #

elixir
def create(conn, params) do
  case Hello.Accounts.create_user(params) do
    {:ok, user} ->
      redirect(conn, to: ~p"/users/#{user.id}")

    {:error, %Ecto.Changeset{} = changeset} ->
      conn
      |> put_status(:unprocessable_entity)
      |> render(:new, changeset: changeset)

    {:error, :email_taken} ->
      conn
      |> put_status(:conflict)
      |> json(%{error: "Email already taken"})
  end
end

十一、API控制器 #

11.1 JSON响应 #

elixir
defmodule HelloWeb.Api.UserController do
  use HelloWeb, :controller

  def index(conn, _params) do
    users = Hello.Accounts.list_users()
    json(conn, %{data: users})
  end

  def show(conn, %{"id" => id}) do
    user = Hello.Accounts.get_user!(id)
    json(conn, %{data: user})
  end

  def create(conn, params) do
    case Hello.Accounts.create_user(params) do
      {:ok, user} ->
        conn
        |> put_status(:created)
        |> json(%{data: user})

      {:error, changeset} ->
        conn
        |> put_status(:unprocessable_entity)
        |> json(%{errors: format_errors(changeset)})
    end
  end

  defp format_errors(changeset) do
    Ecto.Changeset.traverse_errors(changeset, fn {msg, opts} ->
      Regex.replace(~r"%{(\w+)}", msg, fn _, key ->
        opts |> Keyword.get(String.to_existing_atom(key), key) |> to_string()
      end)
    end)
  end
end

11.2 分页响应 #

elixir
def index(conn, params) do
  page = Map.get(params, "page", "1") |> String.to_integer()
  per_page = Map.get(params, "per_page", "10") |> String.to_integer()

  %{entries: users, total_pages: total_pages, page_number: page_number} =
    Hello.Accounts.list_users_paginated(page: page, per_page: per_page)

  json(conn, %{
    data: users,
    meta: %{
      current_page: page_number,
      total_pages: total_pages
    }
  })
end

十二、总结 #

12.1 核心操作 #

操作 函数
渲染模板 render/3
返回JSON json/2
返回文本 text/2
重定向 redirect/2
Flash消息 put_flash/3
状态码 put_status/2
响应头 put_resp_header/3
Assigns assign/3

12.2 下一步 #

现在你已经了解了控制器基础,接下来让我们学习 请求与响应,深入了解请求处理和响应生成!

最后更新:2026-03-28