Elixir Phoenix 指南 - 视图和模板 - 精简翻译

396 阅读10分钟

Views and templates 视图和模板

Phoenix 视图的主要工作是渲染响应的主体,该主体被发送回浏览器和 API 客户端。 大多数时候,我们使用模板来构建这些响应,但我们也可以手工构建它们。 我们将学习如何来实现。

渲染模板

Phoenix 假定从控制器到视图再到它们渲染的模板都有严格的命名约定。 PageController 需要一个 PageView 来渲染 lib/hello_web/templates/page/ 目录中的模板。 虽然所有这些都可以自定义(请参阅 Phoenix.ViewPhoenix.Template 了解更多信息),但我们建议用户坚持使用 Phoenix 的风格。

新生成的 Phoenix 应用程序具有三个视图模块 - ErrorViewLayoutViewPageView - 它们都位于 lib/hello_web/views/ 目录中。

让我们快速了解一下 LayoutView

defmodule HelloWeb.LayoutView do
  use HelloWeb, :view
end

这很简单。 只有一行,use HelloWeb,:view。 这一行调用 HelloWeb 中定义的 view/0 函数,它为我们的视图和模板设置基本的 imports 和 configuration。

我们在视图中创建的所有 imports 和 aliases 也将在我们的模板中可用。 那是因为模板也被有效地编译成各自视图中的函数。 例如,如果您在视图中定义了一个函数,您将能够直接从模板中调用它。 让我们在实践中看看这个。

打开我们的应用程序布局模板,lib/hello_web/templates/layout/root.html.heex,并更改这一行,

<%= live_title_tag assigns[:page_title] || "Hello", suffix: " · Phoenix Framework" %>

调用 title/0 函数:

<title><%= title() %></title>

LayoutView 中添加这个 title/0 函数。

defmodule HelloWeb.LayoutView do
  use HelloWeb, :view

  def title() do
    "Awesome New Title!"
  end
end

当我们重新加载我们的主页时,我们应该看到我们的新标题。 由于模板是在视图中编译的,我们可以简单地调用视图函数像 title(),否则我们将不得不键入 HelloWeb.LayoutView.title()

您可能还记得,Elixir 模板使用 .heex,它代表“HTML+EEx”。 EEx 是一个 Elixir 库,它使用 <%= expression %> 来执行 Elixir 表达式并将它们的结果插入到模板中。 这经常用于显示我们通过 @ 快捷方式设置的 assigns。 在您的控制器中,如果您调用:

  render(conn, "show.html", username: "joe")

然后您可以在模板中访问所述用户名,像这样 <%= @username %>。 除了展示 assigns和函数之外,我们几乎可以使用任何 Elixir 表达式。 例如,条件:

<%= if some_condition? do %>
  <p>Some condition is true for user: <%= @username %></p>
<% else %>
  <p>Some condition is false for user: <%= @username %></p>
<% end %>

这里可以类比: vue 模板中的 v-if

或者 循环:

<table>
  <tr>
    <th>Number</th>
    <th>Power</th>
  </tr>
<%= for number <- 1..10 do %>
  <tr>
    <td><%= number %></td>
    <td><%= number * number %></td>
  </tr>
<% end %>
</table>

这里可以类比: vue 模板中的 v-for

您是否注意到上面 <%= %><% %> 的用法? 所有向模板输出内容的表达式都必须使用等号 (=)。 如果不包含 =,代码仍将执行,但不会将任何内容插入到模板中。

HTML extensins

除了允许通过 <%= %> 插入 Elixir 表达式外,.heex 模板还带有 HTML-aware 扩展。 例如,让我们看看如果您尝试插入一个带有“<”或“>”的值会发生什么,这会导致 HTML 注入:

<%= "<b>Bold?</b>" %>

渲染模板后,您将在页面上看到文字 <b>。 这意味着用户无法在页面上注入 HTML 内容。 如果你想让他们这样做,你可以调用 raw,但要格外小心:

<%= raw "<b>Bold?</b>" %>

类比 vue 中的 v-html

HEEx 模板的另一个超强功能 是校验 HTML 和属性的精简插值语法。 你可以写:

<div title="My div" class={@class}>
  <p>Hello <%= @username %></p>
</div>

请注意如何简单地使用 key={value}。 HEEx 将自动处理特殊值,例如 false 以删除属性或类列表。

要在关键字列表或映射中插入动态数量的属性,请执行以下操作:

<div title="My div" {@many_attributes}>
  <p>Hello <%= @username %></p>
</div>

另外,尝试删除结尾的 </div> 或将其重命名为 </div-typo>。 HEEx 模板会让您知道您的错误。

HTML components - 组件

HEEx 提供的最后一个特性是组件的思想。 组件是纯函数,可以是本地的(同一模块)或远程的(外部模块)。

HEEx 允许使用类似 HTML 的符号在模板中直接调用这些函数组件。 例如远程函数:

<MyApp.Weather.city name="Kraków"/>

可以使用点调用局部函数:

<.city name="Kraków"/>

其中组件可以定义如下:

