查看源代码 控制器

要求:本指南假定您已阅读过入门指南并已成功创建 Phoenix 应用并运行

要求:本指南假定您已阅读过请求生命周期指南

Phoenix 控制器充当中间模块。它们的功能(称为动作)由路由器在响应 HTTP 请求时调用。然后,动作会收集所有必要的数据并执行所有必要的步骤,然后再调用视图层来渲染模板或返回 JSON 响应。

Phoenix 控制器也基于 Plug 包,并且本身也是插件。控制器提供执行动作中几乎所有所需操作的功能。如果我们发现 Phoenix 控制器无法满足需求,我们可以在 Plug 本身中找到所需的功能。有关更多信息,请参阅Plug 指南Plug 文档

新生成的 Phoenix 应用将只有一个名为 PageController 的控制器,位于 lib/hello_web/controllers/page_controller.ex,如下所示

defmodule HelloWeb.PageController do
  use HelloWeb, :controller

  def home(conn, _params) do
    render(conn, :home, layout: false)
  end
end

模块定义下方的第一行调用 HelloWeb 模块的 __using__/1 宏,它会导入一些有用的模块。

PageController 为我们提供了 home 动作,用于显示 Phoenix 欢迎页面,该页面与 Phoenix 在路由器中定义的默认路由相关联。

动作

控制器动作仅仅是函数。我们可以随意命名它们,只要它们遵循 Elixir 的命名规则即可。我们必须满足的唯一要求是动作名称与路由器中定义的路由匹配。

例如,在 lib/hello_web/router.ex 中,我们可以更改 Phoenix 在新应用中为我们提供的默认路由中的动作名称,从 home

get "/", PageController, :home

更改为 index

get "/", PageController, :index

只要我们在 PageController 中将动作名称也更改为 index欢迎页面 将按原样加载。

defmodule HelloWeb.PageController do
  ...

  def index(conn, _params) do
    render(conn, :index)
  end
end

虽然我们可以随意命名动作,但对于动作名称有一些约定,我们应该尽可能地遵循这些约定。我们在路由指南中已经介绍过这些约定,但这里将再次简要介绍一下。

  • index - 渲染给定资源类型的所有项目的列表
  • show - 通过 ID 渲染单个项目
  • new - 渲染用于创建新项目的表单
  • create - 接收一个新项目的参数并将其保存到数据存储中
  • edit - 通过 ID 检索单个项目并在表单中显示,用于编辑
  • update - 接收一个已编辑项目的参数并将项目保存到数据存储中
  • delete - 接收要删除的项目的 ID 并将其从数据存储中删除

这些动作中的每一个都接受两个参数,它们将由 Phoenix 在幕后提供。

第一个参数始终是 conn,一个包含有关请求信息(如主机、路径元素、端口、查询字符串等)的结构。conn 通过 Elixir 的 Plug 中间件框架传递到 Phoenix。有关 conn 的更多详细信息,请参阅Plug.Conn 文档

第二个参数是 params。毫不奇怪,这是一个包含 HTTP 请求中传递的所有参数的映射。最好在函数签名中对参数进行模式匹配,以便以简单的数据包的形式提供数据,我们可以将这些数据传递给渲染。我们在请求生命周期指南中看到了这一点,当时我们在 lib/hello_web/controllers/hello_controller.ex 中的 show 路由中添加了 messenger 参数。

defmodule HelloWeb.HelloController do
  ...

  def show(conn, %{"messenger" => messenger}) do
    render(conn, :show, messenger: messenger)
  end
end

在某些情况下(例如,通常在 index 动作中),我们不关心参数,因为我们的行为不依赖于它们。在这些情况下,我们不使用传入的参数,而只是在变量名前加下划线,将其命名为 _params。这将使编译器不会抱怨未使用变量,同时保持正确的参数数量。

渲染

控制器可以通过多种方式渲染内容。最简单的方法是使用 Phoenix 提供的text/2 函数来渲染一些纯文本。

例如,让我们将 HelloController 中的 show 动作重写为返回文本。为此,我们可以执行以下操作。

def show(conn, %{"messenger" => messenger}) do
  text(conn, "From messenger #{messenger}")
end

现在,在浏览器中访问 /hello/Frank 应该会以纯文本形式显示 From messenger Frank,没有任何 HTML。

