Фронтенд
Бэкенд умеет хранить и отдавать выкрики. Теперь давайте их покажем. На этой странице мы монтируем виджет на главную страницу, затем добавляем страницу в панели, где администраторы управляют выкриками. Это половина на Svelte — часть, которую посетители и администраторы действительно видят и на которую нажимают.
Полный справочник: Разработка фронтенда.
Эта половина горячая — без пересборок
В отличие от Kotlin, UI перезагружается горячо. Запустите наблюдатель один раз и оставьте его работать всё время:
bun run devКаждое изменение ниже появляется при обновлении браузера (F5), пока включён режим разработки и ваш клон находится внутри папки plugins/ установки.
Точка входа: src/main.js
Всё начинается в main.js, который у шаблона уже есть. Он экспортирует один класс по умолчанию, наследующийся от PanoPlugin. Pano запускает его onLoad() дважды — один раз в теме (публичный сайт) и один раз в панели (панель администратора). Вы различаете их по pano.isPanel:
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 (тема):
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(), пока страница готовится, и передаёт то, что вы вернули, компоненту как пропсы:
<!-- 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):
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 теперь есть виджет на главной странице с серверно-отрисованными данными и страница панели с собственной ссылкой навигации. Но его текст всё ещё захардкоженный английский. Давайте это исправим.
Далее: Переводы →