Phoenix视图和模板渲染Skill phoenix-views-templates

Phoenix视图和模板技能用于在Elixir的Phoenix框架中创建动态HTML内容,涉及使用HEEx模板进行服务器端渲染,定义可重用的函数组件,使用插槽进行组件组合,以及管理从控制器传递的赋值。关键词:Phoenix、HEEx、视图、模板、函数组件、插槽、Web开发、Elixir、服务器端渲染、UI组件。

后端开发 0 次安装 0 次浏览 更新于 3/25/2026

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

何时使用此技能

当您需要时使用此技能:

  1. 为Phoenix应用创建动态HTML模板
  2. 为一致的UI元素构建可重用的函数组件
  3. 基于应用状态实现条件渲染
  4. 使用动态数据渲染列表和表格
  5. 创建具有嵌套组件和插槽的复杂布局
  6. 将LiveView组件与静态模板集成
  7. 测试视图渲染逻辑和组件行为
  8. 构建可访问和语义化的HTML结构
  9. 使用动态类实现响应式设计
  10. 创建具有验证反馈的表单
  11. 显示闪存消息和用户通知
  12. 渲染导航菜单和面包屑
  13. 构建基于卡片的布局和仪表板
  14. 实现分页控制

最佳实践

  1. 使用函数组件 - 将可重用的UI模式封装在组件中
  2. 明确声明属性 - 对所有组件属性使用attr
  3. 提供默认值 - 通过合理的默认值使组件灵活
  4. 使用语义HTML - 为可访问性选择适当的HTML元素
  5. 利用插槽 - 使用插槽进行灵活的组件组合
  6. 保持模板简单 - 将复杂逻辑移动到控制器或上下文
  7. 使用:for简写 - 对于简单迭代,优先使用:for属性
  8. 避免内联样式 - 使用CSS类进行样式设置
  9. 测试组件 - 为复杂组件逻辑编写测试
  10. 文档化组件 - 添加文档字符串解释组件用法
  11. 使用验证路由 - 在模板中始终使用~p sigil
  12. 转义用户内容 - 让HEEx自动处理转义
  13. 优化渲染 - 最小化模板代码中的计算
  14. 使用描述性名称 - 清晰地命名组件和属性
  15. 遵循惯例 - 坚持Phoenix命名模式

常见陷阱

  1. 将逻辑放在模板中 - 复杂的业务逻辑属于上下文,而不是视图
  2. 不转义HTML - 使用raw/1而不清理用户输入
  3. 深度嵌套模板 - 创建难以维护的模板层次结构
  4. 缺少属性声明 - 不为组件属性使用attr
  5. 过度使用内联条件 - 使模板难以阅读
  6. 不使用组件 - 重复标记而不是提取组件
  7. 忘记插槽检查 - 不检查是否提供了可选插槽
  8. 混合关注点 - 将数据获取与呈现逻辑结合
  9. 大型模板文件 - 创建整体模板而不是组件
  10. 不一致的格式 - 不遵循HEEx格式约定
  11. 使用EEx而不是HEEx - 错过编译时验证的好处
  12. 忽视可访问性 - 不添加ARIA标签和语义标记
  13. 硬编码值 - 不使用可配置内容的赋值
  14. 不测试边缘情况 - 缺少nil检查和空状态处理
  15. 过度嵌套 - 创建深度嵌套的组件树

资源