Перейти к содержанию

Первый экран мини-приложения

Модель «Учебного плана» отделяет публичный каталог от сохранённого личного состояния. Теперь создадим первый экран. Он покажет четыре курса, позволит выбрать один и объяснит текущий результат без ложного подтверждения записи. Это полный маленький frontend без сборщика и сторонних пакетов. В реальном Telegram его предстоит открыть по отдельно подготовленному HTTPS-адресу; здесь файлы только сохраняются, браузер и бот не запускаются.

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

Документ и раннее подключение хоста

Создайте три файла: index.html, screen.css, screen.js. Официальный скрипт Telegram подключается в head раньше собственного кода, как описано в инициализации Mini Apps. Параметр ?64 соответствует опубликованному примеру на дату подготовки, а не означает фиксированную версию всего Telegram-клиента. Скрипт — внешняя зависимость; он не включён в архив и не гарантирован при отсутствии сети.

Полный index.html:

<!doctype html>
<html lang="ru">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Учебный план</title>
  <script src="https://telegram.org/js/telegram-web-app.js?64"></script>
  <link rel="stylesheet" href="screen.css">
</head>
<body>
  <main>
    <h1>Учебный план</h1>
    <p>Выберите направление для ближайших занятий.</p>
    <label for="course">Курс</label>
    <select id="course">
      <option value="javascript">JavaScript — 20 уроков</option>
      <option value="html-css">HTML и CSS — 16 уроков</option>
      <option value="markdown">Markdown — 12 уроков</option>
      <option value="performance">Производительность — 12 уроков</option>
    </select>
    <p id="selection"></p>
    <p id="status" role="status">План пока не сохранён.</p>
  </main>
  <script type="module" src="screen.js"></script>
</body>
</html>

Полный screen.css:

* { box-sizing: border-box; }
body { margin: 0; color: #202126; background: #fff; font: 1.125rem/1.6 system-ui, sans-serif; }
main { max-width: 40rem; margin: auto; padding: 1.25rem; }
label { display: block; margin-top: 1rem; }
select { width: 100%; min-height: 3rem; font: inherit; }
:focus-visible { outline: 3px solid #6330a8; outline-offset: 3px; }

Полный screen.js:

const catalog = new Map([
  ['javascript', {title: 'JavaScript', lessons: 20}],
  ['html-css', {title: 'HTML и CSS', lessons: 16}],
  ['markdown', {title: 'Markdown', lessons: 12}],
  ['performance', {title: 'Производительность', lessons: 12}]
]);
const select = document.querySelector('#course');
const selection = document.querySelector('#selection');
function renderSelection() {
  const course = catalog.get(select.value);
  selection.textContent = course ? `Выбран курс «${course.title}», ${course.lessons} уроков.` : 'Выберите доступный курс.';
}
select.addEventListener('change', renderSelection);
renderSelection();

Что уже работает в модели

JavaScript берёт объект из известного каталога по короткому ключу, не считает текст option авторитетной моделью данных. Текст выводится через textContent, поэтому случайная разметка в названии не создаёт новый HTML-элемент. Сейчас список статический и один для всех читателей. Он не доказывает, что соответствующие уроки загружены на сервер mini app, и не запрашивает библиотеку ProfessorWeb по сети. В дальнейшем публичный каталог можно вынести в собственный API с отдельным контрактом.

При первом открытии выбран JavaScript, поэтому ожидается сообщение о двадцати уроках. Выберите Markdown: ожидается двенадцать. Это изменение интерфейса, не сохранение личного плана. Строка «План пока не сохранён» остаётся правдивой после любого выбора. Она не превращается в «успешно» по событию change. Такая простая деталь защищает доверие к будущему сохранению: подтверждение будет связано с серверным ответом, а не с реакцией локального select.

Label связывается с полем через for и id. Видимая подпись остаётся после выбора значения и помогает понять назначение. Штатный select удобен для четырёх направлений и имеет привычное клавиатурное управление. Не создаём собственный выпадающий список ради оформления, потому что тогда пришлось бы повторить фокус, выбор, открытие и закрытие панели. Тема Telegram будет адаптировать цвета, а не менять семантику управления.

Размер страницы и готовность

Ширина main ограничивает длину строки, а относительный размер шрифта позволяет увеличить текст. Мы не ставим фиксированную высоту равной экрану телефона и не запрещаем масштабирование viewport. Telegram WebView меняет доступную область при движении и появлении клавиатуры; отдельный урок рассмотрит эти свойства. Пока достаточно естественного потока документа, который сохраняет элементы доступными при небольшом окне и не прячет нижнюю строку за декоративной карточкой.

Метод ready() будем вызывать после подключения адаптера в следующем шаге. Он сообщает Telegram о готовности интерфейса, но не проверяет нашу разметку и не подтверждает серверную сессию. Сначала маленький экран может подготовить статическое содержимое без API. Если ждать личный план бесконечно до сообщения ready, пользователь будет видеть загрузку вместо доступного каталога. Разделение готовности оболочки и готовности данных понадобится при сетевых отказах.

В документации каждого шага будем отличать полный frontend от подключаемого модуля и серверного договора. Эти три файла уже образуют полный первый экран. Следующие главы добавят самостоятельные модули с явными параметрами; их наличие в архиве не означает, что все функции одновременно включены в этот экран. Например, проверка initData принадлежит серверу и никогда не импортируется браузерным module. Такая карта зависимостей важна, чтобы читатель не попытался разместить файл с bot token рядом с публичным CSS.

Для небольшого интерфейса не нужен сборщик лишь ради самой страницы. Когда появляются зависимости, TypeScript или сложное разделение ресурсов, решение о сборке можно принять отдельно. Сейчас браузер выполняет обычный ES module, а архив не содержит сгенерированный bundle или скрыто установленный пакет. Это не заявление о лучшей архитектуре любого mini app: простое начало помогает увидеть границы хоста, сетевого запроса и пользовательского состояния до добавления инструментов.

Отказ внешнего скрипта

Если SDK не загрузился, текущий screen.js не обращается к отсутствующему Telegram.WebApp, поэтому выбор публичного курса должен оставаться доступным. Это ожидаемая граница, не выполненный offline-тест. Позднее будем определять хост через небольшой адаптер и выбирать запасное поведение. Нельзя сделать вывод «мы внутри Telegram» только потому, что адрес содержит похожий query-параметр или глобальный объект определён внешним скриптом. Серверное доверие начинается совсем в другом месте.

Откройте подготовленные файлы на самостоятельном учебном HTTPS-сайте, если хотите пройти сценарий. В этой задаче такое размещение и открытие не выполнялись. Прямое открытие file может показывать текст, но не подтверждает работу модулей, сетевой политики и WebView. Бот с Main Mini App также ещё не настроен. Запишите отдельно результат обычного браузера и Telegram, не выдавая один за доказательство другого.

Ожидаемый результат урока — четыре понятных направления, один выбранный курс и честное сообщение о несохранённом плане. Для ошибки вручную задайте в состоянии неизвестный ключ: функция должна показать просьбу выбрать доступный курс, а не обращаться к свойствам undefined. После проверки верните известное значение. Следующий урок создаст адаптер SDK и определит, какие функции окружения действительно доступны этому первому экрану.

После выбора другого курса меняется только пояснение о числе уроков. Статус продолжает говорить, что личный план не сохранён. Это намеренный результат: мы показали чтение каталога и событие select, а не имитацию серверной записи. Если в отдельном браузере название меняется, такой результат ещё не доказывает работу внутри Telegram, доступность клавиатурой или подтверждённую личность. Каждый следующий механизм добавляет собственную границу и не должен задним числом делать этот первый экран «авторизованным».

Оглавление серии