javascript

Как создать мини-приложение в Telegram

  • четверг, 6 марта 2025 г. в 00:00:08
https://habr.com/ru/companies/timeweb/articles/887974/

С каждым днем в Telegram появляется всё больше и больше мини-приложений, или mini apps, которые так или иначе влияют на развитие этой среды. Кто-то реализует в Mini App простые игры, кто-то удобные инструменты для той или иной задачи, а кто-то решения для бизнеса.

В этой статье мы рассмотрим, как создать свое Mini-App-приложение с frontend- и backend-частью, а также запустим его на сервере.

❯ Разработка Telegram Mini App

Создадим простой Mini App с механикой игры кликера и лидерборда, который будет изменятся в зависимости от того, сколько у кого очков.

Выбор технологий для разработки

При разработке мини-приложения мы будем использовать React и Nest.js.

React — это популярная библиотека JavaScript для создания пользовательских интерфейсов, которая будет использована для Frontend-части.

Nest.js — это фреймворк для Node.js, предназначенный для создания масштабируемых серверных приложений, который будет использован для Backend-части.

Создание проекта

Прежде чем начать разработку, необходимо проверить, что на вашем компьютере уже есть Node.js, если нет, то его необходимо скачать с официального сайта. Также должен быть глобально установлен Nest.js. Чтобы установить Nest.js, необходимо открыть консоль и ввести команду: 

  npm i -g @nestjs/cli

После этого можно приступить к созданию проекта.

  1. Создайте папку на рабочем столе.

  2. Откройте консоль Windows или другой терминал.

  3. Перейдите в созданную вами папку. Например, если вы создали папку на рабочем столе, то введите команду:

cd Desktop
  1. Затем введите команду cd еще раз, но уже с названием созданной вами папкой:

cd mini-app-clicker
  1. Выполните команду для создания Frontend-части:

npm create vite@latest frontend

frontend в данном случае — название папки. Вы можете назвать свою папку для фронтенда любым именем.

  1. После выполнения команды вам будет предложено выбрать фреймворк. С помощью стрелок выберите React.

  2. Далее выберите вариант «JavaScript + SWC».

  3. В этой же консоли введите команду для создания Backend-части:

nest new backend

backend в данном случае тоже имя папки, у вас оно может быть иным. 

  1. Далее вас попросят выбрать менеджер пакетов. С помощью стрелок выберите npm.

  2. Откройте главную папку вашего проекта (в моем случае это mini-app-clicker) в любом удобном редакторе кода. Я рекомендую использовать Visual Studio Code (VS Code).

Структура проекта в редакторе кода будет выглядеть следующим образом:

Image17

Написание кода Frontend-части

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

В первую очередь создадим Frontend-часть нашего Mini App. В ней расположим кнопку, прибавление очков, отображение имени (First Name) пользователя, а также добавим вкладку с лидербордом.

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

Откройте консоль и введите команду:

cd frontend 

Вместо frontend укажите ваше название папки для фронтенда.

Затем введите:

npm install

После выполнения этой команды у вас появится новая папка node_modules со всеми необходимыми файлами для запуска проекта. 

Попробуйте запустить пустой проект с помощью команды:

npm run dev

Если всё успешно, вам предложат открыть проект по ссылке http://localhost:5173/. При переходе по ссылке вы увидите стандартную страницу только что созданного проекта.

Image21

Вернитесь в редактор кода. Все необходимые для проекта файлы мы будем создавать в папке src

С помощью консоли установим библиотеку для работы с Telegram Mini App:

npm i @telegram-apps/sdk 

Теперь откройте файл main.jsx, чтобы внести в него некоторые изменения. В будущем мы вернемся к этому файлу.

import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.jsx'

import { init } from '@telegram-apps/sdk';

const initializeTelegramSDK = async () => {
  try {
    await init();

  } catch (error) {
    console.error('Ошибка инициализации:', error);
  }
};

initializeTelegramSDK();

createRoot(document.getElementById('root')).render(
  <StrictMode>
    <App />
  </StrictMode>,
)

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

Image23

Изменим index.css, удалив лишние стили, которые нам не нужны. Файл будет выглядеть следующим образом:

:root {
  font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
  line-height: 1.5;
  font-weight: 400;

  color: #ffffff;
  background-color: #181818;

  font-synthesis: none;
  text-rendering: optimizeLegibility;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

Установим fontawesome-иконки, которые будут использованы в нашем проекте. Введите команду:

npm i --save @fortawesome/fontawesome-svg-core @fortawesome/free-solid-svg-icons @fortawesome/free-regular-svg-icons @fortawesome/free-brands-svg-icons

Теперь откройте файл App.jsx и введите там следующий код:

import { useState } from 'react';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faTrophy } from '@fortawesome/free-solid-svg-icons';
import './App.css';

import Leaderboard from './Components/Leaderboard';

function App() {
  const [score, setScore] = useState(0);
  const [showLeaderboard, setShowLeaderboard] = useState(false);

  const handleClick = () => {
    setScore(score + 1);
  };

  return (
    <div className='App'>
      {showLeaderboard ? (
        <Leaderboard setShowLeaderboard={setShowLeaderboard} />
      ) : (
        <>
          <div className='header'>
            <h1 className='firstname'>
              <span id='firstname'>Имя</span>
            </h1>
          </div>
          <div className='content'>
            <div className='score-container'>
              <h2 className='score'>
                <span id='score'>{score}</span>
              </h2>
            </div>
            <div className='button-container'>
              <button className='button-click' id='button-click' onClick={handleClick}>Нажми</button>
            </div>
          </div>
          <div className='footer'>
            <button className='btn-leaderboard' id='btn-leaderboard' onClick={() => setShowLeaderboard(true)}>
              <FontAwesomeIcon icon={faTrophy} />
            </button>
          </div>
        </>
      )}
    </div>
  );
}

export default App;

Добавим стили. Откройте файл App.css и измените его следующим образом:

.App {
  display: flex;
  flex-direction: column;
  align-items: center;
}

.header {
  margin-top: 20px;
  margin-bottom: 20px;
}

.score-container {
  margin-bottom: 20px;
}

.score {
  display: flex;
  justify-content: center;
  font-size: 48px;
}

.button-container {
  display: flex;
  justify-content: center;
}

.button-click {
  width: 200px;
  height: 200px;
  background-color: #d3862e;
  color: white;
  border-radius: 50%;
  border: none;
  cursor: pointer;
  display: flex;
  justify-content: center;
  align-items: center;
  font-size: 30px;
  font-weight: bold;
  margin: 8px 0;
}

.button-click:hover {
  opacity: 0.8;
}

.footer {
  background-color: #181818;
  position: fixed;
  bottom: 0;
  width: 100%;
  color: white;
  text-align: center;
  padding: 10px 0;
}

.btn-leaderboard {
  background-color: transparent;
  color: white;
  border: none;
  padding: 10px 20px;
  cursor: pointer;
  border-radius: 10px;
  font-size: 40px;
}

После этого создадим новую папку Components внутри src, а в ней — два новых файла: Leaderboard.jsx и Leaderboard.css

Откройте файл Leaderboard.jsx и вставьте следующий код:

import React from 'react';
import PropTypes from 'prop-types';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faHouse } from '@fortawesome/free-solid-svg-icons';
import './Leaderboard.css';

function Leaderboard({ setShowLeaderboard }) {
  return (
    <div className='Leaderboard'>
      <div className='header-leaderboard'>
        <h1 className='leaderboard-title'>
          Топ игроков
        </h1>
      </div>
      <div className='content-leaderboard'>
        <ul className='leaderboard-list'>
          <h2>Лучшие игроки:</h2>
          <li>
            <div className='leaderboard-item'>
              <div className='leaderboard-rank'>
                <h3 className='leaderboard-place'>
                  <span id='leaderboard-place'>1</span>.
                </h3>
                <h3 className='leaderboard-name'>
                  <span id='leaderboard-name'>Имя</span>
                </h3>
              </div>
              <h3 className='leaderboard-score'>
                <span id='leaderboard-score'>10</span>
              </h3>
            </div>
          </li>
          <li>
            <div className='leaderboard-item'>
              <div className='leaderboard-rank'>
                <h3 className='leaderboard-place'>
                  <span id='leaderboard-place'>1</span>.
                </h3>
                <h3 className='leaderboard-name'>
                  <span id='leaderboard-name'>Имя</span>
                </h3>
              </div>
              <h3 className='leaderboard-score'>
                <span id='leaderboard-score'>10</span>
              </h3>
            </div>
          </li>
          <li>
            <div className='leaderboard-item'>
              <div className='leaderboard-rank'>
                <h3 className='leaderboard-place'>
                  <span id='leaderboard-place'>1</span>.
                </h3>
                <h3 className='leaderboard-name'>
                  <span id='leaderboard-name'>Имя</span>
                </h3>
              </div>
              <h3 className='leaderboard-score'>
                <span id='leaderboard-score'>10</span>
              </h3>
            </div>
          </li>
        </ul>
      </div>
      <div className='footer-leaderboard'>
        <button className='btn-home' onClick={() => setShowLeaderboard(false)}>
          <FontAwesomeIcon icon={faHouse} />
        </button>
      </div>
    </div>
  );
}