比这更进一步的是使用 json/2 函数渲染纯 JSON。我们需要向其传递一些 Jason 库 可以解码为 JSON 的内容,例如映射。(Jason 是 Phoenix 的依赖项之一。)

def show(conn, %{"messenger" => messenger}) do
  json(conn, %{id: messenger})
end

如果我们再次在浏览器中访问 /hello/Frank,我们应该会看到一个 JSON 块,其中键 id 映射到字符串 "Frank"

{"id": "Frank"}

json/2 函数对于编写 API 很有用,还有一个用于渲染 HTML 的 html/2 函数,但大多数情况下我们会使用 Phoenix 视图来构建响应。为此,Phoenix 包含了 render/3 函数。它对 HTML 响应尤其重要,因为 Phoenix 视图提供了性能和安全性优势。

让我们将 show 动作回滚到我们在请求生命周期指南中最初编写的内容

defmodule HelloWeb.HelloController do
  use HelloWeb, :controller

  def show(conn, %{"messenger" => messenger}) do
    render(conn, :show, messenger: messenger)
  end
end

为了使 render/3 函数正常工作,控制器和视图必须共享相同的根名称(在本例中为 Hello),并且 HelloHTML 模块必须包含一个指定其模板位置的 embed_templates 定义。默认情况下,控制器、视图模块和模板会共存于同一个控制器目录中。换句话说,HelloController 需要 HelloHTML,而 HelloHTML 需要 lib/hello_web/controllers/hello_html/ 目录存在,该目录必须包含 show.html.heex 模板。

render/3 还会将 show 动作从参数中接收的 messenger 的值作为分配传递。

如果我们需要在使用 render 时将值传递到模板中,这很简单。我们可以像我们在 messenger: messenger 中看到的那样传递一个关键字,或者我们可以使用Plug.Conn.assign/3,它会方便地返回 conn

  def show(conn, %{"messenger" => messenger}) do
    conn
    |> Plug.Conn.assign(:messenger, messenger)
    |> render(:show)
  end

注意:使用 Phoenix.Controller 会导入 Plug.Conn,因此将调用缩短为 assign/3 就可以了。

将多个值传递到模板中就像将 assign/3 函数连接在一起一样简单

  def show(conn, %{"messenger" => messenger}) do
    conn
    |> assign(:messenger, messenger)
    |> assign(:receiver, "Dweezil")
    |> render(:show)
  end

或者,您可以直接将分配传递给 render

  def show(conn, %{"messenger" => messenger}) do
    render(conn, :show, messenger: messenger, receiver: "Dweezil")
  end

一般来说,一旦所有分配都配置好了,我们就会调用视图层。视图层 (HelloWeb.HelloHTML) 然后会渲染 show.html 以及布局,并将响应发送回浏览器。

组件和 HEEx 模板 有自己的指南,所以我们不会在这里花太多时间介绍它们。我们将重点介绍如何在控制器动作中渲染不同的格式。

新的渲染格式

通过模板渲染 HTML 很好,但是如果我们需要动态地更改渲染格式怎么办?假设我们有时需要 HTML,有时需要纯文本,有时需要 JSON。然后怎么办?

视图的工作不仅仅是渲染 HTML 模板。视图是关于数据展示的。给定一组数据,视图的目的是根据某些格式以有意义的方式展示这些数据,无论是 HTML、JSON、CSV 还是其他格式。如今,许多 Web 应用向远程客户端返回 JSON,而 Phoenix 视图非常适合 JSON 渲染。

例如,让我们以从新生成的应用中获取的 PageControllerhome 动作为例。默认情况下,它具有正确的视图 PageHTML、来自 (lib/hello_web/controllers/page_html) 的嵌入式模板以及用于渲染 HTML 的正确模板 (home.html.heex)。

def home(conn, _params) do
  render(conn, :home, layout: false)
end

它缺少的是用于渲染 JSON 的视图。Phoenix 控制器会将模板渲染委托给视图模块,并且它会根据格式进行操作。我们已经有了用于 HTML 格式的视图,但我们需要指示 Phoenix 如何渲染 JSON 格式。默认情况下,您可以在 lib/hello_web.ex 中查看您的控制器支持哪些格式

  def controller do
    quote do
      use Phoenix.Controller,
        formats: [:html, :json],
        layouts: [html: HelloWeb.Layouts]
      ...
    end
  end