defmodule MyApp.Weather do
  use Phoenix.Component

  def city(assigns) do
    ~H"""
    The chosen city is: <%= @name %>.
    """
  end

  def country(assigns) do
    ~H"""
    The chosen country is: <%= @name %>.
    """
  end
end

在上面的示例中,我们使用 ~H sigil 语法将 HEEx 模板直接嵌入到我们的模块中。 我们已经调用了 city 组件,调用 country 组件也不会有什么不同:

<div title="My div" {@many_attributes}>
  <p>Hello <%= @username %></p>
  <MyApp.Weather.country name="Brazil" />
</div>

您可以在 Phoenix.Component 中了解有关组件的更多信息。

了解模板编译

Phoenix 模板被编译成 Elixir 代码,这使得它们具有极高的性能。 让我们进一步了解这一点。

当一个模板被编译成一个视图时,它被简单地编译为一个 render/2 函数,它需要两个参数:模板名称和 assigns。

您可以通过将此函数子句临时添加到 lib/hello_web/views/page_view.ex 中的 PageView 模块来证明这一点。

defmodule HelloWeb.PageView do
  use HelloWeb, :view

  def render("index.html", assigns) do
    "rendering with assigns #{inspect Map.keys(assigns)}"
  end
end

现在,如果您使用 mix phx.server 启动服务器并访问 http://localhost:4000, 您应该会在布局标题下方看到以下文本,而不是主模板页面:

rendering with assigns [:conn]

通过在 render/2 中定义我们自己的子句,它比模板具有更高的优先级,但模板仍然存在,您可以通过简单地删除新添加的子句来验证这一点。

很整洁,对吧? 在编译时,Phoenix 预编译所有 *.html.heex 模板并将它们转换为各自视图模块上的 render/2 函数子句。 在运行时,所有模板都已加载到内存中。 不涉及磁盘读取、复杂的文件缓存或模板引擎计算。

手动渲染模板

到目前为止,Phoenix 已经负责为我们准备好一切并且可以渲染视图。 但是,我们也可以直接渲染视图。

让我们创建一个新模板来试用,lib/hello_web/templates/page/test.html.heex

This is the message: <%= @message %>

这不对应于我们控制器中的任何操作,这很好。 我们将在 IEx 会话中进行练习。 在项目的根目录下,我们可以运行 iex -S mix,然后显式渲染我们的模板。 让我们通过调用 Phoenix.View.render/3 来尝试一下,其中包含视图名称、模板名称和一组我们可能想要传递的 assigns,我们将渲染的模板作为字符串:

iex(1)> Phoenix.View.render(HelloWeb.PageView, "test.html", message: "Hello from IEx!")
%Phoenix.LiveView.Rendered{
  dynamic: #Function<1.71437968/1 in Hello16Web.PageView."test.html"/1>,
  fingerprint: 142353463236917710626026938006893093300,
  root: false,
  static: ["This is the message: ", ""]
}

我们上面得到的输出不是很有帮助。 这是 Phoenix 如何保存我们渲染的模板的内部表示。 幸运的是,我们可以使用 render_to_string/3 将它们转换成字符串:

iex(2)> Phoenix.View.render_to_string(HelloWeb.PageView, "test.html", message: "Hello from IEx!")
"This is the message: Hello from IEx!"

那好多了! 让我们测试一下 HTML 转义,只是为了好玩:

iex(3)> Phoenix.View.render_to_string(HelloWeb.PageView, "test.html", message: "<script>badThings();</script>")
"This is the message: &lt;script&gt;badThings();&lt;/script&gt;"

共享视图和模板

现在我们已经熟悉了 Phoenix.View.render/3,我们准备好从其他视图和模板中共享视图和模板。 我们使用 render/3 来组合我们的模板,最后 Phoenix 会将它们全部转换为正确的表示形式以发送到浏览器。

例如,如果你想从我们的布局中渲染 test.html 模板,你可以直接从布局 lib/hello_web/templates/layout/root.html.heex 调用 render/3

<%= Phoenix.View.render(HelloWeb.PageView, "test.html", message: "Hello from layout!") %>

如果您访问欢迎页面,您应该会看到来自布局的消息。

由于 Phoenix.View 自动导入到我们的模板中,我们甚至可以跳过 Phoenix.View 模块名称并直接调用 render(...)

<%= render(HelloWeb.PageView, "test.html", message: "Hello from layout!") %>

如果你想在同一个视图中渲染一个模板,你可以跳过视图名称,直接调用 render("test.html", message: "Hello from sibling template!") 来代替。 例如,打开 lib/hello_web/templates/page/index.html.heex 并在顶部添加:

<%= render("test.html", message: "Hello from sibling template!") %>

现在,如果您访问欢迎页面,您会看到还显示了模板结果。

Layouts

布局只是模板。 他们有一个视图,就像其他模板一样。 在新生成的应用程序中,这是 lib/hello_web/views/layout_view.ex。 您可能想知道渲染视图产生的字符串如何在布局中结束。 这是一个很好的问题! 如果我们查看 lib/hello_web/templates/layout/root.html.heex,就在 <body> 的末尾,我们会看到这个。