Leaderboard.propTypes = {
  setShowLeaderboard: PropTypes.func.isRequired,
}
;
export default Leaderboard;

Добавим стили для страницы с лидербордом. Откройте файл Leaderboard.css и измените его следующим образом:

.Leaderboard {
    display: flex;
    flex-direction: column;
    align-items: center;
}

.content-leaderboard {
    width: 100%;
    max-width: 400px;
    margin: 20px auto;
}

.leaderboard-list {
    list-style: none;
    padding: 0;
    margin: 0;
    width: 300px;
    height: 100px;
}

.leaderboard-item {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 10px;
    margin-bottom: 10px;
    background-color: #333;
    border-radius: 10px;
    height: 50px;
}

.leaderboard-rank {
    display: flex;
    align-items: center;
    gap: 10px;
}

.leaderboard-place,
.leaderboard-name,
.leaderboard-score {
    font-size: 24px;
}

.footer-leaderboard {
    position: fixed;
    bottom: 0;
    width: 100%;
    color: white;
    text-align: center;
    padding: 10px 0;
    background-color: #181818;
}
 
.btn-home {
  background-color: transparent;
  color: white;
  border: none;
  padding: 10px 20px;
  cursor: pointer;
  border-radius: 10px;
  font-size: 40px;
}

После всех этих изменений у нас будет готова логика кликера, а также заготовка на будущий лидерборд:

Image24

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

Image13

Затем нужно настроить параметры экрана так, чтобы они соответствовали параметрам Mini App. В поле ширина (первая строка) введите значение 360, а в поле высота (вторая строка) — 800. Эти значения будут примерным разрешением для Mini App.

Image15

Написание кода Backend-части

Теперь приступим к написанию Backend-части. 

Так как мы создаваем простой тестовый проект для быстрого знакомства с разработкой Mini App, мы будем использовать SQLite3. В реальности, если вы собираетесь создавать большой проект, с тысячами пользователей и большой нагрузкой, я рекомендую использовать PostgreSQL или другую подобную базу данных.

Откройте консоль и введите команду cd backend, указав ваше имя папки для бэкенда. Введите команду ниже для установки всех зависимостей, которые нам будут нужны:

npm install @nestjs/common @nestjs/core @nestjs/platform-express @nestjs/config @nestjs/typeorm sqlite3 typeorm class-validator class-transformer

Все необходимые файлы для разработки будем создавать и изменять в папке src.

Создайте файл ormconfig.ts и пропишите в нем настройки для TypeORM (SQLite3):

import { TypeOrmModuleOptions } from '@nestjs/typeorm';

export const typeOrmConfig: TypeOrmModuleOptions = {
  type: 'sqlite',
  database: 'database.sqlite',
  entities: [__dirname + '/**/*.entity{.ts,.js}'],
  synchronize: true,
};

Теперь создайте новую папку с названием users, а в ней файл user.entity.ts. Изменим файл следующим образом:

import { Entity, Column, PrimaryColumn } from 'typeorm';

@Entity()
export class User {
  @PrimaryColumn()
  id: string;

  @Column()
  firstname: string;

  @Column({ default: 0 })
  points: number;
}

В этой же папке создайте файл user.service.ts:

import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from './user.entity';

@Injectable()
export class UserService {
  constructor(
    @InjectRepository(User)
    private userRepository: Repository<User>,
  ) {}