因此,默认情况下,Phoenix 会根据请求格式和控制器名称查找 HTMLJSON 视图模块。我们也可以在控制器中显式告诉 Phoenix 对每种格式使用哪个视图。例如,Phoenix 默认执行的操作可以通过以下代码在控制器中显式设置

plug :put_view, html: HelloWeb.PageHTML, json: HelloWeb.PageJSON

让我们在 lib/hello_web/controllers/page_json.ex 中添加一个 PageJSON 视图模块

defmodule HelloWeb.PageJSON do
  def home(_assigns) do
    %{message: "this is some JSON"}
  end
end

由于 Phoenix 视图层只是一个控制器渲染的函数,它会传递连接分配,因此我们可以定义一个常规的 home/1 函数并返回一个要作为 JSON 序列化的映射。

要使它正常工作,我们还需要做几件事。因为我们希望从同一个控制器渲染 HTML 和 JSON,所以我们需要告诉路由器它应该接受 json 格式。我们通过在 :browser 管道中将 json 添加到接受的格式列表中来实现这一点。让我们打开 lib/hello_web/router.ex 并将 plug :accepts 更改为像这样包含 jsonhtml

defmodule HelloWeb.Router do
  use HelloWeb, :router

  pipeline :browser do
    plug :accepts, ["html", "json"]
    plug :fetch_session
    plug :fetch_live_flash
    plug :put_root_layout, html: {HelloWeb.LayoutView, :root}
    plug :protect_from_forgery
    plug :put_secure_browser_headers
  end
...

Phoenix 允许我们使用 _format 查询字符串参数动态更改格式。如果我们访问 http://localhost:4000/?_format=json,我们将看到 %{"message": "this is some JSON"}

然而,在实践中,需要渲染两种格式的应用程序通常会为每种格式使用两个不同的管道,例如路由器文件中已经定义的 pipeline :api。要了解更多信息,请参阅 我们的 JSON 和 API 指南

直接发送响应

如果以上渲染选项都不符合我们的需求,我们可以使用 Plug 提供的一些函数来组合我们自己的响应。假设我们要发送一个状态为“201”且没有任何正文的响应。我们可以使用 Plug.Conn.send_resp/3 函数来实现。

编辑 lib/hello_web/controllers/page_controller.ex 中的 PageControllerhome 操作,使其看起来像这样

def home(conn, _params) do
  send_resp(conn, 201, "")
end

重新加载 http://localhost:4000 应该会显示一个完全空白的页面。浏览器开发者工具的网络选项卡应该显示一个状态为“201”(已创建)的响应。一些浏览器(Safari)会下载响应,因为内容类型没有设置。

为了明确指定内容类型,我们可以使用 put_resp_content_type/2send_resp/3 结合使用。

def home(conn, _params) do
  conn
  |> put_resp_content_type("text/plain")
  |> send_resp(201, "")
end

通过这种方式使用 Plug 函数,我们可以创建我们需要的响应。

设置内容类型

类似于 _format 查询字符串参数,我们可以通过修改 HTTP 内容类型标头并提供相应的模板来渲染任何类型的格式。

如果我们想要渲染 home 操作的 XML 版本,我们可以在 lib/hello_web/page_controller.ex 中像这样实现该操作。

def home(conn, _params) do
  conn
  |> put_resp_content_type("text/xml")
  |> render(:home, content: some_xml_content)
end

然后,我们需要提供一个 home.xml.eex 模板,该模板创建有效的 XML,我们就完成了。

有关有效内容 MIME 类型的列表,请参阅 MIME 库。

设置 HTTP 状态

我们还可以像设置内容类型一样设置响应的 HTTP 状态码。已导入到所有控制器中的 Plug.Conn 模块有一个 put_status/2 函数来执行此操作。

Plug.Conn.put_status/2conn 作为第一个参数,以整数或作为原子使用的“友好名称”作为第二个参数,表示我们要设置的状态码。状态码原子表示的列表可以在 Plug.Conn.Status.code/1 文档中找到。

让我们更改 PageControllerhome 操作中的状态。

def home(conn, _params) do
  conn
  |> put_status(202)
  |> render(:home, layout: false)
end

我们提供的状态码必须是一个有效的数字。

重定向

通常,我们需要在请求中间重定向到一个新的 URL。例如,一个成功的 create 操作通常会重定向到我们刚刚创建的资源的 show 操作。或者,它可以重定向到 index 操作以显示所有相同类型的项目。还有很多其他情况下重定向也很有用。

