Assigns и шаблоны HEEx
Исходный кодВсе данные в LiveView хранятся в сокете — серверной структуре данных под названием Phoenix.LiveView.Socket. Ваши собственные данные хранятся в поле assigns этой структуры. Данные сервера никогда не передаются клиенту, кроме тех, которые выводит шаблон.
Язык шаблонов Phoenix называется HEEx (HTML+EEx). EEx — это Embedded Elixir, механизм шаблонов строк на Elixir. Такие шаблоны либо хранятся в файлах с расширением .heex, либо создаются непосредственно в исходных файлах с помощью сигила ~H. Подробнее о синтаксисе HEEx можно узнать в документации по сигилу ~H.
Функции Phoenix.Component.assign/2 и Phoenix.Component.assign/3 помогают хранить эти значения. В LiveView к ним можно обращаться как к socket.assigns.name, а внутри шаблонов HEEx — как к @name.
В этом разделе мы расскажем, как LiveView сокращает объём данных, передаваемых по сети, учитывая взаимодействие assigns и шаблонов.
Отслеживание изменений
При первом отображении шаблона .heex клиенту отправляются все его статические и динамические части. Рассмотрим следующий шаблон:
<h1>{expand_title(@title)}</h1>
В нём есть две статические части — <h1> и </h1> — и одна динамическая часть, состоящая из expand_title(@title). При последующих отображениях этого шаблона статические части не отправляются повторно, а динамическая отправляется только в случае изменения.
Изменения отслеживаются с помощью assigns. Если assign @title изменится, LiveView выполнит динамические части шаблона, expand_title(@title), и отправит новое содержимое. Если @title не изменился, ничего не выполняется и не отправляется.
Отслеживание изменений также работает при обращении к полям map/struct. Рассмотрим этот шаблон:
<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} />
Отслеживание assigns также означает, что в шаблоне НЕЛЬЗЯ выполнять операции напрямую. Например, если выполнить запрос к базе данных в шаблоне:
<%= for user <- Repo.all(User) do %>
{user.name}
<% end %>
то Phoenix никогда не будет повторно отображать раздел выше, даже если количество пользователей в базе данных изменится. Вместо этого перед отображением шаблона нужно сохранить пользователей в assigns LiveView:
assign(socket, :users, Repo.all(User))
В целом, загрузка данных никогда не должна происходить внутри шаблона, независимо от того, используете ли вы LiveView. Разница в том, что LiveView обеспечивает соблюдение этой рекомендации.
Распространённые ошибки
При использовании сигила ~H или шаблонов .heex внутри LiveView важно помнить о нескольких распространённых ошибках.
Переменные
Из-за области видимости переменных LiveView отключает отслеживание изменений при использовании переменных в шаблоне. Исключение составляют переменные, введённые блочными конструкциями Elixir, например case, for, if и другими. Поэтому в шаблонах HEEx не следует писать код вроде этого:
<% some_var = @x + @y %>
{some_var}
Вместо этого используйте функцию:
{sum(@x, @y)}
Аналогично, не следует определять переменные в начале функции render для LiveView или LiveComponents. Поскольку LiveView не может отслеживать sum или title, при изменении любого из этих значений LiveView придётся повторно отобразить оба.
def render(assigns) do
sum = assigns.x + assigns.y
title = assigns.title
~H"""
<h1>{title}</h1>
{sum}
"""
end
Вместо этого используйте функции assign/2, assign/3, assign_new/3 и update/3 для вычисления этого значения. Любой assign, определённый или обновлённый таким способом, будет отмечен как изменённый, а другие assigns, например @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, например if и for. Например, обращение к переменной post, определённой в приведённом ниже включении, работает как ожидается:
<%= for post <- @posts do %> ... <% end %>
Переменная assigns
Раз уж речь зашла о переменных, стоит также обсудить специальную переменную assigns. Каждый раз, когда вы используете сигил ~H, необходимо определить переменную assigns, которая также доступна в каждом шаблоне .heex. Однако не следует обращаться к этой переменной напрямую внутри шаблонов — для доступа к отдельным ключам используйте @. Это относится и к функциональным компонентам. Рассмотрим несколько примеров.
Иногда может потребоваться передать все assigns из одного функционального компонента в другой. Например, представьте сложный компонент 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
Из-за особенностей работы функциональных компонентов с атрибутами приведённый выше код не будет отслеживать изменения и при каждом изменении будет повторно отображать все три компонента.
Как правило, не следует передавать все assigns. Вместо этого явно указывайте, какие assigns нужны дочерним компонентам:
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, используйте обычный синтаксис вызова функции. Это единственный случай, когда допустимо обращаться к assigns внутри шаблонов:
def card(assigns) do
~H"""
<div class="card">
{card_header(assigns)}
{card_body(assigns)}
{card_footer(assigns)}
</div>
"""
end
Так информация об отслеживании изменений передаётся от родительского компонента каждому дочернему, и повторно отображается только необходимое. Однако в целом лучше вообще не передавать assigns, а предоставить LiveView возможность самостоятельно выбрать оптимальный способ отслеживания изменений.
Изменение переменной assigns
Никогда не изменяйте переменную assigns в функциональном компоненте с помощью универсальных функций, таких как Map.put/3 или Map.merge/2. Вместо этого используйте Phoenix.Component.assign/2, Phoenix.Component.assign/3, Phoenix.Component.assign_new/3 или Phoenix.Component.update/3. Если изменить переменную assigns, например, с помощью Map.put/3, assigns внутри шаблона HEEx не будут обновляться после первоначального отображения. Для работы отслеживания изменений необходимо использовать специальные функции assign из LiveView.
Итак, никогда не делайте так:
def card(assigns) do
assigns = Map.put(assigns, :sum, Enum.sum(assigns.values))
~H"""
<p>{@sum}</p>
"""
end
Вместо этого делайте так:
def card(assigns) do
assigns = assign(assigns, :sum, Enum.sum(assigns.values))
~H"""
<p>{@sum}</p>
"""
end
Если здесь использовать Map.put/3 вместо assign/3, assign sum не будет обновлён, если изменить values после первоначального отображения шаблона HEEx.
Включения
HEEx поддерживает включения в шаблонах, позволяющие обходить списки и коллекции. Например:
<%= for post <- @posts do %>
<section>
<h1>{expand_title(post.title)}</h1>
</section>
<% end %>
Или с помощью специального атрибута :for:
<section :for={post <- @posts}>
<h1>{expand_title(post.title)}</h1>
</section>
Включения в шаблонах оптимизированы: статические части включения отправляются только один раз, независимо от количества элементов. Кроме того, LiveView отслеживает изменения в коллекции, переданной во включение. В идеальном случае, если изменится только одна запись в @posts, повторно будет отправлена только эта запись. По умолчанию для отслеживания изменений используется индекс. Это означает, что при вставке записи все записи после неё будут считаться изменёнными и отправятся повторно. Чтобы оптимизировать это поведение, в HEEx можно также передать тегам атрибут :key:
<section :for={post <- @posts} :key={post.id}>
<h1>{expand_title(post.title)}</h1>
</section>
Подробнее об :key читайте в документации по sigil_H/2.
Для отслеживания изменений во включениях LiveView выполняет дополнительные операции учёта, требующие дополнительной памяти на сервере. Если потребление памяти вызывает беспокойство, рассмотрите возможность использования Phoenix.LiveView.stream/4, позволяющего управлять коллекциями без хранения их в памяти.
Итоги
Итак:
Не определяйте локальные переменные в шаблонах HEEx, кроме как внутри конструкций Elixir.
Не передавайте переменную
assignsи не обращайтесь к ней внутри шаблонов HEEx.Для изменения переменной
assignsиспользуйте только специальные функции assign из LiveView.
© 2014 Chris McCord
Licensed under the MIT License.
https://phoenix-live-view.hexdocs.pm/assigns-eex.html