  async createUser(id: string, firstname: string): Promise<User> {
    let user = await this.userRepository.findOne({ where: { id } });
    if (!user) {
      user = this.userRepository.create({ id, firstname });
      await this.userRepository.save(user);
    }
    return user;
  }

  async getUser(id: string): Promise<User | null> {
    return this.userRepository.findOne({ where: { id } });
  }

  async updatePoints(id: string, points: number): Promise<User | null> {
    const user = await this.getUser(id);
    if (user) {
      user.points += points;
      await this.userRepository.save(user);
      return user;
    }
    return null;
  }

  async getLeaderboard(): Promise<User[]> {
    return this.userRepository.find({ order: { points: 'DESC' }, take: 10 });
  }

  async getUserById(id: string): Promise<User | null> {
    return this.userRepository.findOne({ where: { id } });
  }

  async getUserRank(
    id: string,
  ): Promise<{ firstname: string; points: number; rank: number } | null> {
    const leaderboard = await this.getLeaderboard();
    const index = leaderboard.findIndex((user) => user.id === id);

    if (index === -1) {
      return null;
    }

    const user = leaderboard[index];
    return {
      firstname: user.firstname,
      points: user.points,
      rank: index + 1,
    };
  }
}

Рядом с этими файлами создайте файл user.controller.ts:

import {
  Controller,
  Post,
  Get,
  Body,
  Param,
  NotFoundException,
} from '@nestjs/common';
import { UserService } from './user.service';

@Controller('users')
export class UserController {
  constructor(private readonly userService: UserService) {}

  @Post('create')
  async createUser(@Body() body: { id: string; firstname: string }) {
    return this.userService.createUser(body.id, body.firstname);
  }

  @Post('update')
  async updatePoints(@Body() body: { id: string; points: number }) {
    return this.userService.updatePoints(body.id, body.points);
  }

  @Get('leaderboard')
  async getLeaderboard() {
    return this.userService.getLeaderboard();
  }

  @Get('leaderboard/:id')
  async getUserRank(@Param('id') id: string) {
    return this.userService.getUserRank(id);
  }

  @Get(':id')
  async getUser(@Param('id') id: string) {
    const user = await this.userService.getUserById(id);
    if (!user) {
      throw new NotFoundException('User not found');
    }
    return { firstname: user.firstname, points: user.points };
  }
}

Теперь подключим всё это в один файл. Создайте user.module.ts и напишите в нем следующие:

import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from './user.entity';
import { UserService } from './user.service';
import { UserController } from './user.controller';

@Module({
  imports: [TypeOrmModule.forFeature([User])],
  controllers: [UserController],
  providers: [UserService],
})
export class UserModule {}

Откройте файл app.module.ts, который находится в папке src, и измените его:

import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UserModule } from './users/user.module';
import { typeOrmConfig } from './ormconfig';

@Module({
  imports: [TypeOrmModule.forRoot(typeOrmConfig), UserModule],
})
export class AppModule {}

Если после внесения изменений файлы подсвечиваются у вас красным цветом, эта проблема связана с проверкой форматирования, выполняемой Prettier. Чтобы исправить эти ошибки, просто введите в консоли команду:

npx prettier --write .

В конце команды обязательна должна быть точка.

Запустите Backend с помощью команды:

npm run start

Первый запуск может быть долгим, но если всё успешно, то в консоли вы увидите примерно следующее:

Image1

Теперь необходимо протестировать все запросы, чтобы мы точно знали, что наш Backend работает правильно. Лучше всего для тестирования запросов подойдет приложение Postman.

  1. Откройте Postman на своем ПК.

  2. Создайте новый запрос для создания пользователя, нажав на «+» в верхнем меню.

  3. Рядом с полем для ввода URL нажмите на метод запроса и в открывшемся списке выберите POST-запрос, после чего в поле для ввода URL введите:

  http://localhost:3000/users/create
  1. Перейдите во вкладку «Body», которая находится под полем для ввода URL. В ней выберите «raw». Справа нажмите на «Text и выберите метод «JSON». В текстовое поле введите:

{
  "id": "123456",
  "firstname": "Test"
}

Тут мы указываем, какие параметры мы передаем в теле запроса, чтобы создать нового пользователя.

  1. Перейдите во вкладку «Headers». В поле Key необходимо ввести «Content-Type», а в «Value» — «application/json».

  2. Отправляем наш запрос, нажав на кнопку «Send». Если всё успешно, то внизу появится ответ от нашего запроса с ID, firstname и points.

