Skip to content

Latest commit

 

History

History
390 lines (265 loc) · 37.9 KB

File metadata and controls

390 lines (265 loc) · 37.9 KB

Демо Telegram Mini App

В этом репозитории представлен пример разработки мини-приложения "Магазин мини приложений" для платформы Telegram. Живая тестовая версия приложения доступна по этой ссылке.

Основные функции:

  • просмотр приложений по категориям
  • добавление приложений в избранное
  • выставление оценки
  • запуск приложения прямо из магазина

Технический стэк

В примере используются следующий технический стэк: HTML, JS, CSS, Kotlin, Ktor, Heroku.

Для комфортного изучения примера необходимы базовые знания вэб разработки. Достаточно основ владения HTML, JS и CSS. Для понимания работы серверной части желательно владение языком Kotlin на базовом уровне.

Использование

Данный репозиторий можно использовать как отправную точку при создании своих Telegram Mini App. Исходный код клиента и сервера представляют собой минималистичный набор методов, достатчных для понимания основ разработки собственных приложений. Многие вещи сознательно упрощены, чтобы не перегружать тестовый пример излишними деталями. В то же время сделан акцент на важных нюансах, которые стоит учитывать при разработке собственного приложения.

Быстрый запуск

Для начала подготовим свою версию проекта и запустим собственную версию бота.

Форк репозитория

Чтобы была возможность вносить свои изменения и получать обновления создайте форк текущего репозитория.

Telegram бот

Создайте свой бот в Telegram. Если вы никогда до этого не имели дело с ботами в Telegram, воспользуйтесь одной из инструкцией, например, этой из официальной документации.

На данном этапе нам достаточно дойти до получения токена, сам код для бота писать не нужно, однако вы всегда можете добавить некоторую функциональность в классический Telegram бот позже.

Проверка доступности index.html

В отличии от классического Telegram бота, которому важно наличие backend, для работы Telegram Mini App в минимальном варианте достаточно только наличия вэб страницы. Самым простым и бесплатным способом хостинга статических HTML страниц является сам GitHub. Для этого мы будем использовать GitHub Pages разместив в нем содержимое нашего каталога webapp. GitHub Pages разрешает размещать такой контент в 2-х местах (на момент написания инструкции): корневая дирректория /(root) или дирректория /docs.

Для удобства разделения различных частей проекта наш контент размещается в дирректории /webapp. Чтобы он стал доступен для отображения в GitHub Pages, в репозитории используется плагин GitHub Pages Overwriter. Убедитесь, что он работает корректно во вкладке Actions вашего репозитория.

Если Action отрабатывает с ошибкой, убедитесь, что для Actions включен режим Read and write permissions. Проверить это можно в настройках репозитория по следующему пути: Settings -> Actions (Code and automation) -> General -> Workflow permissions. Должно быть выбрано Read and write permissions.

Если публикация прошла успешно, по адресу http://username.github.io/repository (в текущем репозитории это http://landarskiy.github.io/tg-app-store) откроется HTML страница и отобразится контент нашего магазина.

Если по каким-либо причинам после успешного выполнения Action содержимое папки webapp не стало доступно по ссылке GitHub Pages, сверьтесь с инструкцией плагина GitHub Pages Overwriter. Обратите внимание, правильно ли вы настроили Pages в вашем репозитории, запускается ли Action при пуше изменений, существует ли ветка, на которую настроен плагин. В крайнем случае рассмотрите вариант перемещение сожержимого из дирректории /webapp в дирректорию /docs, как того требует GitHub Pages, в этом случае вам не понадобится пользоваться указанным выше плагином.

Подключение Mini App к боту

Следуя официальной инструкции добавьте в ранее созданный бот путь к GitHub Pages странице репозитория.

Если всё выполнено верно, можно зайти в ваш бот и открыть в нём приложение магазина через кнопку меню.

Сейчас ваш бот берет данные с сервера основного репозитория. Это значит, что есть вероятность того, что вы не увидите сам контент приложения (имеется ввиду наличие приложений в магазине). Данная ситуация может возникнуть по разным причинам: сервер прекратил работу, временно недоступен и т.д. Однако в любом случае, вне зависимости от доступности сервера источника данных, страница должа быть загружена и корректно отобразиться, пусть и без наполнения. Далее мы рассмотрим то, каким образом можно обойтись на начальном этапе без сервера, сейчас не будем заострять на этом внимание.

Так же вы можете заметить, что некоторые функции, такие как добавление в избранное и проставление оценок работают некорректно. Это происходит из-за того, что на стороне сервера происходит валидация клиента. Данный аспект также будет рассмотрен немного позже.

