查看源代码 组件和 HEEx
要求:本指南假定您已阅读了请求生命周期指南。
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
属性支持 if
和 for
表达式的简写语法。例如,与其这样写
<%= 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.ex
中 HelloController
的 index
操作,使其看起来像这样。
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 代码生成器中也扮演着重要的角色,因为代码生成器假定这些组件可用,以便快速搭建您的应用程序。如果您想了解更多关于所有这些部分的信息,您可以
探索生成的
CoreComponents
模块,以从实际示例中学习更多阅读
Phoenix.Component
的官方文档。阅读HEEx 和 ~H 符号的官方文档。