Image10

А также справа будет указан код ответа 201:

Image8
  1. Создайте новый запрос для обновления очков пользователя, нажав на «+» в верхнем меню.

  2. Выберите метод «POST-запрос», после чего в поле для ввода URL введите:

http://localhost:3000/users/update
  1. Перейдите во вкладку «Body». В ней выберите «raw» и выберите метод «JSON». В текстовое поле введите:

{
  "id": "123456",
  "points": 10
}
  1. Перейдите во вкладку «Headers». В поле «Key» необходимо ввести «Content-Type», а в «Value» — «application/json».

  2. Отправляем наш запрос, нажав на кнопку «Send». Если всё успешно, то вы получите ответ с обновленными очками:

Image7
  1. Создайте новый запрос для получения данных пользователя, нажав на «+» в верхнем меню.

  2. Выберите «GET-запрос», после чего в поле для ввода URL введите:

http://localhost:3000/users/123456

Будьте внимательны при отправке этого запроса: после users/ необходимо указать ID пользователя, который уже создан, иначе вам вернет ошибку 404.

  1. Отправляем наш запрос, нажав на кнопку «Send». Если всё успешно, то вы получите firstname и points этого пользователя:

Image22

А также справа будет указан код ответа 200:

Image20
  1. Создайте новый запрос для получения лидерборда, нажав на «+» в верхнем меню.

  2. Выберите «GET-запрос», после чего в поле для ввода URL введите:

http://localhost:3000/users/leaderboard
  1. Отправляем наш запрос, нажав на кнопку «Send». Если всё успешно, то вы получите список пользователей:

Image3

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

В процессе тестирования Backend’а можно отслеживать, сколько времени занимает тот или иной запрос, а также сколько он весит, благодаря данным из Postman.

С помощью Response Time можно отследить, какой процесс занимает то или иное время, благодаря чему можно проанализировать их и при необходимости оптимизировать запросы, сделав их быстрее.

Image16

А с помощью Response Size и Request Size можно узнать вес запросов, что позволяет при необходимости их оптимизировать.

Image25

Объединение Frontend и Backend

Теперь нужно объединить написанные нами Frontend- и Backend-части.

Откройте файл vite.config.js из Frontend-части и измените его следующим образом, чтобы в будущем при сборке на сервере не возникло ошибок:

import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react-swc'

export default defineConfig({
  plugins: [react()],
  build: {
    outDir: 'build',
  }
})

Откройте файл main.jsx и измените его следующим образом:

import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import './index.css';
import App from './App.jsx';

import { init, initData, miniApp } from '@telegram-apps/sdk';

const initializeTelegramSDK = async () => {
  try {
    await init();

    if (miniApp.ready.isAvailable()) {
      await miniApp.ready();
      window.dispatchEvent(new Event('miniAppReady'));
    }

    initData.restore();
    initData.state();

    const user = initData.user();
    console.log('Данные пользователя:', user);

    if (user) {
      window.userId = user.id;
      window.firstName = user.first_name;

      console.log('ID пользователя:', window.userId);
      console.log('Имя пользователя:', window.firstName);

      // Ожидаем полной готовности всех данных
      window.dispatchEvent(new Event('userIdReady'));
    } else {
      console.error('Ошибка: Пользовательские данные не загружены!');
    }
  } catch (error) {
    console.error('Ошибка инициализации Telegram Mini App:', error);
  }
};

// Дожидаемся обоих событий, затем рендерим приложение
const waitForUserData = async () => {
  await new Promise((resolve) => {
    const checkReady = () => {
      if (window.userId && window.firstName && miniApp.ready.isAvailable()) {
        resolve();
      }
    };

    window.addEventListener('userIdReady', checkReady);
    window.addEventListener('miniAppReady', checkReady);

    checkReady();
  });

  createRoot(document.getElementById('root')).render(
    <StrictMode>
      <App />
    </StrictMode>,
  );
};

initializeTelegramSDK().then(waitForUserData);

Затем откройте и измените App.jsx:

import { useState, useEffect } from 'react';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faTrophy } from '@fortawesome/free-solid-svg-icons';
import './App.css';
import Leaderboard from './Components/Leaderboard';
import Enquiry from './API/Enquiry';

function App() {
  const [score, setScore] = useState(0);
  const [firstname, setFirstname] = useState('Загрузка...');
  const [showLeaderboard, setShowLeaderboard] = useState(false);
  const [leaderboard, setLeaderboard] = useState([]);

  useEffect(() => {
    const handleScoreUpdate = (event) => {
      if (event.detail.points !== undefined) {
        setScore(event.detail.points);
      }
    };

    window.addEventListener('updateScore', handleScoreUpdate);
    return () => {
      window.removeEventListener('updateScore', handleScoreUpdate);
    };
  }, []);

  const handleClick = () => {
    setScore((prev) => prev + 1);
    window.dispatchEvent(new CustomEvent('updateScoreFromApp', { detail: { points: score + 1 } }));
  };

  return (
    <div className='App'>
      <Enquiry
        score={score}
        setScore={setScore}
        setFirstname={setFirstname}
        setLeaderboard={setLeaderboard}
      />
      {showLeaderboard ? (
        <Leaderboard setShowLeaderboard={setShowLeaderboard} leaderboard={leaderboard} />
      ) : (
        <>
          <div className='header'>
            <h1 className='firstname'><span id='firstname'>{firstname}</span></h1>
          </div>
          <div className='content'>
            <div className='score-container'>
              <h2 className='score'><span id='score'>{score}</span></h2>
            </div>
            <div className='button-container'>
              <button className='button-click' onClick={handleClick}>Нажми</button>
            </div>
          </div>
          <div className='footer'>
            <button className='btn-leaderboard' onClick={() => setShowLeaderboard(true)}>
              <FontAwesomeIcon icon={faTrophy} />
            </button>
          </div>
        </>
      )}
    </div>
  );
}

export default App;

Теперь измените Leaderboard.jsx:

import PropTypes from 'prop-types';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faHouse } from '@fortawesome/free-solid-svg-icons';
import './Leaderboard.css';

function Leaderboard({ setShowLeaderboard, leaderboard }) {
  return (
    <div className='Leaderboard'>
      <div className='header-leaderboard'>
        <h1 className='leaderboard-title'>Топ игроков</h1>
      </div>
      <div className='content-leaderboard'>
        <ul className='leaderboard-list'>
          {leaderboard.length > 0 ? (
            leaderboard.map((player, index) => (
              <li key={player.id}>
                <div className='leaderboard-item'>
                  <div className='leaderboard-rank'>
                    <h3 className='leaderboard-place'>{index + 1}.</h3>
                    <h3 className='leaderboard-name'>{player.firstname}</h3>
                  </div>
                  <h3 className='leaderboard-score'>{player.points}</h3>
                </div>
              </li>
            ))
          ) : (
            <p>Лидерборд загружается...</p>
          )}
        </ul>
      </div>
      <div className='footer-leaderboard'>
        <button className='btn-home' onClick={() => setShowLeaderboard(false)}>
          <FontAwesomeIcon icon={faHouse} />
        </button>
      </div>
    </div>
  );
}

Leaderboard.propTypes = {
  setShowLeaderboard: PropTypes.func.isRequired,
  leaderboard: PropTypes.array.isRequired,
};

export default Leaderboard;

Создайте новую папку API внутри src Frontend’а, а в ней — файл Enquiry.jsx. Через него мы будем отправлять запросы на наш Backend.

import { useEffect, useState } from 'react';
import PropTypes from 'prop-types';