Дополнительная информации

Как видно из инструкции, достаточно всего 3-х простых шагов для того, чтобы можно было начать разрабатывать Telegram Mini App. Для этого даже не обязательно писать серверную часть. Например, если вы делаете простое приложение вроде калькулятора, или простую игру, не требующую взаимодействия с сервером, вам не нужно валидировать пользовательские данные и вам достаточно возможностей чистого HTML, JS и CSS, то предыдущих шагов хватит, чтобы ваше приложение успешно функционировало в среде Telegram.

Подготовка окружения

Для локального запуска backend части необходимо установить:

  • Java 17 или выше

Рекомендуемые IDE для работы (вы можете выбрать другие, более привычные для вас):

Сервис для развертывания backend (опционально):

  • В примере используется Heroku, но подойдёт любая другая аналогичная платформа для развертывания Java приложений

Вы можете запускать серверную часть локально, в этом случае отладку и разработку можно вести в обычном браузере. Стоит учитывать, что в таком режиме ни одно свойство из telegram-web-app.js не будет доступно, а методы не будут работать согласно документации. Если вы планируете обращаться к локальному серверу через бота - убедитесь, что сервер будет доступен вне локальной сети. Данная настройка выходит за рамки текущего примера.

Структура репозитория

Репозиторий содержит 2 основные части: frontend и backend. Структура выглдяит следующим образом:

tg-app-store
├─ backend
│  ├─ src
│  │  ├─ ...
│  │  ... 
│  ├─ build.gradle.kts
│  ...
├─ webapp
│  ├─ index.html
│  ├─ css
│  │  ├─ ...
│  │  ...
│  ├─ stub
│  │  ├─ ...
│  │  ...
│  ...
└─ README.md

Где webapp это минималистичный вэб-проект (HTML, CSS, JS) без лишних зависимостей, а backend - Kotlin Gradle проект, представляющий собой Ktor сервер.

Работа с проектом

С приложением можно работать в 3-х условных режимах:

  • HTML-only песочница, без backend. Данный режим позволяет проверять frontend часть как локально в обычном браузере, так и через приложение Telegram. С помощью этого метода невозможно корректно валидировать пользовательские данные, при добавлении новой функциональности на backend так же необходимо реализовать локальные заглушки в js файлах.
  • песочница с локальным backend-ом. Без дополнительной настройки backend позволяет запускать приложение только локально в обычном браузере, проверка в Telegram требует дополнительную настройку доступности сервера из вне.
  • production версия. Как и первый режим позволяет проверять frontend часть как локально в обычном браузере, так и через приложение Telegram. Однако, в отличии от локальной песочницы, этот режим позволяет безопасно проводить валидацию пользовательских данных.

HTML-only песочница

Данный режим работы с проектом предназначем в первую очередь для вёрстки приложения и тестирования корректности отработки базовых сценариев.

Идея использования этого режима заключается в том, что мы вместо реальных вызовов методов нашего backend-а используем вызовы локальных функций, которые в упрощённом виде обрабатывают данные и эмулируют ответ реального сервера.

Для того, что в нашем примере воспользоваться этим режимом, необходимо раскоментировать одну строку в файле index.html, в итоге он должен выглядеть слебующим образом:

    ...
    <script src="data-repository.js"></script>
    <!-- Uncomment mock-data-repository.js when you would like to test the app in a local sandbox mode, without real requests to the server -->
    <script src="stub/mock-data-repository.js"></script>
    <script src="css/css-class-names.js"></script>
    ...

Наше мини-приложение общается с сервером через специальные функции-делегаты, которые определены в другом файле data-repository.js. Например, определение метода обращения за списком приложений выглядит следующим образом:

let loadAppListDelegate = function loadAppList(userId, categoryId, initData, successCallback, failCallback) {
    const params = { category_id: categoryId };
    if (userId) {
        params.user_id = userId;
    }
    const query = new URLSearchParams(params);
    fetch(`${configuration.serverUrl}/app/list?${query.toString()}`, buildInitDataPostParams(initData)).then(response => {
        if (!response.ok) {
            throw new Error('Error occured');
        }
        return response.json();
    }).then(data => {
        successCallback(data);
    }).catch(error => {
        failCallback(error);
    });
}

В самом приложении мы вызываем метод с помощью переменной loadAppListDelegate:

function loadApps(category) {
    loadAppListDelegate(
        window.Telegram.WebApp.initDataUnsafe?.user?.id,
        category,
        initDataProviderDelegate(),
        data => {
            displayApps(data);
        },
        error => { }
    );
}

При подключении файла mock-data-repository.js эта переменная перезапишется и вместо вызова функции выше будет вызвана другая функция, переопределенная в mock-data-repository.js:

loadAppListDelegate = function loadAppList(userId, categoryId, initData, successCallback, failCallback) {
    let returnList = [];
    for (app of stubApps) {
        let userApp = { ...app };
        userApp.fav = stubUserFavorites[app.id];
        returnList.push(userApp);
    }
    successCallback(returnList);
}

Такая архитектура позволяет быстро разрабатывать прототипы фокусируясь в первую очередь на то, как выглядит приложение. У данного подхода есть ряд минусов: необходимо писать дополнительный код на каждый запрос для сервера, нет возможности валидации пользовательских данных.

Локальный backend

Запуск локального backend наиболее предпочтителен при активной разработке приложения. Запустить локальный backend можно двумя способами.

Запуск из командной строки

Самым простым способом является запуск через командную строку из дирректории backend. Перед тем, как запустить сервер, нам нужно установить переменную окружения TELEGRAM_BOT_TOKEN передав туда токен нашего бота, который мы получили при его создании. Это необходимо для корректной валидации данных, которые будут приходить к нам на сервер. Для установки переменной нужно в терминале выполнить следующую команду (macOS, linux):

export TELEGRAM_BOT_TOKEN="your token here"

После установки токена к переменную окружения нужно запустить сам сервер, выполнив в терминале следующую команду (macOS, linux):

./gradlew runFatJar

Если всё выполнено верно, в консоли отобразится стартовые лог с информацией об инициализации:

[main] INFO  Application - Autoreload is disabled because the development mode is off.
[main] DEBUG Application - Repository initialization started
[main] DEBUG Application - Repository initialization finished
[main] INFO  Application - Telegram bot token loaded, hash: 1979904964
[DefaultDispatcher-worker-1] TRACE i.ktor.client.plugins.HttpPlainText - Adding Accept-Charset=UTF-8 to https://api.ipify.org
[main] INFO  Application - Application started in 0.379 seconds.
[DefaultDispatcher-worker-4] INFO  Application - Responding at http://0.0.0.0:8080

В этом логе нас интересует 2 значения. Убедитесь, что Telegram bot token loaded, hash: не равен 0. Если там 0, проверьте ещё раз команду установки переменной среды. Второе значение - локальный адрес нашего сервера, он располагается после строки Application - Responding at. В приведённом выше логе адресом нашего сервера является http://0.0.0.0:8080.

Запуск из IDE

Второй способ требует наличия Intellij Idea Community Edition. Для начала необходимо открыть проект backend. В первый раз это нужно сделать из меню IDE File->Open... и выбрать файл build.gradle.kts. При первом открытии IDE сгенерирует папку .idea которая будет указывать IDE, что текущая дирректория является проектом и в дальнейшем можно будет открывать сразу папку, а не файл build.gradle.kts.

Необходимо дождаться окончания синхронизации проекта и открыть в IDE файл Application.kt находящийся по пути src/main/kotlin/io/github/landarskiy/Application.kt. Слева от метода fun main(args: Array<String>) будет зеленая стрелочка, которую необходимо нажать, чтобы запустить проект прямо в IDE.

При первом запуске в локах возле Telegram bot token будет значение 0. Это из-за того, что мы не установили переменную окружения. Следуя этой инструкции добавьте значение для переменной TELEGRAM_BOT_TOKEN и перезапустите сервер, теперь значение должно быть отличным от 0.

Обновление frontend

Для того, чтобы наше приложение отправляло запрос именно на наш сервер, откройте файл /webapp/config.json и замените serverUrl на адрес локального сервера. После изменений файл config.json должен выглядеть следующим образом:

const configuration = {
    serverUrl: "http://0.0.0.0:8080"
}

Теперь откройте /webapp/index.html в своём браузере, вы должны увидеть страницу приложения, а влогах сервера должны отобразиться запросы за контентом следующего вида:

[eventLoopGroupProxy-4-1] TRACE io.ktor.routing.Routing - Trace for [app, list]
/, segment:0 -> SUCCESS @ /
  /app, segment:1 -> SUCCESS @ /app
    /app/list, segment:2 -> SUCCESS @ /app/list
      /app/list/(method:POST), segment:2 -> SUCCESS @ /app/list/(method:POST)
    /app/details, segment:1 -> FAILURE "Selector didn't match" @ /app/details
    /app/rating, segment:1 -> FAILURE "Selector didn't match" @ /app/rating
  /user, segment:0 -> FAILURE "Selector didn't match" @ /user
