Controllers
Phoenix 中 controllers 是一种中间模块。 它们的函数(称为 action )会被路由调用以响应 HTTP 请求。 反过来,这些 actions 会先收集所有必要的数据并执行所有必要的步骤,然后调用视图层,以渲染模板或返回 JSON 响应。
Phoenix 中的 controllers 也建立在 Plug 包之上,并且它们本身就是 plugs。 控制器提供的功能几乎可以完成我们在 action 中需要做的任何事情。 如果我们确实发现自己正在寻找 Phoenix 控制器不提供的东西,我们可能会在 Plug 本身中找到我们正在寻找的东西。 请参阅插件指南或插件文档了解更多信息。
新生成的 Phoenix 应用程序将有一个名为 PageController 的控制器,可以在 lib/hello_web/controllers/page_controller.ex 中找到它,如下所示:
defmodule HelloWeb.PageController do
use HelloWeb, :controller
def index(conn, _params) do
render(conn, "index.html")
end
end
模块定义下面的第一行调用 HelloWeb 模块的 __using__/1 宏,该宏导入了一些有用的模块。
PageController 为我们提供了 index 操作来显示与 Phoenix 在路由器中定义的默认路由相关联的 Phoenix 欢迎页面。
Actions
控制器 action 是函数。 只要它们遵循 Elixir 的命名规则,我们可以为它们命名任何我们喜欢的名字。 我们必须满足的唯一要求是 action 的名字与路由器中定义的路由匹配。
例如,在 lib/hello_web/router.ex 中,我们可以更改默认的 :index action :
get "/", PageController, :index
变为 home :
get "/", PageController, :home
只要我们将对应的 PageController 中的 action 也改为 home 就行了。
defmodule HelloWeb.PageController do
...
def home(conn, _params) do
render(conn, "index.html")
end
end
虽然我们可以随意命名我们的 action ,但我们应该尽可能遵循 action 名称的约定。 我们在路由指南中讨论了这些,但我们还是可以在这里过一下。
- index - 返回资源的列表
- show - 通过 ID 返回单个资源
- new - 渲染创建表单
- create - 保存一个新的资源到数据库
- edit - 通过 ID 返回某个资源到编辑表单
- update - 更新资源
- delete 通过 id 来删除资源
每个 action 都会接收两个参数, Phoenix 框架背后提供的。
第一个参数始终是 conn,它是一个包含请求信息的结构体,例如 host、path、端口、query等等。 conn 通过 Elixir 的 Plug 中间件框架来到 Phoenix 中。 有关 conn 的更多详细信息可以在 Plug.Conn 文档中找到。
第二个参数是 params。 毫不奇怪,这是一个 map,包含 HTTP 请求中传递的所有参数。 通常会对该参数进行模式匹配,以简化后续参数的传递。 当我们向 lib/hello_web/controllers/hello_controller.ex 中的 show 路由添加 messenger 参数时,我们在请求生命周期指南中看到了这一点。
defmodule HelloWeb.HelloController do
...
def show(conn, %{"messenger" => messenger}) do
render(conn, "show.html", messenger: messenger)
end
end
在某些情况下——例如通常在 index action 中——我们不关心参数,因为我们的行为不依赖于它们。 在这些情况下,我们不使用传入参数,而只是在变量名前加上下划线,称之为 _params。 这将使编译器不会警告未使用的变量,同时仍保持正确的数量。
Rendering
控制器可以通过多种方式渲染内容。 最简单的是使用 Phoenix 提供的 text/2 函数渲染一些纯文本。
例如,让我们重写 HelloController 中的 show action 以返回文本。 为此,我们可以执行以下操作。
def show(conn, %{"messenger" => messenger}) do
text(conn, "From messenger #{messenger}")
end
现在 /hello/Frank 在您的浏览器中应该显示 From messenger Frank 的纯文本。
除此之外,还有一种是使用 json/2 函数渲染纯 JSON。 我们需要传递 Jason 库可以解码成 JSON 的数据,例如 一个 map 数据。 (Jason 是 Phoenix 的依赖之一。)
def show(conn, %{"messenger" => messenger}) do
json(conn, %{id: messenger})
end
如果我们再次在浏览器中访问 /hello/Frank,我们应该会看到一个 JSON ,其键 id 映射到字符串“Frank”。
{"id": "Frank"}
json/2 函数对编写 API 很有用,还有 html/2 函数用于渲染 HTML,但大多数时候我们使用 Phoenix 的 views 来构建我们的响应。 为此,Phoenix 有了 render/3 函数。 这对于 HTML 响应特别重要,因为 Phoenix Views 提供了性能和安全优势。
让我们将我们的 show action 回滚到我们最初在请求生命周期指南中编写的内容:
defmodule HelloWeb.HelloController do
use HelloWeb, :controller
def show(conn, %{"messenger" => messenger}) do
render(conn, "show.html", messenger: messenger)
end
end
为了让 render/3 函数正常工作,控制器和视图必须共享相同的根名称(在本例中为 Hello),并且它还必须与模板目录(在本例中为 hello)具有相同的根名称,就是 show.html.heex 模板存在的目录。 也就是说HelloController 需要 HelloView,而 HelloView 需要 lib/hello_web/templates/hello 目录存在,里面必须包含 show.html.heex 模板。(加一句,反正就是 controller 和 view 和 template 的名称要能对应上)
render/3 还将传递 show action 中获取的参数,通过将值赋给 assign 中的 messenger 来传递。
如果我们需要在使用 render 时将值传递到模板中,这很容易。 我们可以像 messenger: messenger 这样来传递 keyword 数据,或者我们可以使用 Plug.Conn.assign/3,它可以方便地返回 conn。
def show(conn, %{"messenger" => messenger}) do
conn
|> Plug.Conn.assign(:messenger, messenger)
|> render("show.html")
end
注意: 使用 Phoenix.Controller 来引入 Plug.Conn 来缩短 assign/3 方法的调用。
要传递多个值也是很简单的,多个 assign/3 方法连起来:
def show(conn, %{"messenger" => messenger}) do
conn
|> assign(:messenger, messenger)
|> assign(:receiver, "Dweezil")
|> render("show.html")
end
一般来说,一旦配置了所有 assigns ,我们就会调用视图层。 然后,视图层在布局旁边 渲染 show.html,并将响应发送回浏览器。
视图和模板有它们自己的指南,所以我们不会在这里花费太多时间。 我们将看到的是如何从控制器动作内部分配不同的布局,或者根本不分配。
设置布局(layouts)
layouts 只是模板的一个特殊子集。 它们位于 templates/layout 文件夹(lib/hello_web/templates/layout)中。 当我们生成应用程序时,Phoenix 为我们创建了三个 layouts。 默认的根布局称为 root.html.heex,它是所有模板默认使用的布局。
由于 layouts 实际上是模板,因此它们需要一个 view 来渲染它们。 这就是在 lib/hello_web/views/layout_view.ex 中定义的 LayoutView。 由于 Phoenix 为我们生成了这个 view,我们不必创建一个新视图,只要我们把要渲染的 layouts 放在 lib/hello_web/templates/layout 目录中即可。
不过,在我们创建新布局之前,让我们做最简单的事情并渲染一个完全没有布局的模板。
Phoenix.Controller 模块提供了 put_root_layout/2 函数供我们切换根布局。 这个函数将 conn 作为它的第一个参数和一个字符串作为我们要渲染的布局的基本名称。 它还接受 false 以完全禁用布局。
您可以在 lib/hello_web/controllers/page_controller.ex 中编辑 PageController 的 index action,使其看起来像这样。
def index(conn, _params) do
conn
|> put_root_layout(false)
|> render("index.html")
end
重新加载 http://localhost:4000/ 后,我们应该看到一个非常不同的页面,一个没有标题、logo 图像或 CSS 样式的页面。
现在让我们实际创建另一个布局并将 index 模板渲染到其中。 例如,假设我们的程序的管理后台有一个不同的布局,它没有 logo 图像。 为此,我们将现有的 root.html.heex 复制到同一目录 lib/hello_web/templates/layout 中的新文件 admin.html.heex。 然后让我们将 admin.html.heex 中显示 logo 的行替换为“Administration”一词。
删除这些行:
<a href="https://phoenixframework.org/" class="phx-logo">
<img src={Routes.static_path(@conn, "/images/phoenix.png")} alt="Phoenix Framework Logo"/>
</a>
替换为:
<p>Administration</p>
然后,在 lib/hello_web/controllers/page_controller.ex 中的 index action 中将新布局的基本名称传递给 put_root_layout/2。
def index(conn, _params) do
conn
|> put_root_layout("admin.html")
|> render("index.html")
end
当我们加载页面时,应该看到渲染的 admin 布局是没有 logo 的,但是有 “Administration”。
覆盖渲染格式 Overriding rendering formats
通过模板渲染 HTML ok 了,但是如果我们需要动态更改渲染格式怎么办? 假设有时我们需要 HTML,有时我们需要纯文本,有时我们需要 JSON。 怎么做呢?
Phoenix 允许我们使用 _format query 参数来动态更改格式。 为了实现这一点,Phoenix 需要在正确的目录中有一个适当命名的视图和一个适当命名的模板。
作为一个例子,让我们从一个新生成的应用程序中获取 PageController 的 index action。 开箱即用,它具有正确的视图 PageView、正确的模板目录 (lib/hello_web/templates/page) 和正确的模板 (index.html.heex)。
def index(conn, _params) do
render(conn, "index.html")
end
它没有的是用于呈现文本的替代模板。 让我们在 lib/hello_web/templates/page/index.text.eex 添加一个。 这是我们的示例 index.text.eex 模板。
OMG, this is actually some text.
要完成这项工作,我们还需要做更多的事情。 我们需要告诉我们的路由它应该接受 text 格式。 我们通过将 text 添加到 :browser 管道中接受的格式列表中来做到这一点。 让我们打开 lib/hello_web/router.ex 并更改 plug :accepts 以包含 text 就像 html 那样。
defmodule HelloWeb.Router do
use HelloWeb, :router
pipeline :browser do
plug :accepts, ["html", "text"]
plug :fetch_session
plug :fetch_live_flash
plug :put_root_layout, {HelloWeb.LayoutView, :root}
plug :protect_from_forgery
plug :put_secure_browser_headers
end
...
我们还需要告诉控制器渲染一个与 Phoenix.Controller.get_format/1 返回的格式相同的模板。 我们通过将模板名称“index.html”替换为原子版本 :index 来实现。
def index(conn, _params) do
render(conn, :index)
end
如果我们访问 http://localhost:4000/?_format=text,我们会看到“OMG, this is actually some text.”。
直接返回响应 Sending responses directly
如果上面的渲染选项都不能完全满足我们的需求,我们可以使用 Plug 提供的一些函数来编写自己的渲染选项。 假设我们要发送状态为“201”且没有任何正文的响应。 我们可以使用 Plug.Conn.send_resp/3 函数来做到这一点。
编辑 index action,位于 lib/hello_web/controllers/page_controller.ex 中的 PageController :
def index(conn, _params) do
send_resp(conn, 201, "")
end
重新加载 http://localhost:4000 应该会显示一个完全空白的页面。 我们浏览器的开发者工具的网络标签应显示“201”(已创建)的响应状态。 某些浏览器 (Safari) 将下载响应,因为未设置内容类型。
为了具体说明 content type ,我们可以将 put_resp_content_type/2 与 send_resp/3 结合使用。
def index(conn, _params) do
conn
|> put_resp_content_type("text/plain")
|> send_resp(201, "")
end
像这样使用 Plug 函数,我们就能够构建我们需要的响应。
设置 Content Type
类似于 _format query 参数,我们可以通过修改 HTTP Content-Type Header 并提供适当的模板来呈现我们想要的任何格式。
如果我们想渲染 index action 的 XML 版本,我们可以在 lib/hello_web/page_controller.ex 中实现类似这样的操作。
def index(conn, _params) do
conn
|> put_resp_content_type("text/xml")
|> render("index.xml", content: 这里放一些xml内容)
end
然后我们需要提供一个 index.xml.eex 模板来创建有效的 XML,我们就完成了。
有关有效内容 mime 类型的列表,请参阅 MIME 库。
设置 HTTP 状态码
我们还可以设置响应的 HTTP 状态码,类似于设置内容类型的方式。 所有控制器都导入了的 Plug.Conn 模块有一个 put_status/2 函数来执行此操作。
Plug.Conn.put_status/2 将 conn 作为第一个参数,将一个整数或一个“友好名称”作为第二个参数,用作我们要设置的状态代码的原子。 可以在 Plug.Conn.Status.code/1 文档中找到状态代码原子表示的列表。
让我们更改 PageController 中的 index action中的状态。
def index(conn, _params) do
conn
|> put_status(202)
|> render("index.html")
end
我们提供的状态码必须是有效数字。
重定向 Redirection
通常,我们需要在请求中间重定向到一个新的 URL。 例如,成功的 create action 通常会重定向到我们刚刚创建的资源的 show action 。 或者,它可以重定向到 index action 以显示同一类型的所有内容。 在很多其他情况下,重定向也很有用。
无论在什么情况下,Phoenix 控制器都提供了方便的 redirect/2 函数,使重定向变得容易。 Phoenix 对重定向到应用程序内的路径和重定向到 URL 进行了区分——无论是在我们的应用程序内部还是在应用程序外部。
为了尝试 redirect/2,让我们在 lib/hello_web/router.ex 中创建一个新路由。
defmodule HelloWeb.Router do
...
scope "/", HelloWeb do
...
get "/", PageController, :index
get "/redirect_test", PageController, :redirect_test
...
end
end
然后改变 PageController 的 index action,让其重定向到我们的新路由。
defmodule HelloWeb.PageController do
use HelloWeb, :controller
def index(conn, _params) do
redirect(conn, to: "/redirect_test")
end
end
实际上,我们应该使用path helper,这是链接到应用程序中任何页面的首选方法,正如我们在路由指南中所了解的那样。
def index(conn, _params) do
redirect(conn, to: Routes.page_path(conn, :redirect_test))
end
当我们重新加载我们的欢迎页面时,我们看到我们已经被重定向到 /redirect_test,它显示了原始的欢迎页面。 有用!
如果我们愿意,我们可以打开我们的开发者工具,点击网络选项卡,然后再次访问我们的根路由。 我们看到这个页面的两个主要请求 - 一个 get 请求到 / 状态为 302,以及一个 get 请求到 /redirect_test 状态为 200。
请注意,重定向函数接受 conn 以及表示我们应用程序中相对路径的字符串。 出于安全原因, :to helper 只能重定向到应用程序中的路径。 如果要重定向到完全限定路径或外部 URL,则应使用 :external 代替:
def index(conn, _params) do
redirect(conn, external: "https://elixir-lang.org/")
end
Flash messages
有时我们需要在操作过程中与用户进行交流。 也许是更新数据时出错了,也许我们只是想在用户回到应用程序时欢迎一下。 为此,我们有即时消息。
Phoenix.Controller 模块提供了 put_flash/3 和 get_flash/2 函数来帮助我们设置和检索 flash 消息,数据是键值对的形式。 让我们在 HelloWeb.PageController 中设置两条 flash 消息来尝试一下。
为此,我们修改 index action 如下:
defmodule HelloWeb.PageController do
...
def index(conn, _params) do
conn
|> put_flash(:info, "Welcome to Phoenix, from flash info!")
|> put_flash(:error, "Let's pretend we have an error.")
|> render("index.html")
end
end
为了查看我们的即显消息,我们需要能够获取它们并将它们显示在模板布局中。 完成第一部分的一种方法是使用 get_flash/2,它接受 conn 和我们关心的 key 。 然后它返回该键的值。
为方便起见,应用程序布局 lib/hello_web/templates/layout/app.html.heex 已经包含用于显示 flash 消息的标记。
<p class="alert alert-info" role="alert"><%= get_flash(@conn, :info) %></p>
<p class="alert alert-danger" role="alert"><%= get_flash(@conn, :error) %></p>
重新载入 欢迎页面,就能看到欢迎信息了。
Flash 功能在与重定向混合使用时非常方便。 也许您想重定向到包含一些额外信息的页面。 如果我们重用上一节中的重定向操作,我们可以这样做:
def index(conn, _params) do
conn
|> put_flash(:info, "Welcome to Phoenix, from flash info!")
|> put_flash(:error, "Let's pretend we have an error.")
|> redirect(to: Routes.page_path(conn, :redirect_test))
end
在次重载 欢迎页面,看看效果。
除了 put_flash/3 和 get_flash/2 之外,Phoenix.Controller 模块还有另一个值得了解的有用函数。 clear_flash/1 只接受 conn 并删除任何可能存储在会话中的 flash 消息。
Phoenix 不强制将哪些 keys 存储在 flash 中。 想存啥存啥。 然而,:info 和 :error 是常见的,并且在我们的模板中默认处理。
Action fallback
Action 回退允许我们将错误处理代码集中在 plugs 中,当控制器 action 无法返回 %Plug.Conn{} 结构时就会调用这些代码。 这些 plugs 接收最初传递给控制器操作的 conn 以及 action 的返回值。
假设我们有一个 show 操作,它使用 with 来获取博客文章,然后授权当前用户查看该博客文章。 在此示例中,我们可能期望 fetch_post/1 在找不到帖子时返回 {:error, :not_found} ,而 authorize_user/3 在用户未经授权时可能返回 {:error, :unauthorized} 。 我们可以使用 Phoenix 为每个新应用程序生成的 ErrorView 来相应地处理这些错误路径:
defmodule HelloWeb.MyController do
use Phoenix.Controller
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.json", post: post)
else
{:error, :not_found} ->
conn
|> put_status(:not_found)
|> put_view(HelloWeb.ErrorView)
|> render(:"404")
{:error, :unauthorized} ->
conn
|> put_status(403)
|> put_view(HelloWeb.ErrorView)
|> render(:"403")
end
end
end
现在想象一下,您可能需要在你的 API 内为每个控制器和 action 实现相似的逻辑。 这将导致大量重复。
相反,我们可以定义一个模块 plug,它知道如何专门处理这些错误情况。 由于控制器也是模块 plug,让我们将 plug 定义为控制器的形式:
defmodule HelloWeb.MyFallbackController do
use Phoenix.Controller
def call(conn, {:error, :not_found}) do
conn
|> put_status(:not_found)
|> put_view(HelloWeb.ErrorView)
|> render(:"404")
end
def call(conn, {:error, :unauthorized}) do
conn
|> put_status(403)
|> put_view(HelloWeb.ErrorView)
|> render(:"403")
end
end
然后我们可以通过 action_fallback 引用新的控制器并从我们的 with 中删除 else 块:
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.json", post: post)
end
end
end
每当 with 条件不匹配时,HelloWeb.MyFallbackController 将接收原始 conn 以及操作结果作为参数并做出相应响应。
完
嗯,这章很重要,多看几遍吧
把翻译当成学习的过程,还是可以的。