Skip to content

Фронтенд

Бэкенд умеет хранить и отдавать выкрики. Теперь давайте их покажем. На этой странице мы монтируем виджет на главную страницу, затем добавляем страницу в панели, где администраторы управляют выкриками. Это половина на Svelte — часть, которую посетители и администраторы действительно видят и на которую нажимают.

Полный справочник: Разработка фронтенда.

Эта половина горячая — без пересборок

В отличие от Kotlin, UI перезагружается горячо. Запустите наблюдатель один раз и оставьте его работать всё время:

sh
bun run dev

Каждое изменение ниже появляется при обновлении браузера (F5), пока включён режим разработки и ваш клон находится внутри папки plugins/ установки.

Точка входа: src/main.js

Всё начинается в main.js, который у шаблона уже есть. Он экспортирует один класс по умолчанию, наследующийся от PanoPlugin. Pano запускает его onLoad() дважды — один раз в теме (публичный сайт) и один раз в панели (панель администратора). Вы различаете их по pano.isPanel:

js
export default class ShoutboxUiPlugin extends PanoPlugin {
  onLoad() {
    const { pano } = this;

    if (pano.isPanel) {
      // panel registrations go here
    } else {
      // theme registrations go here
    }
  }
}

Два правила, которые важны везде ниже:

  • Оборачивайте каждый компонент в viewComponent(() => import('./File.svelte')). Это не опционально — это передаёт Pano рецепт загрузки вашего файла с помощью собственной копии Svelte у страницы.
  • pluginId должен в точности совпадать с идентификатором из бэкенда (pano-plugin-shoutbox). Переводы и хуки завязаны на него.

Никогда не добавляйте svelte в package.json

Ваш бандл не поставляет Svelte, svelte-i18n или @panomc/sdk — их предоставляет хост, чтобы вся страница использовала один экземпляр Svelte. Добавление собственной копии ломает гидратацию. Если ваша сборка начинает падать сразу после bun add, проверьте, нет ли случайной записи svelte, и удалите её. Почему так — смотрите в Архитектуре.

Шаг 1 — смонтируйте виджет на главной странице

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

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

Проверка

Откройте главную страницу сайта. Вы должны увидеть контейнер виджета в самом верху (осмотрите его через devtools). Он будет пустым до следующего шага — это ожидаемо. Если его нет вовсе, проверьте консоль браузера и убедитесь, что вы зарегистрировали его в ветке else с правильным pluginId.

Шаг 2 — дайте виджету его данные через load()

Виджету нужны данные, и они нужны ему в первом ответе сервера, чтобы посетители и поисковые системы видели выкрики сразу. Компонент-хук делает это, экспортируя load(event) из своего модульного скрипта — блока <script module>. Pano запускает 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>

Это вызывает публичный эндпоинт, который вы построили на странице Бэкенд. Два правила для load():

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

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

ApiUtil никогда не выбрасывает исключение при ошибках API — неудачный вызов разрешается в объект с установленным error. Проверяйте res.error перед использованием ответа; вот почему в load() выше есть запасной вариант res.shouts ?? [].

Проверка

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

Шаг 3 — страница панели для управления выкриками

Теперь сторона администратора. Когда вашему аддону нужна собственная страница — экран управления по адресу /shoutbox — зарегистрируйте её как страницу и добавьте ссылку в боковую панель, обе в ветке if (pano.isPanel):

js
pano.ui.page.register({
  path: '/shoutbox',
  component: viewComponent(() => import('./panel/ShoutboxPage.svelte')),
  permission: 'pano.plugin.pano-plugin-shoutbox.manage.shoutbox',
});

pano.ui.nav.site.editNavLinks(async (links) => {
  if (!links.some((l) => l.href === '/shoutbox')) {
    const i = links.findIndex((l) => l.href === '/posts');
    const link = {
      href: '/shoutbox',
      icon: 'fas fa-bullhorn',
      text: `plugins.${pluginId}.nav.shoutbox`,
      startsWith: true,
      permission: 'pano.plugin.pano-plugin-shoutbox.manage.shoutbox',
    };
    i >= 0 ? links.splice(i + 1, 0, link) : links.push(link);
  }
  return links;
});

Несколько вещей, о которых стоит знать:

  • nav.site — это главная боковая панель. Дополнительное слово site — не опечатка; существуют и другие области навигации.
  • Эта строка permission — набранная вручную копия узла, который выводит ваш Kotlin-класс ManageShoutboxPermission. Общей константы нет — если вы переименуете Kotlin-класс, меняйте оба места вместе, иначе барьер молча перестанет совпадать.
  • text — это ключ перевода, а не подпись. Пока вы не добавите его (следующая страница), боковая панель показывает сырой ключ plugins.pano-plugin-shoutbox.nav.shoutbox. Здесь это ожидаемо.
  • Защищайтесь от дубликатов. editNavLinks перезапускается при каждой загрузке страницы в долгоживущем сервере, поэтому проверяйте links.some(...) перед добавлением — и всегда возвращайте массив.

Внутри ShoutboxPage.svelte вы строите собственно UI управления: список выкриков, форму, которая вызывает ApiUtil.post({ path: '/api/panel/shoutbox', body: { message } }), чтобы добавить один, и кнопку удаления. Чтобы подтвердить действие, покажите toast с помощью showToast из @panomc/sdk/toasts. Полные примеры — в UI панели.

Проверка

Перезагрузите панель. Иконка рупора появляется в боковой панели прямо под Постами, подписанная сырым ключом (он превратится в настоящий текст, как только вы добавите ключ локали на следующей странице). Нажмите её, чтобы открыть свою страницу по адресу /shoutbox. Если permission не выполнено, страница отдаёт 404, а ссылка скрыта.

Более дешёвая альтернатива: раздел настроек

Если вам не нужна целая страница, вы можете вместо этого добавить компонент на страницу деталей вашего аддона (хук panel:plugin-detail:content:<pluginId>) — самый дешёвый способ дать аддону экран настроек. Большинство встроенных аддонов делают именно так; смотрите UI панели.

Остерегайтесь поддельных API

Если ИИ-ассистент или старый туториал даёт вам вызов, которого нет в Справочнике API фронтенда, значит, его не существует. Частые подделки: ApiUtil.get('/api/...') с простой строкой (каждый вызов принимает объект опций), библиотека компонентов @panomc/sdk/components/panel (её нет) и onContextUpdate (никакой хост его никогда не вызывает — удалите его, если его добавил каркас). Полный список — в конце справочника по фронтенду.

Где мы находимся

У Shoutbox теперь есть виджет на главной странице с серверно-отрисованными данными и страница панели с собственной ссылкой навигации. Но его текст всё ещё захардкоженный английский. Давайте это исправим.

Далее: Переводы →