Перейти к содержанию
Mercury
Русский
Дизайн книга
Меню
Русский

Острова: элемент и его монтирование

Сервер выводит элемент, указывает LiveView не менять его содержимое и передаёт свойства. Клиент находит компонент по имени или URL и рендерит его в этом элементе.

Остров — элемент серверной страницы, содержимым которого управляет клиентский компонент. Урок разбирает этот элемент: что записывает сервер, что читает клиент и что происходит при несовпадении их ожиданий.

Что такое остров

Элемент становится островом благодаря трём вещам:

  • phx-update="ignore". LiveView применяет патчи к странице вокруг элемента, но не внутри него, поэтому результат рендеринга компонента сохраняется после каждого обновления LiveView.
  • phx-hook. LiveView вызывает у именованного хука mounted, updated, reconnected и destroyed, передавая элемент как this.el. Хук монтирует компонент.
  • Свойства на элементе. Сервер записывает входные данные компонента в элемент, поэтому первый рендеринг не требует отдельного запроса.

В репозитории есть два способа создать такой элемент: LiveMercury.mercury/1 с хуком MercuryHook и EchoEduWeb.Island.island/1 из EDU с хуком CourseIsland.

Вариант LiveView — LiveMercury.mercury/1

mercury/1 — функциональный компонент. Каждый assign становится свойством, кроме зарезервированных:

Assign Назначение
name имя компонента в клиентской карте getHooks; обязательно
id идентификатор элемента; если не задан, "<name>-<n>" из счётчика в процессе LiveView
class классы элемента
ssr рендеринг на сервере при первом неподключённом рендеринге; по умолчанию config :live_mercury, :ssr, то есть false
diff передача изменённых свойств патчем; по умолчанию config :live_mercury, :enable_props_diff, true
socket только определяет, является ли первый рендеринг неподключённым

Assign с потоком, созданным через Phoenix.LiveView.stream/4, передаётся как поток, а не свойство. Слот по умолчанию :inner_block передаётся компоненту как дочернее содержимое. Свойства кодируются протоколом LiveMercury.Encoder: обычные карты, списки и примитивы работают сразу, а для структуры нужно получить реализацию протокола через @derive либо написать её самостоятельно.

~H"""
<.mercury name="Card" title={@title}>
  <p>Rendered by LiveView, passed to the component as children.</p>
</.mercury>
"""

Элемент содержит всё необходимое клиенту:

  • data-name: имя компонента;
  • data-props: все свойства в компактном JSON;
  • data-props-diff и data-use-diff: патч изменённых свойств и признак того, что клиент должен его читать;
  • data-streams-diff: патч изменений потоков;
  • data-slots: HTML слота по умолчанию;
  • data-ssr: признак уже имеющейся серверной разметки;
  • phx-update="ignore" и phx-hook="MercuryHook".

Как клиент монтирует остров

Бандл страницы регистрирует компоненты по имени:

import { getHooks } from "@fiberfx/live-mercury";

const hooks = getHooks({ KanBoard, SeatsExplorer, SquadsExplorer, KanAdmin });

getHooks(components) возвращает { MercuryHook }. Когда LiveView монтирует элемент, хук читает data-name, находит components[name] и рендерит его:

  1. Свойства берутся из data-props и декодируются через decodeCompactJson ({} при отсутствии атрибута). Потоки начинаются с data-streams-diff, применённого к пустому объекту.
  2. Компонент получает единый объект свойств: сначала свойства, затем потоки, затем шесть функций хука (pushEvent, pushEventTo, handleEvent, removeHandleEvent, upload, uploadTo). HTML слота, если есть, передаётся как дочернее содержимое.
  3. Рендеринг происходит внутри LiveMercuryProvider, поэтому любой вложенный компонент может получить те же функции через useLiveMercury().
  4. При установленном data-ssr первый клиентский рендеринг очищает серверную разметку и создаёт новую. Гидратации нет.

getHooks использует карту поверх createMercuryHook(resolve). Последняя принимает функцию, которая по элементу возвращает компонент или promise компонента. Поздно загруженный компонент увидит все предшествующие обновления: патчи свойств и потоков продолжают применяться, и при первом рендеринге передаются их актуальные значения.

Вариант EDU — EchoEduWeb.Island.island/1

Страницы EDU загружают каждый остров отдельным бандлом по URL. Компонент размещения принимает три атрибута — id и bundle (оба обязательны), props (карта, по умолчанию %{}) — и слот с серверной разметкой:

~H"""
<.island id={@island_id} bundle={@bundle} props={%{title: "The demo island"}}>
  <p class="mx-text mx-text--muted">The island mounts here when JavaScript runs.</p>
</.island>
"""

Он выводит phx-hook="CourseIsland", phx-update="ignore" и data-bundle, а свойства помещает в первый дочерний <script type="application/json">, а не в атрибут. props_json/1 записывает каждый < как \u003c, поэтому значение не может закрыть элемент script. Слот остаётся внутри элемента, и страницу можно читать без JavaScript.

Бандл взаимодействует с загрузчиком через два экспорта:

import { courseIsland } from "@echd/edu-boot/island";
import { DemoIsland } from "./DemoIsland";

export default DemoIsland;

export const hook = courseIsland(DemoIsland);

Загрузчик EDU, edu-boot.js, определяет CourseIsland. В mounted он импортирует URL из data-bundle, проверяет наличие четырёх обратных вызовов у hook модуля, сохраняет серверную разметку отдельно (всё, кроме дочернего элемента свойств) и вызывает hook.mounted бандла. В destroyed он вызывает хук бандла и возвращает серверную разметку.

courseIsland(component) — это createMercuryHook с правилами EDU: один раз читает дочерний JSON, рендерит компонент внутри элемента data-island-root и передаёт функции хука, свойства и islandId — идентификатор элемента. Свойства EDU имеют приоритет перед любым одноимённым ключом Live Mercury.

Когда остров не монтируется

Два варианта сообщают об ошибках по-разному.

  • MercuryHook вызывает исключение: "the island has no data-name attribute", если атрибута нет, либо getHooks's map has no component named "<name>". Отклонённый promise компонента записывается в консоль.
  • CourseIsland записывает код в data-island-error элемента и в журнал: no-bundle (нет data-bundle), import (бандл не загрузился), no-hook (нет корректного экспорта hook), props (дочерний JSON не разбирается) либо render (mounted бандла вызвал исключение). При props и render серверная разметка возвращается.

EDU проверяет элементы островов и при сборке. EchoEduWeb.Island.check_hosts/3, используемая в mix edu.gen, требует, чтобы каждый элемент CourseIsland находился в body, имел уникальный среди элементов id, атрибут phx-update="ignore" и data-bundle с путём или URL http(s). Первый дочерний элемент должен быть script с разбираемым JSON без буквального <.

Какой вариант выбрать

LiveMercury.mercury/1 EchoEduWeb.Island.island/1
хук MercuryHook CourseIsland
поиск компонента data-name в карте getHooks бандла страницы data-bundle — URL, импортируемый загрузчиком
свойства data-props, обновляемые патчами при изменении assigns дочерний JSON, читаемый один раз при монтировании
серверная разметка ssr, по умолчанию отключён; очищается при первом клиентском рендеринге слот, сохраняемый до монтирования острова и восстанавливаемый после
в репозитории четыре представления KAN в echd_os демоостров и страницы курса KAN в html/kan, где тот же элемент записан в HTML

Используйте mercury/1, когда свойства острова следуют за assigns в LiveView. Вариант EDU подходит для самостоятельного острова на странице курса: свойства фиксируются при рендеринге страницы, а события проходят через её LiveView, как показывает следующий урок.