查看源代码 分配和 HEEx 模板

LiveView 中的所有数据都存储在套接字中,套接字是一个称为 Phoenix.LiveView.Socket 的服务器端结构。您的数据存储在该结构的 assigns 键下。服务器数据永远不会与客户端共享,超出了您的模板呈现的内容。

Phoenix 模板语言称为 HEEx(HTML+EEx)。EEx 是嵌入式 Elixir,一种 Elixir 字符串模板引擎。这些模板要么是带有 .heex 扩展名的文件,要么是通过 ~H 标识直接在源文件中创建。您可以通过查看 ~H 标识 的文档来了解更多关于 HEEx 语法的知识。

Phoenix.Component.assign/2Phoenix.Component.assign/3 函数有助于存储这些值。这些值可以在 LiveView 中作为 socket.assigns.name 访问,但在 HEEx 模板中作为 @name 访问。

在本节中,我们将介绍 LiveView 如何通过了解分配和模板之间的相互作用来最大限度地减少网络上的负载。

变更跟踪

当您第一次渲染 .heex 模板时,它会将模板的所有静态和动态部分发送到客户端。假设以下模板

<h1><%= expand_title(@title) %></h1>

它有两个静态部分,<h1></h1>,以及一个由 expand_title(@title) 组成的动态部分。进一步渲染此模板将不会重新发送静态部分,并且只有在动态部分发生更改时才会重新发送。

变更跟踪是通过分配完成的。如果 @title 分配发生更改,则 LiveView 将执行模板的动态部分,即 expand_title(@title),并发送新的内容。如果 @title 相同,则不会执行任何操作,也不会发送任何内容。

变更跟踪也适用于访问映射/结构字段。以这个模板为例

<div id={"user_#{@user.id}"}>
  <%= @user.name %>
</div>

如果 @user.name 发生更改,但 @user.id 没有更改,则 LiveView 将仅重新渲染 @user.name,并且根本不会执行或重新发送 @user.id

变更跟踪也适用于渲染其他模板,只要它们也是 .heex 模板

<%= render "child_template.html", assigns %>

或者当使用函数组件时

<.show_name name={@user.name} />

分配跟踪功能还意味着您必须避免在模板中执行直接操作。例如,如果您在模板中执行数据库查询

<%= for user <- Repo.all(User) do %>
  <%= user.name %>
<% end %>

那么 Phoenix 将永远不会重新渲染上面的部分,即使数据库中的用户数量发生变化。相反,您需要在 LiveView 渲染模板之前将用户存储为分配

assign(socket, :users, Repo.all(User))

一般来说,无论您是否使用 LiveView,**数据加载都永远不应该在模板内部进行**。不同之处在于 LiveView 强制执行此最佳实践。

陷阱

在 LiveView 中使用 ~H 标识或 .heex 模板时,需要注意一些常见的陷阱。

变量

由于变量的范围,LiveView 必须在模板中使用变量时禁用变更跟踪,除了由 Elixir 块结构(如 caseforif 等)引入的变量。因此,您必须避免在 HEEx 模板中使用以下代码

<% some_var = @x + @y %>
<%= some_var %>

相反,请使用函数

<%= sum(@x, @y) %>

同样,**不要**在 LiveView 或 LiveComponent 的 render 函数顶部定义变量。由于 LiveView 无法跟踪 sumtitle,因此如果任何一个值发生更改,LiveView 必须重新渲染两者。

def render(assigns) do
  sum = assigns.x + assigns.y
  title = assigns.title

  ~H"""
  <h1><%= title %></h1>

  <%= sum %>
  """
end

相反,请使用 assign/2assign/3assign_new/3update/3 函数来计算它。以这种方式定义或更新的任何分配都将被标记为已更改,而其他分配(如 @title)仍将由 LiveView 跟踪。

assign(assigns, sum: assigns.x + assigns.y)

相同的函数也可以在函数组件内部使用

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>

  <%= @sum %>
  """
end

一般来说,避免在 HEEx 模板中访问变量,因为访问变量的代码在每次渲染时都会被执行。例外情况是 Elixir 块结构引入的变量。例如,访问下面推导定义的 post 变量按预期工作

<%= for post <- @posts do %>
  ...
<% end %>

assigns 变量

说到变量,还值得讨论一下 assigns 特殊变量。每次使用 ~H 标识时,都必须定义一个 assigns 变量,该变量在每个 .heex 模板中也都可用。但是,我们必须避免在模板中直接访问此变量,而应该使用 @ 来访问特定键。这也适用于函数组件。让我们看一些例子。

有时您可能想要将一个函数组件中的所有分配传递给另一个函数组件。例如,假设您有一个带有标题、内容和页脚部分的复杂 card 组件。您可能将组件内部重构为三个较小的组件

def card(assigns) do
  ~H"""
  <div class="card">
    <.card_header {assigns} />
    <.card_body {assigns} />
    <.card_footer {assigns} />
  </div>
  """
end

defp card_header(assigns) do
  ...
end

defp card_body(assigns) do
  ...
end

defp card_footer(assigns) do
  ...
end

由于函数组件处理属性的方式,上面的代码将不会执行变更跟踪,并且会在每次更改时始终重新渲染所有三个组件。

一般来说,您应该避免传递所有分配,而是明确指定子组件需要哪些分配

def card(assigns) do
  ~H"""
  <div class="card">
    <.card_header title={@title} class={@title_class} />
    <.card_body>
      <%= render_slot(@inner_block) %>
    </.card_body>
    <.card_footer on_close={@on_close} />
  </div>
  """
end

如果您确实需要传递所有分配,您应该使用常规的函数调用语法。这是在模板中访问 assigns 可接受的唯一情况

def card(assigns) do
  ~H"""
  <div class="card">
    <%= card_header(assigns) %>
    <%= card_body(assigns) %>
    <%= card_footer(assigns) %>
  </div>
  """
end

这确保了来自父组件的变更跟踪信息被传递到每个子组件,并且只重新渲染必要的部分。但是,一般来说,最好避免完全传递 assigns,而是让 LiveView 找出跟踪更改的最佳方式。

总结

总结一下

  1. 除了 Elixir 结构之外,避免在 HEEx 模板中定义局部变量

  2. 避免在 HEEx 模板中传递或访问 assigns 变量