Остров — элемент серверной страницы, содержимым которого управляет клиентский компонент. Урок разбирает этот элемент: что записывает сервер, что читает клиент и что происходит при несовпадении их ожиданий.
Что такое остров
Элемент становится островом благодаря трём вещам:
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] и рендерит его:
- Свойства берутся из
data-propsи декодируются черезdecodeCompactJson({}при отсутствии атрибута). Потоки начинаются сdata-streams-diff, применённого к пустому объекту. - Компонент получает единый объект свойств: сначала свойства, затем потоки, затем шесть функций хука (
pushEvent,pushEventTo,handleEvent,removeHandleEvent,upload,uploadTo). HTML слота, если есть, передаётся как дочернее содержимое. - Рендеринг происходит внутри
LiveMercuryProvider, поэтому любой вложенный компонент может получить те же функции черезuseLiveMercury(). - При установленном
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, как показывает следующий урок.