查看源代码 组件和 HEEx

要求:本指南假定您已阅读了入门指南,并已成功运行 Phoenix 应用程序启动并运行

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

Phoenix 终端管道接收请求,将其路由到控制器,并调用视图模块来渲染模板。控制器中的视图接口很简单 - 控制器使用连接分配来调用视图函数,而该函数的工作是返回一个 HEEx 模板。我们将任何接受 assigns 参数并返回 HEEx 模板的函数称为函数组件。函数组件是使用 Phoenix.Component 模块定义的。

函数组件是你在 Phoenix 中执行任何基于标记的模板渲染的必要构建块。它们作为标准 MVC 控制器应用程序、LiveView 应用程序、布局和你在其他模板中使用的更小的 UI 定义的共享抽象。

在本节中,我们将回顾组件如何在之前的章节中使用,并找到它们的新用例。

函数组件

在请求生命周期章节的最后,我们在 lib/hello_web/controllers/hello_html/show.html.heex 中创建了一个模板,让我们打开它

<section>
  <h2>Hello World, from <%= @messenger %>!</h2>
</section>

此模板作为 HelloHTML 的一部分嵌入在 lib/hello_web/controllers/hello_html.ex

defmodule HelloWeb.HelloHTML do
  use HelloWeb, :html

  embed_templates "hello_html/*"
end

这很简单。只有两行,use HelloWeb, :html。这行代码调用 HelloWeb 中定义的 html/0 函数,该函数为我们的函数组件和模板设置基本导入和配置。

我们在模块中进行的所有导入和别名也将在我们的模板中可用。这是因为模板实际上被编译到它们各自模块中的函数中。例如,如果在模块中定义一个函数,就可以直接从模板中调用它。让我们在实践中看看。

假设我们想重构我们的 show.html.heex,将 <h2>Hello World, from <%= @messenger %>!</h2> 的渲染移动到它自己的函数中。我们可以将其移动到 HelloHTML 中的函数组件,让我们这样做

defmodule HelloWeb.HelloHTML do
  use HelloWeb, :html

  embed_templates "hello_html/*"

  attr :messenger, :string, required: true

  def greet(assigns) do
    ~H"""
    <h2>Hello World, from <%= @messenger %>!</h2>
    """
  end
end

我们通过 Phoenix.Component 提供的 attr/3 宏声明了我们接受的属性,然后我们定义了我们的 greet/1 函数,该函数返回 HEEx 模板。

接下来我们需要更新 show.html.heex

<section>
  <.greet messenger={@messenger} />
</section>

当我们重新加载 http://localhost:4000/hello/Frank 时,我们应该看到与以前相同的内容。

由于模板嵌入在 HelloHTML 模块中,因此我们能够简单地以 <.greet messenger="..." /> 的方式调用视图函数。

如果组件在其他地方定义,我们也可以键入 <HelloWeb.HelloHTML.greet messenger="..." />

通过将属性声明为必需,Phoenix 将在编译时发出警告,如果我们在没有传递属性的情况下调用 <.greet /> 组件。如果属性是可选的,可以指定 :default 选项和一个值

attr :messenger, :string, default: nil

虽然这是一个快速示例,但它展示了函数组件在 Phoenix 中扮演的不同角色

  • 函数组件可以定义为接受 assigns 作为参数并调用 ~H 标记的函数,就像我们在 greet/1 中所做的那样

  • 函数组件可以从模板文件嵌入,这就是我们将 show.html.heex 加载到 HelloWeb.HelloHTML 中的方式

  • 函数组件可以声明哪些属性是预期的,这些属性将在编译时进行验证

  • 函数组件可以直接从控制器渲染

  • 函数组件可以直接从其他函数组件渲染,就像我们将 <.greet messenger={@messenger} />show.html.heex 中调用一样

还有更多。在我们深入探讨之前,让我们充分理解 HEEx 模板语言背后的表达能力。

HEEx

函数组件和模板文件由 HEEx 模板语言 提供支持,它代表“HTML+EEx”。EEx 是一个 Elixir 库,它使用 <%= expression %> 来执行 Elixir 表达式并将它们的结果插入到模板中。这经常用于显示我们通过 @ 快捷方式设置的分配。在您的控制器中,如果调用

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

然后您可以在模板中以 <%= @username %> 的方式访问用户名。除了显示分配和函数之外,我们还可以使用几乎所有 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 %>

甚至循环

<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>

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