Matched routes:
  "" -> "app" -> "list" -> "(method:POST)"
Route resolve result:
  SUCCESS @ /app/list/(method:POST)

Production backend

Первый метод удобен для проверки того, как приложение выглядит в Telegram, но не позволяет нормально проверить бизнес-логику. Второй метод удобен для активной разработки, но не позволяет проверить корректность отображения приложения в Telegram. Чтобы посмотреть на целостную картину нам необходимо опубликовать наш сервер на одном из хостингов.

К сожалению, на рынке не существует бесплатных Java хостингов, однако можно найти относительно недорогие, которые позволяют за разумные деньги (менее 10$ в месяц) разместить ваш сервер. Конечно, можно поднять локальные сервер и это будет условно бесплатно, однако это выходит за рамки нашего примера.

Пользуясь официальной инструкцией разместите ваш экземпляр сервера в Heroku. Из-за того, что код нашего сервера расположен не в корне, понадобится дополнительная настройка окружения, чтобы Heroku увидел наш проект. Воспользуйтесь инструкцией и установите subdir-heroku-buildpack.

Пользуясь инструкцией, уставновите в Heroku Dashboard переменную окружения TELEGRAM_BOT_TOKEN.

После этого запустите ваше приложение и проверьте логи, в них должна содержаться та же информация, что и при запуске локального сервера. Воздмите оттуда адрес вашего сервера и обновите config.json, он должен выглядеть похожим образом:

const configuration = {
    serverUrl: "https://tgminiapp-65728c571d53.herokuapp.com"
}

Компоненты приложения

После завершения предыдущего шага у вас имеется полностью готовое для работы в production приложение, рассмотрим детальнее каждую его часть. Некоторые аспекты работы приложения уже были описаны в процессе настройки, далее будет размещена более подродная информация о других аспектах работы.

Webapp

webapp представляет собой легковесный UI, отображаемый в боте. Исходный код находится в папке webapp. По своей сути это минималистичный вэб-проект (HTML, CSS, JS) без лишних зависимостей, реализующий концепцию single web page application - все необходимые скрипты и стили для генерации страниц загружаются один раз, а сами страницы строятся на основании небольших порций данных, полученных от backend. Такой подход позволяет минимизировать время, необходимое для генерации новой страницы, что оснобенно важно при реализации плавных и быстрых приложений.

Основная и единственная HTML страница - index.html, представляет собой оболочку, которая подключает необходимые css и js зависимости и содержит единственный элемент frame-root в который динамически помещаются отображаемые страницы:

<body>
    <div id="frame-root" class="frame-container"></div>
</body>

navigation.js

Легковесный фрэймворк для реализации концепции single web page application. Содержит основные методы по замене, добавлению и удалению страниц-экранов в frame-root фрэйм. Одной из полезных функция является автоматическое обновление свойства window.Telegram.WebApp.BackButton.isVisible при изменении стэка страниц.

При работе над своей версий приложения, выполненной на базе текущего проекта, для минимизации ошибок навигации рекомендуется использовать методы из этого файла.

*-app.js

Страницы приложения. Каждый такой файл содержит метод display* который должен быть вызван при необходимости открыть страницу. Например, файл page-main.js содержит следующий метод:

function displayMainPage() {
    replaceTopPage("main-page", mainPage());
    loadApps(selectedCategoryId);
    selectCategoryOnUi(selectedCategoryId);
}

Он вызывается после того, как index.html будет полностью загружен, включая все стили и необходимые ресурсы:

window.onload = function() {
    displayMainPage();
};

Выше представлен фрагмент из файла main.js - основной точки входа приложения.

webapp/css

Основной файл style.css - таблица стилей, используемых в приложении. В папке находятся 2 вспомогательных файла: css-class-name-generator.sh и css-class-names.js. Т.к. наше приложение генерирует весь контент на месте, в js файлах, удобно иметь доступ к названиям стилей без необходимости каждый раз копировать их название.

Для решения этой задачи можно каждый раз после добавления или изменения класса в файле style.css запускать скрипт css-class-name-generator.sh, который в свою очередь генерирует файл css-class-names.js - js файл с константами названий классов из style.css. Такой подход позволяет использовать автодополнение во время вёрстки блоков в js файлах.

Пример сгенерированного файла:

// Generated class names from style.css
const cssFrameContainer = "frame-container";
const cssPageContainer = "page-container";
const cssContainerScrollH = "container-scroll-h";

Если ваш css файл называет по другому или вы хотите изменить название выходного файла - модифицируйте строки в css-class-name-generator.sh отвечающие за именование, либо доработайте скрипт таким образом, чтобы он принимал эти значения в виде аргументов.

Backend

Backend часть обрабатывает пользовательские запросы. Исходный код находится в папке backend. Проект сервера представляет собой классический Ktor сервер.

В проекте используются 3 бозовых плагина: Routing для обработки запросов, CORS для поддержки нормального функционирования клиентской части и Content negotiation для сериализации и десирализации объектов.

src/main/resources

Помимо стандартных файлов, необходимых для корректной настройки и запуска сервера, данная дирректория содержит файлы с тестовыми данными: mock-app-details.json - файл со списком приложений и mock-app-rating.json - файл с некоторыми оценками этих приложений.

Для упрощения примера сервер не поддерживает интеграцию с СУБД и не сохраняет состояние при перезапуске. При рестарте сервера все данные сбрасываются до состояния, описанного в указанных выше файлах.

Архитектура сервера позволяет легко реализовать работу с СУБД, о чем будет написано ниже, однако это выходит за рамки данного примера.

src/main/kotlin/.../repository

Содержит интерфейсы для чтения и записи данных. Так же тут находятся 2 подкаталога: mock - классы моделей данных, в которые десириализуются данные из дирректории resources и model - классы, которые используются при работе с бизнес-логикой приложения.

Так же, в дирректории mock расположены реализации интерфейсов репозиториев, которые загружают начальные данные из дирректории resources. По аналогии с реализацией мок репозиториев можно добавить репозитории, которые бы работали с СУБД вместо чтения и десириализации моделей из resources.

src/main/kotlin/.../handler

Содержит классы-обработчики для каждого из запросов, отправляемых из нашего приложения. Самым важным моментом в этих обработчиках является идентификация пользователя.

Клиентское приложение для каждого вызова метода добавляет в тело запроса значение initData. Сервер использует эти данные, чтобы понять от имени какого конкретно пользователя выполяется запрос. Данная информация нужна для того, чтобы возвращать пользователю релевантную информацию по поводу того, какие приложения добавлены в избранное, какие оценки были выставлены и предотвратить утечку этих данных неавторизованным пользователям.

Чтобы убедиться, что данные отправлены от того пользователя, который указан в initDta реализована валидация согласно описанному в документации алгоритму. Именно для этих целей нам и нужен был токен нашего бота.

Код валидации можно посмотреть в файле InitDataParser.kt. Этот класс используется во всех методах обработчиках перед тем, кк заполнить ответ приватными данными.

Например, код, формирующий ответ списка приложений, вышлядит следующим образом:

override suspend fun handle(call: ApplicationCall) {
    val initDataModel = initDataParser.parseInitData(call)
    val userId = initDataModel?.userModel?.id
    log.info("Call from user: $userId")
    val categoryId = call.parameters["category_id"] ?: CATEGORY_ID_ALL
    val userBookmarkedApps = userId?.let { userRepository.getUserAppBookmarks(it) } ?: emptySet()
    val rawApps = when (categoryId) {
        CATEGORY_ID_ALL -> appRepository.getAllApps().sortedByDescending { it.rating }
        CATEGORY_ID_BOOKMARKED -> appRepository.getApps(userBookmarkedApps).sortedBy { it.title }
        else -> appRepository.getApps(categoryId).sortedByDescending { it.rating }
    }
    val returnApps = rawApps.map {
        NetworkAppModel.fromModel(it).copy(bookmarked = userBookmarkedApps.contains(it.id))
    }
    call.respondText(Json.encodeToString(returnApps), ContentType.Application.Json, HttpStatusCode.OK)
}

В случае ошибки валидации данных либо их отсутствии initDataParser вернёт null объект, таким образом сервер будет считать, что запрос обезличиный и отдаст только публичную информацию, без какой-либо персонализации.

По этой причине в первых шагах документации, когда мы клонировали приложение, мы видели данные, однако не могли оценить или добавить приложения в избранное.

Если вы хотите затруднить доступ к вашим данным вне приложения, открытого в Telegram, вы можете более строго обрабатывать данный случай и возвращать ошибку, если обнаружите, что initData не валиден.

Heroku

Для развертывания проекта на Heroku в проекте содержится файл Procfile. Подробнее об интеграции проекта и Heroku можно узнать из официального гайда Ktor