无论是什么情况,Phoenix 控制器都提供了方便的 redirect/2 函数来简化重定向操作。Phoenix 区分了重定向到应用程序内的路径和重定向到 URL(无论是应用程序内部还是外部)。

为了尝试 redirect/2,让我们在 lib/hello_web/router.ex 中创建一个新路由。

defmodule HelloWeb.Router do
  ...

  scope "/", HelloWeb do
    ...
    get "/", PageController, :home
    get "/redirect_test", PageController, :redirect_test
    ...
  end
end

然后,我们将更改控制器的 PageControllerhome 操作,使其只重定向到我们的新路由。

defmodule HelloWeb.PageController do
  use HelloWeb, :controller

  def home(conn, _params) do
    redirect(conn, to: ~p"/redirect_test")
  end
end

我们使用了 Phoenix.VerifiedRoutes.sigil_p/2 来构建我们的重定向路径,这是引用应用程序内任何路径的首选方法。我们了解了在 路由指南 中的验证路由。

最后,让我们在同一个文件中定义我们重定向到的操作,该操作只是渲染主页,但现在是在一个新的地址下

def redirect_test(conn, _params) do
  render(conn, :home, layout: false)
end

当我们重新加载我们的 欢迎页面 时,我们看到我们已被重定向到 /redirect_test,它显示了原始的欢迎页面。它可以正常工作!

如果需要,我们可以打开开发者工具,单击网络选项卡,然后再次访问我们的根路由。我们看到该页面的两个主要请求——一个状态为 302/ 请求,以及一个状态为 200/redirect_test 请求。

请注意,重定向函数接受 conn 以及一个字符串,该字符串表示应用程序内的相对路径。出于安全原因,:to 选项只能重定向到应用程序内的路径。如果您想重定向到完全限定的路径或外部 URL,则应使用 :external

def home(conn, _params) do
  redirect(conn, external: "https://elixir.erlang.ac.cn/")
end

闪存消息

有时我们需要在操作过程中与用户进行通信。可能更新模式时出现了错误,或者我们只是想欢迎他们回到应用程序。为此,我们有闪存消息。

Phoenix.Controller 模块提供了 put_flash/3 来设置闪存消息作为键值对,并将它们放入连接中的 @flash 分配中。让我们在我们的 HelloWeb.PageController 中设置两个闪存消息来尝试一下。

为此,我们按如下方式修改 home 操作

defmodule HelloWeb.PageController do
  ...
  def home(conn, _params) do
    conn
    |> put_flash(:error, "Let's pretend we have an error.")
    |> render(:home, layout: false)
  end
end

为了查看我们的闪存消息,我们需要能够检索它们并在模板布局中显示它们。我们可以使用 Phoenix.Flash.get/2 来实现,它接受闪存数据和我们关心的键。然后,它返回该键的值。

为了方便起见,一个 flash_group 组件已准备就绪,并已添加到我们的 欢迎页面 的开头。

<.flash_group flash={@flash} />

当我们重新加载 欢迎页面 时,我们的消息应该出现在页面右上角。

当与重定向结合使用时,闪存功能非常方便。也许您想重定向到带有额外信息的页面。如果我们重复使用上一节中的重定向操作,我们可以执行

  def home(conn, _params) do
    conn
    |> put_flash(:error, "Let's pretend we have an error.")
    |> redirect(to: ~p"/redirect_test")
  end

现在,如果您重新加载 欢迎页面,您将被重定向,并且闪存消息将再次显示。

除了 put_flash/3 之外,Phoenix.Controller 模块还有另一个值得了解的有用函数。 clear_flash/1 只接受 conn,并删除可能存储在会话中的任何闪存消息。

Phoenix 不会强制执行哪些键存储在闪存中。只要我们在内部保持一致,一切都会好起来的。但是,:info:error 是常见的,并且在我们的模板中默认处理。

错误页面

Phoenix 有两个名为 ErrorHTMLErrorJSON 的视图,它们位于 lib/hello_web/controllers/ 中。这些视图的目的是以通用的方式处理传入 HTML 或 JSON 请求的错误。与我们在本指南中构建的视图类似,错误视图可以返回 HTML 和 JSON 响应。有关更多信息,请参阅 自定义错误页面操作指南