name: phoenix-views-templates user-invocable: false description: 在Phoenix中使用HEEx模板、函数组件、插槽和赋值渲染视图和模板 allowed-tools: [Bash, Read]
Phoenix视图和模板
Phoenix使用HEEx(HTML+EEx)模板来渲染动态HTML内容。HEEx提供编译时验证、通过自动转义实现的安全性,以及基于组件的架构。Phoenix中的视图模块组织模板渲染逻辑并容纳可重用的函数组件。
视图模块结构
Phoenix视图模块使用embed_templates宏从目录加载HEEx模板:
defmodule HelloWeb.HelloHTML do
use HelloWeb, :html
embed_templates "hello_html/*"
end
这会自动为hello_html/目录中的每个.html.heex文件创建函数。
HEEx模板
基本模板结构
HEEx模板结合HTML与嵌入的Elixir表达式:
<section>
<h2>Hello World, from Phoenix!</h2>
</section>
插值动态内容
使用<%= ... %>将Elixir表达式插入HTML:
<section>
<h2>Hello World, from <%= @messenger %>!</h2>
</section>
@符号访问从控制器传递的赋值。
多行表达式
对于没有输出的表达式,省略=:
<% # 这是一个注释 %>
<% user_name = String.upcase(@user.name) %>
<p>Welcome, <%= user_name %>!</p>
使用赋值
赋值是从控制器传递给模板的键值对:
# 控制器
def show(conn, %{"messenger" => messenger}) do
render(conn, :show, messenger: messenger, receiver: "Dweezil")
end
<!-- 模板 -->
<section>
<h2>Hello <%= @receiver %>, from <%= @messenger %>!</h2>
</section>
所有赋值在模板中使用@前缀访问。
条件渲染
使用if/else
HEEx支持使用if/else块进行条件渲染:
<%= if some_condition? do %>
<p>某些条件对用户<%= @username %>为真</p>
<% else %>
<p>某些条件对用户<%= @username %>为假</p>
<% end %>
使用unless
对于负条件:
<%= unless @user.premium do %>
<div class="upgrade-banner">
升级到高级版以获得更多功能!
</div>
<% end %>
使用case进行模式匹配
对于多个条件:
<%= case @status do %>
<% :pending -> %>
<span class="badge badge-warning">待处理</span>
<% :approved -> %>
<span class="badge badge-success">已批准</span>
<% :rejected -> %>
<span class="badge badge-danger">已拒绝</span>
<% end %>
循环和迭代
For推导式
使用for生成动态列表:
<table>
<tr>
<th>数字</th>
<th>平方</th>
</tr>
<%= for number <- 1..10 do %>
<tr>
<td><%= number %></td>
<td><%= number * number %></td>
</tr>
<% end %>
</table>
遍历集合
遍历列表或映射:
<ul>
<%= for post <- @posts do %>
<li>
<h3><%= post.title %></h3>
<p><%= post.excerpt %></p>
</li>
<% end %>
</ul>
简写:for属性
HEEx为简单迭代提供更简洁的语法:
<ul>
<li :for={item <- @items}><%= item.name %></li>
</ul>
访问索引
使用Enum.with_index/2获取迭代索引:
<%= for {item, index} <- Enum.with_index(@items) do %>
<div class="item-<%= index %>">
<%= item.name %>
</div>
<% end %>
函数组件
函数组件是定义为Elixir函数的可重用UI元素,返回HEEx模板。
定义函数组件
使用attr宏声明属性,使用~H sigil用于模板:
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
使用函数组件
使用<.component_name />语法调用组件:
<section>
<.greet messenger={@messenger} />
</section>
具有默认值的可选属性
定义具有默认值的可选属性:
attr :messenger, :string, default: nil
attr :class, :string, default: "greeting"
def greet(assigns) do
~H"""
<h2 class={@class}>
Hello World<%= if @messenger, do: ", from #{@messenger}" %>!
</h2>
"""
end
多种属性类型
组件可以接受各种属性类型:
attr :title, :string, required: true
attr :count, :integer, default: 0
attr :active, :boolean, default: false
attr :user, :map, required: true
attr :items, :list, default: []
def card(assigns) do
~H"""
<div class={"card" <> if @active, do: " active", else: ""}>
<h3><%= @title %></h3>
<p>计数: <%= @count %></p>
<p>用户: <%= @user.name %></p>
<ul>
<li :for={item <- @items}><%= item %></li>
</ul>
</div>
"""
end
具有计算值的组件
使用assign/2在组件内计算值:
attr :x, :integer, required: true
attr :y, :integer, required: true
attr :title, :string, required: true
def sum_component(assigns) do
assigns = assign(assigns, sum: assigns.x + assigns.y)
~H"""
<h1><%= @title %></h1>
<p>总和: <%= @sum %></p>
"""
end
插槽
插槽允许组件接受内容块,实现强大的组合模式。
定义和使用插槽
定义插槽并渲染它:
slot :inner_block, required: true
def card(assigns) do
~H"""
<div class="card">
<%= render_slot(@inner_block) %>
</div>
"""
end
使用组件与内容:
<.card>
<h2>卡片标题</h2>
<p>卡片内容在这里</p>
</.card>
命名插槽
组件可以有多个命名插槽:
slot :header, required: true
slot :body, required: true
slot :footer
def panel(assigns) do
~H"""
<div class="panel">
<div class="panel-header">
<%= render_slot(@header) %>
</div>
<div class="panel-body">
<%= render_slot(@body) %>
</div>
<%= if @footer != [] do %>
<div class="panel-footer">
<%= render_slot(@footer) %>
</div>
<% end %>
</div>
"""
end
用法:
<.panel>
<:header>
<h2>面板标题</h2>
</:header>
<:body>
<p>面板内容</p>
</:body>
<:footer>
<button>关闭</button>
</:footer>
</.panel>
具有属性的插槽
插槽可以接受属性以进行更动态的渲染:
slot :item, required: true do
attr :title, :string, required: true
attr :highlighted, :boolean, default: false
end
def list(assigns) do
~H"""
<ul>
<%= for item <- @item do %>
<li class={if item.highlighted, do: "highlight"}>
<%= item.title %>: <%= render_slot(item) %>
</li>
<% end %>
</ul>
"""
end
渲染子模板
渲染其他模板
在父模板中包含子模板:
<%= render("child_template.html", assigns) %>
从其他模块渲染组件
调用不同模块的组件:
<MyApp.Components.button text="点击我" />
或使用别名:
alias MyApp.Components
# 在模板中:
<Components.button text="点击我" />
布局模板
使用布局
布局包装渲染的模板。在控制器中配置布局:
def controller do
quote do
use Phoenix.Controller,
formats: [:html, :json],
layouts: [html: HelloWeb.Layouts]
...
end
end
根布局
根布局包含@inner_content占位符:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<title>我的应用</title>
</head>
<body>
<%= @inner_content %>
</body>
</html>
应用布局组件
使用组件嵌套布局:
<Layouts.app flash={@flash}>
<section>
<h2>Hello World, from <%= @messenger %>!</h2>
</section>
</Layouts.app>
禁用布局
渲染时不使用布局:
def home(conn, _params) do
render(conn, :home, layout: false)
end
LiveView集成
委托给Phoenix视图
LiveView可以将渲染委托给现有视图模块:
defmodule AppWeb.ThermostatLive do
use Phoenix.LiveView
def render(assigns) do
Phoenix.View.render(AppWeb.PageView, "page.html", assigns)
end
end
在模板中嵌入LiveView
在静态模板中渲染LiveView组件:
<h1>温度控制</h1>
<%= live_render(@conn, AppWeb.ThermostatLive) %>
LiveView中的函数组件
在LiveView中定义和使用函数组件:
def weather_greeting(assigns) do
~H"""
<div title="我的div" class={@class}>
<p>Hello <%= @name %></p>
<MyApp.Weather.city name="Kraków"/>
</div>
"""
end
测试视图
测试视图渲染
使用render_to_string/4直接测试视图:
defmodule HelloWeb.ErrorHTMLTest do
use HelloWeb.ConnCase, async: true
import Phoenix.Template
test "renders 404.html" do
assert render_to_string(HelloWeb.ErrorHTML, "404", "html", []) == "Not Found"
end
test "renders 500.html" do
assert render_to_string(HelloWeb.ErrorHTML, "500", "html", []) == "Internal Server Error"
end
end
测试函数组件
隔离测试组件:
import Phoenix.LiveViewTest
test "renders greet component" do
assigns = %{messenger: "Phoenix"}
html = rendered_to_string(~H"""
<HelloWeb.HelloHTML.greet messenger={@messenger} />
""")
assert html =~ "Hello World, from Phoenix!"
end
何时使用此技能
当您需要时使用此技能:
- 为Phoenix应用创建动态HTML模板
- 为一致的UI元素构建可重用的函数组件
- 基于应用状态实现条件渲染
- 使用动态数据渲染列表和表格
- 创建具有嵌套组件和插槽的复杂布局
- 将LiveView组件与静态模板集成
- 测试视图渲染逻辑和组件行为
- 构建可访问和语义化的HTML结构
- 使用动态类实现响应式设计
- 创建具有验证反馈的表单
- 显示闪存消息和用户通知
- 渲染导航菜单和面包屑
- 构建基于卡片的布局和仪表板
- 实现分页控制
最佳实践
- 使用函数组件 - 将可重用的UI模式封装在组件中
- 明确声明属性 - 对所有组件属性使用
attr宏 - 提供默认值 - 通过合理的默认值使组件灵活
- 使用语义HTML - 为可访问性选择适当的HTML元素
- 利用插槽 - 使用插槽进行灵活的组件组合
- 保持模板简单 - 将复杂逻辑移动到控制器或上下文
- 使用:for简写 - 对于简单迭代,优先使用
:for属性 - 避免内联样式 - 使用CSS类进行样式设置
- 测试组件 - 为复杂组件逻辑编写测试
- 文档化组件 - 添加文档字符串解释组件用法
- 使用验证路由 - 在模板中始终使用
~psigil - 转义用户内容 - 让HEEx自动处理转义
- 优化渲染 - 最小化模板代码中的计算
- 使用描述性名称 - 清晰地命名组件和属性
- 遵循惯例 - 坚持Phoenix命名模式
常见陷阱
- 将逻辑放在模板中 - 复杂的业务逻辑属于上下文,而不是视图
- 不转义HTML - 使用
raw/1而不清理用户输入 - 深度嵌套模板 - 创建难以维护的模板层次结构
- 缺少属性声明 - 不为组件属性使用
attr宏 - 过度使用内联条件 - 使模板难以阅读
- 不使用组件 - 重复标记而不是提取组件
- 忘记插槽检查 - 不检查是否提供了可选插槽
- 混合关注点 - 将数据获取与呈现逻辑结合
- 大型模板文件 - 创建整体模板而不是组件
- 不一致的格式 - 不遵循HEEx格式约定
- 使用EEx而不是HEEx - 错过编译时验证的好处
- 忽视可访问性 - 不添加ARIA标签和语义标记
- 硬编码值 - 不使用可配置内容的赋值
- 不测试边缘情况 - 缺少nil检查和空状态处理
- 过度嵌套 - 创建深度嵌套的组件树