HEEx 还附带了一些有用的 HTML 扩展,我们将在下一步学习。

HTML 扩展

除了允许通过 <%= %> 插入 Elixir 表达式之外,.heex 模板还带有支持 HTML 的扩展。例如,让我们看看如果尝试在其中插入包含“<”或“>”的值会发生什么,这会导致 HTML 注入

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

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

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

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 模板会告知您错误。

HEEx 还通过特殊的 :if:for 属性支持 iffor 表达式的简写语法。例如,与其这样写

<%= if @some_condition do %>
  <div>...</div>
<% end %>

您可以这样写

<div :if={@some_condition}>...</div>

同样,for 推导可以写成

<ul>
  <li :for={item <- @items}><%= item.name %></li>
</ul>

布局

布局只是函数组件。它们像所有其他函数组件模板一样在模块中定义。在新生成的应用程序中,它是 lib/hello_web/components/layouts.ex。您还会在 layouts 文件夹中找到两个由 Phoenix 生成的内置布局。默认的根布局称为 root.html.heex,它是默认情况下所有模板将渲染到的布局。第二个是应用程序布局,称为 app.html.heex,它在根布局中渲染并包含我们的内容。

您可能想知道渲染后的视图产生的字符串如何最终出现在布局中。这是一个好问题!如果我们查看 lib/hello_web/components/layouts/root.html.heex,在 <body> 结束的地方,我们会看到这一点。

<%= @inner_content %>

换句话说,在渲染页面后,结果被放置在 @inner_content 分配中。

Phoenix 提供了各种便利功能来控制要渲染哪个布局。例如,Phoenix.Controller 模块为我们提供了 put_root_layout/2 函数来切换根布局。它以 conn 作为第一个参数,以及格式和布局的关键字列表。您可以将其设置为 false 来完全禁用布局。

您可以编辑 lib/hello_web/controllers/hello_controller.exHelloControllerindex 操作,使其看起来像这样。

def index(conn, _params) do
  conn
  |> put_root_layout(html: false)
  |> render(:index)
end

重新加载 http://localhost:4000/hello 后,我们应该看到一个非常不同的页面,没有标题或 CSS 样式。

要自定义应用程序布局,我们调用一个名为 put_layout/2 的类似函数。让我们实际创建一个另一个布局并将 index 模板渲染到其中。例如,假设我们有一个用于应用程序管理部分的不同布局,它没有徽标图像。为此,将现有的 app.html.heex 复制到同一目录 lib/hello_web/components/layouts 中的新文件 admin.html.heex 中。然后在新建文件中的 <header>...</header> 标签内删除所有内容(或将其更改为您想要的任何内容)。

现在,在 lib/hello_web/controllers/hello_controller.ex 中控制器的 index 操作中,添加以下内容

def index(conn, _params) do
  conn
  |> put_layout(html: :admin)
  |> render(:index)
end

加载页面后,我们应该在没有标题(或您编写的自定义标题)的情况下渲染管理布局。

此时,您可能想知道,为什么 Phoenix 有两个布局?

首先,它给了我们灵活性。在实践中,我们几乎不会有多个根布局,因为它们通常只包含 HTML 标题。这使我们能够只关注不同的应用程序布局,其中只有更改的部分。其次,Phoenix 附带了一个名为 LiveView 的功能,它允许我们使用服务器端渲染的 HTML 构建丰富且实时的用户体验。LiveView 能够动态更改页面的内容,但它只更改应用程序布局,而不会更改根布局。查看 LiveView 文档 了解更多信息。

CoreComponents

在一个新的 Phoenix 应用程序中,您还会在 components 文件夹中找到一个 core_components.ex 模块。此模块是定义函数组件以在整个应用程序中重用的一个很好的例子。这保证了随着应用程序的发展,我们的组件将保持一致。

如果您查看位于 lib/hello_web.ex 中的 HelloWeb 中的 def html,您会发现 CoreComponents 通过 use HelloWeb, :html 自动导入到所有 HTML 视图中。这也是 CoreComponents 本身在顶部执行 use Phoenix.Component 而不是 use HelloWeb, :html 的原因:执行后者会导致死锁,因为我们会尝试将 CoreComponents 导入自身。

CoreComponents 在 Phoenix 代码生成器中也扮演着重要的角色,因为代码生成器假定这些组件可用,以便快速搭建您的应用程序。如果您想了解更多关于所有这些部分的信息,您可以