Skip to content

Разработка фронтенда

Что даёт вам эта страница: единственную точку входа, общую для фронтенда каждого дополнения — src/main.js — плюс карту к двум тематическим страницам, которые строят сам UI.

У вашего дополнения две половины: бэкенд на Kotlin и фронтенд на Svelte (визуальная часть, которую пользователи видят и по которой кликают). При сборке дополнения скомпилированный бэкенд Kotlin и ваши собранные файлы Svelte архивируются вместе в один .jar-файл — этот целый файл и есть ваше дополнение. Вы не делаете для этого ничего особого; сборка справляется сама.

Pano запускает два отдельных сайта: тему (то, что посетители видят на yoursite.com) и панель (админ-панель на yoursite.com/panel). Ваше одно дополнение добавляет UI на оба. Один файл — src/main.js — точка входа для обоих.

Сквозной пример на этих страницах — Shoutbox: небольшой виджет недавних «выкриков» вверху главной страницы плюс панель, где админы ими управляют. Эта страница связывает main.js; две тематические страницы — UI темы и UI панели — строят сами экраны (смотрите Куда дальше).

Если ИИ-ассистент или старое руководство даёт вам API, которого нет на этих страницах или в Справочнике API фронтенда, значит его не существует. Частые вымышленные или удалённые вызовы перечислены внизу этой страницы.

Новичок в Svelte?

UI дополнений пишутся на Svelte, так же как темы Pano. Если вы никогда его не использовали, интерактивное руководство Svelte охватывает всё, что нужно UI дополнения.

Прежде чем начать

У вас должно быть дополнение, созданное из шаблона pano-boilerplate-plugin, и завершённая Разработка бэкенда. Файл src/main.js уже существует в boilerplate — вы будете его редактировать, а не создавать. Запустите цикл разработки командой bun run dev и держите его запущенным всё время; каждое изменение на этих страницах перезагружается горячо, так что вы можете сразу видеть результат. (Превращение готового дополнения в релиз рассмотрено в Сборка и публикация.)

Файлы вашего дополнения

Вот раскладка, на которую ссылаются эти страницы. Папки theme/ и panel/ — просто аккуратное соглашение для разделения компонентов посетителей и компонентов админов — Pano не принуждает к именам, вы могли бы организовать файлы как угодно.

text
src/
├─ main.js                    ← entry point; Pano loads this first
├─ theme/                     ← components shown to visitors (the public site)
│   └─ ShoutboxWidget.svelte
└─ panel/                     ← components shown to admins (the dashboard)
    ├─ ShoutboxSettings.svelte
    └─ ShoutboxPage.svelte

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

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

Он вызывает onLoad() дважды — один раз в панели и один раз в теме. Тема и панель — две отдельные работающие программы, и каждая загружает ваше дополнение независимо, так что onLoad() действительно выполняется дважды, по одному в каждой. Это нормально, а не баг. Проверка pano.isPanel — это то, как вы даёте каждой стороне разный UI: он true в панели и false в теме.

Вот скелет, на котором строится всё дополнение. Одна строка в нём — export const _ = ... — выглядит пугающе; пока скопируйте её как есть, это помощник перевода, полностью объяснённый в разделе Перевод текста ниже, и вам не нужно понимать его, чтобы использовать скелет.

js
// src/main.js
import { PanoPlugin, viewComponent } from '@panomc/sdk';
import ApiUtil from '@panomc/sdk/utils/api';
import { derived } from 'svelte/store';
import { _ as i18n } from '@panomc/sdk/utils/language';

export const pluginId = 'pano-plugin-shoutbox';

// A translate function scoped to your addon's keys — see the i18n section below.
export const _ = derived(i18n, ($t) => (key, options) => $t(`plugins.${pluginId}.${key}`, options));

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

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

  onUnload() {}
}

Несколько важных правил, все из которых соблюдают встроенные дополнения (работающие эталонные дополнения в организации PanoMC на GitHub):

  • Класс должен быть экспортом default. Pano ищет ровно один.
  • this.pano — весь ваш API. Pano устанавливает его за вас до выполнения onLoad() — внутри onLoad() вы просто читаете pano.isPanel и используете pano.ui.*.
  • pluginId должен точно совпадать с ID вашего дополнения из Разработки бэкенда / вашего манифеста плагина. Переводы и хук страницы деталей панели привязаны к нему, так что несовпадение незаметно их ломает.
  • Оборачивайте каждый компонент в viewComponent(() => import('./File.svelte')). Это не опционально. Каждый компонент, который вы передаёте вызову register — хуки, страницы, слоты представлений — должен быть обёрнут так. Простыми словами: это передаёт Pano рецепт загрузки вашего файла вместо самого файла, чтобы Pano мог загрузить его в нужный момент с собственной копией Svelte страницы. Детали вам не нужны — смотрите Архитектуру, если любопытно.
  • Держите общее состояние в main.js. Переменные, которые вы объявляете на верхнем уровне main.js (как хранилище _ выше), существуют ровно один раз и разделяются всеми вашими компонентами. Сборка гарантирует это только для main.js, так что не полагайтесь на другие файлы для общего состояния. (Механика в Архитектуре.)
  • onUnload() выполняется при отключении вашего дополнения. Пока оставьте его пустым; большинству дополнений он никогда не нужен.

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

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

Проверьте прогресс

Этот скелет сам по себе пока ничего не рисует на экране — он просто настраивает ваше дополнение. С запущенным bun run dev перезагрузите и yoursite.com (тему), и yoursite.com/panel (панель) и откройте консоль браузера (F12). Вы должны увидеть никаких красных ошибок от вашего дополнения. Если увидите ошибку об отсутствующем экспорте default или плохом pluginId, исправьте это, прежде чем идти дальше — всё, что ниже, надстраивается на этом работающем скелете.

Теперь заполните две ветки. То, что идёт в каждую, имеет свою страницу: UI темы для ветки else (тема), UI панели для ветки if (pano.isPanel). Остальная часть этой страницы охватывает две вещи, которые используют обе ветки.

Перевод текста в ваших компонентах

В отличие от разработки темы, никакой помощник перевода не внедряется в компоненты вашего дополнения автоматически — вы должны импортировать _ из вашего собственного main.js. Этот _ — пугающая строка из скелета:

js
export const _ = derived(i18n, ($t) => (key, options) => $t(`plugins.${pluginId}.${key}`, options));

Вот всё, что она делает: это Svelte derived хранилище (значение, которое пересчитывается при изменении его источника), оборачивающее функцию перевода Pano, и оно автоматически добавляет к каждому переданному ключу префикс plugins.<pluginId>.. Так что в компоненте вы пишете $_('widget.title'), и оно ищет plugins.pano-plugin-shoutbox.widget.title. Вам не нужно понимать, как оно устроено внутри, чтобы использовать его — просто импортируйте и читайте с префиксом $:

svelte
<script>
  import { _ } from '../main.js';
</script>

<h2>{$_('widget.title')}</h2>

{$_('widget.title')} разрешается в ключ plugins.pano-plugin-shoutbox.widget.title в ваших файлах локализации. Смотрите Локализацию, где эти ключи живут и как они разбиты по пространствам имён.

Проверьте прогресс

Пока вы реально не добавите этот ключ в Локализации, экран показывает сырую строку ключа (plugins.pano-plugin-shoutbox.widget.title) вместо приятной надписи. На этом этапе так и должно быть — это ваш знак, что помощник подключён правильно и просто ждёт файл локализации.

Старые и выдуманные ИИ API, которых не существует

Если ИИ-инструмент, старое руководство или каркас предлагают что-либо из этого, игнорируйте — ничего из этого не существует. Используйте только то, что есть на этих страницах и в Справочнике API фронтенда.

  • pano.ui.page.register({ name, view, scopes }) — реальный page.register принимает { path, component, permission, ... } (смотрите UI панели). Формы name/view/scopes не существует.
  • import { Button, Card } from '@panomc/sdk/components/panel' — в SDK нет такой библиотеки компонентов.
  • onContextUpdate — более старый boilerplate определяет этот метод, но ни один хост его не вызывает. Если ваш каркасный main.js содержит onContextUpdate, удалите его.
  • ApiUtil.get('/api/...') с обычной строкой — каждый вызов ApiUtil принимает объект опций, например ApiUtil.get({ path: '/api/...' }).
  • pano.utils.toast — такого нет; тосты приходят только из @panomc/sdk/toasts.

Куда дальше

main.js настроен. Теперь стройте UI:

  • Показать что-то на сайте → UI темы — хуки темы, виджет главной страницы, данные с отрисовкой на сервере и вызов вашего API.
  • Добавить админские экраны → UI панели — разделы настроек, полноценные страницы панели со ссылками навигации и тосты.
  • Полный справочник → Справочник API фронтенда — каждое имя хука, слот представления, событие жизненного цикла и экспорт @panomc/sdk в одном месте.
  • Локализация — где живут ваши строки plugins.<pluginId>.<key> и как панель позволяет админам их переопределять.
  • Разработка бэкенда — Kotlin-сторона эндпоинтов и разрешений, которые вызывают эти страницы.
  • Сборка и публикация — превратите готовое дополнение в релизный jar. Релизная сборка обязана включать UI, так что никогда не используйте для неё -Pnoui (-Pnoui — флаг Gradle, который пропускает сборку UI при итерации только над бэкендом — смотрите «Сборка и публикация»).
  • Эталонные дополнения — встроенные дополнения в организации PanoMC на GitHub — работающий эталон для каждого паттерна на этих страницах.