const Enquiry = ({ score, setScore, setFirstname, setLeaderboard }) => {
  const [isLoaded, setIsLoaded] = useState(false);

  useEffect(() => {
    const fetchUserData = async () => {
      if (!window.userId) return;

      try {
        let response = await fetch(`/users/${window.userId}`);
       
        if (response.status === 404) {
          await fetch('/users/create', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ id: window.userId, firstname: window.firstName }),
          });

          response = await fetch(`/users/${window.userId}`);
        }

        const userData = await response.json();
        setFirstname(userData.firstname || 'Неизвестный');
        setScore(userData.points || 0);
        setIsLoaded(true);
      } catch (error) {
        console.error('Ошибка при получении данных пользователя:', error);
      }
    };

    if (window.userId) {
      fetchUserData();
    } else {
      window.addEventListener('userIdReady', fetchUserData);
    }

    return () => {
      window.removeEventListener('userIdReady', fetchUserData);
    };
  }, [setScore, setFirstname]);

  useEffect(() => {
    if (!isLoaded) return;

    const updateUserPoints = async () => {
      if (!window.userId) return;

      try {
        await fetch('/users/update', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ id: window.userId, points: score }),
        });
      } catch (error) {
        console.error('Ошибка при обновлении очков:', error);
      }
    };

    updateUserPoints();
  }, [score, isLoaded]);

  useEffect(() => {
    const fetchLeaderboard = async () => {
      try {
        const response = await fetch('/users/leaderboard');
        if (!response.ok) throw new Error('Ошибка загрузки');
        const data = await response.json();
        setLeaderboard(data);
      } catch (error) {
        console.error('Ошибка при получении лидерборда:', error);
      }
    };

    fetchLeaderboard();
  }, [setLeaderboard]);

  return null;
};

Enquiry.propTypes = {
  score: PropTypes.number.isRequired,
  setScore: PropTypes.func.isRequired,
  setFirstname: PropTypes.func.isRequired,
  setLeaderboard: PropTypes.func.isRequired,
  setUserRank: PropTypes.func.isRequired,
};

export default Enquiry;

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

❯ Запуск

Когда всё будет готово, необходимо запустить наш Frontend и Backend

Загрузка на GitHub

Создайте два приватных репозитория на GitHub для Frontend’а Backend’а и затем вернитесь к вашему проекту. Нужно выполнить следующие действия для обеих частей Mini App:

  1. Перейдите в нужную директорию.

  2. Создайте новый репозиторий Git:

  git init
  1. Добавьте все изменения к коммиту:

git add . 
  1. Создайте коммит:

git commit -m "first commit"
  1. Переименуйте текущую ветку и установите ее как основную:

git branch -M main
  1. Добавьте удаленный репозиторий и свяжите его с указанным URL. Вместо моей ссылки укажите ссылку на ваш новый созданный репозиторий:

https://github.com/Tulopex/mini-app-clicker-backend
  1. Отправьте изменения на репозитории:

git push -u origin main

Загрузка на сервер и завершение настройки Mini App

После того как файлы будут загружены на GitHub, можно приступить к запуску Mini App на сервере. Я буду использовать сервис Apps от Timeweb Cloud. Если вы еще не зарегистрированы на Timeweb Cloud, то для начала необходимо пройти регистрацию.

Прежде всего необходимо создать новый проект. Дайте ему наименование, а при необходимости — добавьте описание и изображение.

Image12

Далее необходимо создать Apps. В первую очередь мы запустим наш Backend. В процессе создания нужно выбрать тип Backend и платформу Nest.js.

Image5

Для загрузки проектов необходимо привязать учетную запись на GitHub. После привязки введите название репозитория, которое вы указывали при создании. В разделе «Регион» выберите тот, который находится ближе к вам и имеет наименьший пинг.

Image6

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

Image27

В описании приложения укажите его наименование, при необходимости — комментарий, а также выберите проект, к которому будет привязано ваше приложение.

Image9

Затем можно активировать процесс развертывания Nest.js на сервере, нажав на кнопку «Запустить деплой». Через некоторое время приложение будет запущено, и в случае успешного развертывания проекта в журнале деплоя появится сообщение «Deployment successfully completed». Но ещё до полного запуска нам будет доступен домен, который уже можно скопировать. Расположен он будет на Дашборде, в окошке справа

Image2

В моем случае нужно отправлять запросы на адрес:

https://tulopex-mini-app-clicker-backend-c482.twc1.net

Этот адрес (с указанием вашего домена) необходимо вставить во все запросы, которые находятся в файле Enquiry.jsx.

После необходимо отправить наше изменения на GitHub, выполнив поочередно команды:

git add . 
git commit -m "requests"
git push

Теперь можно запускать наш Frontend. При создании Apps нужно выбрать Frontend и React.

Image4

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

Когда Frontend начнет запускаться, вы получите бесплатный домен. Найти его можно в разделе «Настройки» вашего приложения. Его необходимо скопировать.

Image26

