Skip to content

UI темы ​

Что даёт вам эта страница: всё для обращённой к посетителю стороны вашего аддона — смонтируйте компонент в тему, дайте ему серверно-отрисованные данные и вызовите свой бэкенд. К концу виджет Shoutbox показывает недавние выкрики на главной странице.

Всё здесь идёт в ветку else (тема) функции onLoad() в main.js. Если вы ещё не настроили main.js, сначала прочитайте Разработку фронтенда.

Смонтируйте виджет на главной странице ​

Тема предоставляет именованные хуки — места, куда аддоны могут внедрить компонент (полный список имён хуков — в Справочнике API фронтенда; это руководство использует page:home:top). Чтобы показать Shoutbox на главной странице, зарегистрируйте компонент для хука page:home:top, в ветке else (тема):

js
pano.ui.hook.register({
  name: 'page:home:top',
  component: viewComponent(() => import('./theme/ShoutboxWidget.svelte')),
});

Проверка

При запущенных dev-серверах откройте главную страницу сайта. Вы должны увидеть контейнер <div class="shoutbox"> в самом верху страницы (осмотрите его через devtools браузера). Если вы ещё не добавили load() — следующий шаг — он будет пустым; так и должно быть. Если вы его вообще не видите, проверьте консоль браузера на ошибки и убедитесь в правильности pluginId и что вы зарегистрировали в ветке else.

Дайте виджету серверно-отрисованные данные через load() ​

Хорошему виджету нужны данные, и нужно, чтобы эти данные были там в первом ответе сервера — а не подгружались позже в браузере — чтобы посетители и поисковые системы видели выкрики сразу. Pano делает это через SSR (серверный рендеринг — сервер строит готовый HTML перед отправкой, вместо того чтобы отправить пустую страницу, которая подгружает свои данные потом).

Компонент хука может экспортировать функцию load(event) из своего модульного скрипта — блока <script module>. Этот блок выполняется один раз, когда файл впервые загружается, до того как существует хоть один экземпляр компонента, — вот почему load() живёт там, а ваш обычный код на экземпляр компонента — в обычном <script> под ним. Тема выполняет load(), пока страница готовится (на сервере во время SSR и снова на клиенте при навигации между страницами), и передаёт всё, что вы вернёте, компоненту как props. В Shoutbox load() вызывает наш публичный эндпоинт:

svelte
<!-- src/theme/ShoutboxWidget.svelte -->
<script module>
  import ApiUtil from '@panomc/sdk/utils/api';

  export async function load(event) {
    const res = await ApiUtil.get({ path: '/api/shoutbox/list', request: event });
    return { shouts: res.shouts ?? [] };
  }
</script>

<script>
  export let shouts = [];
</script>

<div class="shoutbox">
  {#each shouts as shout}
    <p class="shout">{shout.message}</p>
  {/each}
</div>

event — это входящий запрос страницы — он несёт куки и сессию посетителя. В основном вы просто перенаправляете его в ApiUtil (как request: event), чтобы API знал, кто спрашивает.

Объект, который вы возвращаете, становится props компонента — здесь shouts приходит готовым к отрисовке. (Хост называет этот поток props hookProps; вы встретите это имя в справочнике API и в сообщениях об ошибках.)

load() выполняется на сервере и на клиенте

Один и тот же load() выполняется во время SSR и снова при клиентской навигации, поэтому держите его безопасным для двойного запуска: он должен только получать и возвращать данные. Не меняйте глобальные переменные, ничего не записывайте и не изменяйте объекты, которые вам передали, — потому что одна и та же функция выполняется один раз на сервере и снова в браузере. (Слово из одного термина для «безопасно запускать дважды без побочных эффектов» — идемпотентный.) Всегда передавайте request: event в ApiUtil (следующий раздел), чтобы серверный вызов нёс сессию посетителя.

Проверка

Обновите главную страницу. <div class="shoutbox"> теперь должен содержать по одному <p class="shout"> на выкрик (при условии, что у вашего бэкенда они есть). Чтобы подтвердить, что данные действительно в первом ответе, сделайте жёсткое обновление (Ctrl/Cmd+Shift+R) и используйте «Просмотр исходного кода» — вы должны увидеть выкрики уже присутствующими в HTML, а не пустоту.

Скрывайте виджет, когда ему нечего показать

Если load() возвращает { hookOptions: { invisible: true } }, хост ничего не отрисовывает для этого хука. Аддон Announcement использует это, чтобы исчезать, когда нечего отображать.

Вызов вашего API ​

Все сетевые вызовы идут через ApiUtil. Импортируйте экспорт по умолчанию и используйте методы-глаголы, каждый из которых принимает один объект опций:

js
import ApiUtil from '@panomc/sdk/utils/api';

// In a load() — pass request so the server-side call has the session:
const res = await ApiUtil.get({ path: '/api/shoutbox/list', request: event });

// In a browser event handler — body is your JSON payload:
await ApiUtil.post({ path: '/api/panel/shoutbox', body: { message } });
await ApiUtil.delete({ path: `/api/panel/shoutbox/${id}` });
await ApiUtil.put({ path: '/api/panel/shoutbox/config', body: config });

Правило: внутри load() всегда передавайте request: event, чтобы запрос выполнялся с сессией посетителя во время SSR. В обработчике клика, выполняющемся в браузере, вы можете его опустить.

Если вы забудете request: event

Вызов всё равно работает в браузере, но во время SSR он выполняется вышедшим из системы. Симптом сбивает с толку: данные отсутствуют или вы получаете ошибки прав доступа только при жёстком обновлении, тогда как при кликах по сайту всё выглядит нормально. Если вы когда-нибудь это увидите, сначала проверьте свои вызовы load().

Как ApiUtil сообщает об ошибках

ApiUtil никогда не бросает исключение на ошибках API — проваленный вызов разрешается в объект с установленным error (он не бросает, и вы не проверяете HTTP-статус). Всегда проверяйте res.error перед использованием ответа; вы увидите это в каждом примере.

Продвинутое — пропустите при первом чтении ​

Всё ниже — для случаев, с которыми вы не столкнётесь на своём первом аддоне. Вернитесь, когда понадобится.

Получите общие данные один раз через pano.ui.app.onLoad ​

pano.ui.app.onLoad(callback) регистрирует функцию, которую тема выполняет при каждом запросе страницы, до её отрисовки. Её колбэк получает (data, event), где data — общий мешок данных страницы, а event — запрос (того же вида, что вы передаёте в ApiUtil). Используйте его, когда один запрос должен питать несколько регистраций сразу.

Это альтернатива load() на каждый компонент: зарегистрируйте хук с skipLoad: true и получайте его данные из единственного pano.ui.app.onLoad(async (data, event) => { ... }). Аддоны FAQ и Pages используют это, когда один запрос питает несколько регистраций.

Динамические страницы и очистка ​

Иногда страницы, которые вы регистрируете, неизвестны на этапе сборки — они приходят из вашего бэкенда (пользовательские URL, редиректы и подобное). Регистрируйте их изнутри pano.ui.app.onLoad, после получения списка. Аддон Pages делает именно это.

Сначала ментальная модель, потому что этот раздел зависит от неё: тема — это одна долгоживущая серверная программа, общая для каждого посетителя. Всё, что вы регистрируете, запоминается этой программой до её перезапуска — это не на каждого посетителя и не на каждую загрузку страницы. Так что если ваши данные меняются, старые регистрации остаются, пока вы их не удалите.

Вот здесь острый угол. pano.ui.app.onLoad выполняется при каждом запросе, но маршруты и ссылки, которые вы регистрируете, сохраняются в этом процессе между запросами. Если страница удалена в панели, запись, которую вы зарегистрировали ранее, задерживается — SSR всё ещё обслуживает призрачный маршрут, пока процесс не перезапустится, даже если браузер о нём больше не знает.

Исправление — отслеживать то, что вы зарегистрировали, и удалять записи, которых больше нет, через pano.ui.page.unregister(path):

js
const registeredPaths = new Set();
const customPageComponent = viewComponent(() => import('./theme/CustomPage.svelte'));

pano.ui.app.onLoad(async (data, event) => {
  const res = await ApiUtil.get({ path: '/api/pages', request: event });
  const incoming = new Set(res.pages.map((p) => p.url));

  // Remove routes we registered before that are no longer present.
  for (const path of registeredPaths) {
    if (!incoming.has(path)) {
      pano.ui.page.unregister(path);
      registeredPaths.delete(path);
    }
  }

  for (const page of res.pages) {
    pano.ui.page.register({ path: page.url, component: customPageComponent });
    registeredPaths.add(page.url);
  }
});

Повторная регистрация страницы через pano.ui.page.register безопасна: тот же путь просто перезаписывает предыдущую запись, поэтому защита от дубликатов здесь не нужна — в отличие от ссылок навигации, которые дублировались бы, из-за чего editNavLinks нужна его проверка some(...).

Призрачные маршруты в SSR

Динамические страницы, зарегистрированные из данных, выживают в процессе Node (SSR — серверный рендеринг — выполняется в одной долгоживущей программе). Если вы никогда не снимаете с регистрации удалённые элементы, удалённые страницы продолжают обслуживаться во время SSR, пока Pano не перезапустится. Аддон pano-plugin-link-redirects — полный образец для этого паттерна очистки, включая удаление устаревших ссылок навигации, которые вы добавили.

Что дальше ​

Сторона посетителя готова: виджет на главной странице с SSR-данными, вызовы API и (опционально) динамические страницы.

  • Добавьте административные экраны → UI панели — разделы настроек, полноценные страницы панели со ссылками навигации и toast-ы.
  • Справочник API фронтенда — каждое имя хука, слот представления и событие жизненного цикла в одном месте.
  • Перевод текста — помощник $_, который ваши компоненты используют для ярлыков.
  • Разработка бэкенда — эндпоинты Kotlin, в которые попадают ваши вызовы load() и ApiUtil.