<%= @inner_content %>

换句话说,内部模板放在 @inner_content assign 中。

渲染 JSON

视图的工作不仅仅是呈现 HTML 模板。 视图是关于数据呈现的。 给定一块数据,视图的目的是在给定某种格式(HTML、JSON、CSV 或其他格式)的情况下以有意义的方式呈现该数据。 现在许多 Web 应用程序将 JSON 返回给远程客户端,而 Phoenix 视图非常适合 JSON 渲染。

Phoenix 使用 Jason 库对 JSON 进行编码,因此我们在视图中需要做的就是将我们想要响应的数据格式化为 list 或 map,Phoenix 将完成剩下的工作。

虽然可以直接从控制器返回 JSON 进行响应并跳过视图,但 Phoenix 视图提供了一种更加结构化的方法来执行此操作。 让我们以我们的 PageController 为例,看看当我们以 JSON 而不是 HTML 的形式响应一些静态页面时它会是什么样子。

defmodule HelloWeb.PageController do
  use HelloWeb, :controller

  def show(conn, _params) do
    page = %{title: "foo"}

    render(conn, "show.json", page: page)
  end

  def index(conn, _params) do
    pages = [%{title: "foo"}, %{title: "bar"}]

    render(conn, "index.json", pages: pages)
  end
end

在这里,我们有 show/2index/2 actioins 返回静态页面数据。 我们没有将 "show.html" 作为模板名称传递给 render/3,而是传递 "show.json"。 这样,我们可以拥有负责通过对不同文件类型进行模式匹配来呈现 HTML 和 JSON 的视图。

defmodule HelloWeb.PageView do
  use HelloWeb, :view

  def render("index.json", %{pages: pages}) do
    %{data: Enum.map(pages, fn page -> %{title: page.title} end)}
  end

  def render("show.json", %{page: page}) do
    %{data: %{title: page.title}}
  end
end

在视图中,我们看到我们的 render/2 函数模式匹配 "index.json""show.json""page.json""index.json""show.json" 是直接从控制器请求的。 它们还匹配控制器发送的 assigns。 Phoenix 可以理解 .json 扩展,并负责将我们返回的数据结构转换为 JSON。 "index.json" 会这样响应:

{
  "data": [
    {
     "title": "foo"
    },
    {
     "title": "bar"
    },
 ]
}

"show.json" 会这样:

{
  "data": {
    "title": "foo"
  }
}

但是,index.jsonshow.json 之间存在一些重复,因为两者都对相同的逻辑(渲染 pages)进行编码。 我们可以通过将页面渲染移动到单独的函数子句并使用 render_many/3render_one/3 来重用它来解决这个问题:

defmodule HelloWeb.PageView do
  use HelloWeb, :view

  def render("index.json", %{pages: pages}) do
    %{data: render_many(pages, HelloWeb.PageView, "page.json")}
  end

  def render("show.json", %{page: page}) do
    %{data: render_one(page, HelloWeb.PageView, "page.json")}
  end

  def render("page.json", %{page: page}) do
    %{title: page.title}
  end
end

render_many/3 函数将我们想要响应的数据(pages)、一个视图和一个字符串作为参数,以在视图上定义的 render/2 函数上进行模式匹配。 它将映射 pages 中的每项并调用 PageView.render("page.json", %{page: page})render_one/3 遵循相同的函数参数,最终使用 render/2 匹配 page.json 来表示每个 page长啥样。

像这样构建我们的视图是很有用的,这样它们就可以组合了。 想象这样一种情况,我们的 PageAuthor 有 has_many 关系(#注意:我们还没有讨论 has_many 关系#),根据请求,我们可能希望将作者数据与页面一起发回。 我们可以使用新的 render/2 轻松完成此操作:

defmodule HelloWeb.PageView do
  use HelloWeb, :view
  alias HelloWeb.AuthorView

  def render("page_with_authors.json", %{page: page}) do
    %{title: page.title,
      authors: render_many(page.authors, AuthorView, "author.json")}
  end

  def render("page.json", %{page: page}) do
    %{title: page.title}
  end
end

assigns 中使用的名称由视图确定。 例如 PageView 将使用 %{page: page}AuthorView 将使用 %{author: author}。 这可以用 as 选项覆盖。 假设作者视图使用 %{writer: writer} 而不是 %{author: author}

def render("page_with_authors.json", %{page: page}) do
  %{title: page.title,
    authors: render_many(page.authors, AuthorView, "author.json", as: :writer)}
end

错误页面

Phoenix 有一个名为 ErrorView 的视图,它位于 lib/hello_web/views/error_view.ex 中。 ErrorView的目的是从一个集中位置以通用方式处理错误。 与我们在本指南中构建的视图类似,错误视图可以返回 HTML 和 JSON 响应。 有关详细信息,请参阅自定义错误页面操作方法。

这一章是讲数据渲染的一些方式,如果只用来构建 api,则 HTML 模板的部分可以不用深入学习。