Теперь необходимо открыть файл main.ts из Backend’a и изменить его таким образом, чтобы он принимал запросы только от нашей Frontend-части:

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  app.enableCors({
    origin: 'https://tulopex-mini-app-clicker-frontend-e16c.twc1.net/',
    methods: 'GET,POST,PUT,DELETE',
    allowedHeaders: 'Content-Type,Authorization',
  });

  await app.listen(process.env.PORT ?? 3000);
}

bootstrap();

Отправьте изменения Backend’a на GitHub.

После того как все необходимые шаги были выполнены, можно приступать к завершающему этапу, после которого можно будет начать тестировать Mini App в Telegram.

❯ Настройка Telegram-бота для Mini App

Пока Frontend и Backend запускаются, необходимо создать и настроить нашего бота, чтобы Mini App открывался внутри Telegram.

Перейдите в BotFather и запустите его. После этого необходимо выполнить команду /newbot. Бот предложит вам ввести имя для нового бота, а затем его username. Если вы попытаетесь использовать уже занятый username, бот сообщит о невозможности его использования и предложит вам выбрать другой.

При успешном исходе событий бот ответит следующим образом:

Image14

Теперь вы можете настроить созданного бота. Для этого используйте команду /mybots. Бот отобразит список всех созданных ботов. Выберите нужный с помощью инлайн-кнопок. Откроется следующее меню:

Image18

Перейдите в «Bot Settings», далее в «Menu Button» и нажмите на «Configure menu button». BotFather попросит прислать URL, который будет открываться при нажатие на специальную кнопку. Отправьте ему вашу ссылку на Frontend. После отправки он попросит прислать название для этой кнопки. Если вы сделали всё верно, то бот ответит успехом.

Image19

Далее нажмите на «Back to Bot» и повторно зайдите в настройки. Теперь необходимо нажать на «Configure Mini App» и в открывшемся меню нажать на инлайн-кнопку «Enable Mini App». Бот снова попросит ссылку.

Image11

Снова нажмите на «Back to Bot», зайдите в настройки и опять же кликните по «Configure Mini App». В открывшемся меню нажмите на «Change Mod». Внутри выберите Fullscreen. Благодаря этому приложение будет открываться на весь экран и не будет иметь рамок.

Активируйте бота, нажав на кнопку «Запустить», чтобы отправилась команда «Start». Бот не будет нам отвечать, но слева внизу будет кнопка, активировав которую, мы сможем открыть наш Telegram Mini App.

❯ Полезные ресурсы для разработчиков Mini App

При создании Mini App разработчики сталкиваются с необходимостью изучения различных документаций и инструментов, чтобы обеспечить соответствие своих Mini App стандартам разработки, их корректную работу и актуальность кода.

  • Официальная документация от Telegram: Это официальная документация от Telegram, в которой подробно расписано про каждую функцию, как ее вызывать и что она делает. Преимущественно ориентирована на чистый JavaScript, но с ее помощью можно создавать пакеты для многих фреймворков.

  • Документация с пакетами для Mini App: В этой документации собраны многие пакеты для разработки Mini App. Создана Владиславом Кибенко (бывший сотрудник Яндекса, ныне специалист в области разработки Telegram Mini App) и сообществом разработчиков.

  • Telemetree: С помощью этого инструмента можно собирать различную аналитику с Mini App, связанную с пользователями и их активностью. Подробнее о том, как его интегрировать, мы писали в другой статье.

  • AdsGram: Если вы создаете игру или проект в который нужно добавить монетизацию, то это отличный инструмент. С его помощью можно размещать рекламу, которая будет приносить прибыль. В этой статье вы найдете более подробную информацию о том, как использовать AdsGram.

❯ Заключение

Создание Mini App в Telegram — это увлекательный процесс, который открывает перед разработчиками множество перспектив. В этой статье мы детально рассмотрели ключевые шаги разработки, определили нужные технологии и создали фундамент для игры-кликера с функцией рейтинга.

Автор текста: Вадим Андоськин


Новости, обзоры продуктов и конкурсы от команды Timeweb.Cloud — в нашем Telegram-канале 

Перейти ↩

Перед оплатой в разделе «Бонусы и промокоды» в панели управления активируйте промокод и получите кэшбэк на баланс.

📚 Читайте также: