javascript

Вебсокеты на FastAPI: Реализация простого чата с комнатами за 20 минут

  • воскресенье, 23 февраля 2025 г. в 00:00:05
https://habr.com/ru/companies/amvera/articles/884816/

В ближайшее время я планирую опубликовать большую статью на Хабре, где подробно разберу разработку анонимного чата в формате Telegram MiniApp. Мы создадим сервис для общения тет-а-тет, который будет работать внутри Telegram и обеспечивать мгновенный обмен сообщениями.

Технологический стек проекта:

  • FastAPI + WebSocket – для реального времени и обмена сообщениями

  • Redis – для быстрого поиска и соединения собеседников

  • PostgreSQL – для хранения сообщений и информации о пользователях

  • Vue 3 + Pinia – для удобного и отзывчивого интерфейса

  • Telegram Mini Apps API – чтобы встроить чат прямо в Telegram

Кроме технического разбора, в статье я также затрону вопрос монетизации. Одним из самых эффективных способов монетизации Telegram MiniApp является реклама. Это позволяет оставить сервис бесплатным для пользователей, а разработчику — зарабатывать на популярности проекта.

В качестве рекламной платформы буду рассматривать RichAds – они предлагают отличные инструменты для интеграции рекламы в Telegram MiniApps, что делает их логичным выбором для такого проекта.

Но прежде чем погружаться в разработку MiniApp, давайте разберёмся с основами. В этой статье мы познакомимся с веб-сокетами, узнаем, как они работают, и научимся реализовывать их на FastAPI.

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

Итак, что такое WebSocket и как он поможет нам в разработке чата? Давайте разбираться.

WebSocket: принципы работы

Представьте себе телефонный звонок: вы набираете номер (устанавливаете соединение) один раз, и после этого можете свободно разговаривать, не набирая номер заново для каждой фразы. WebSocket работает похожим образом – это технология, которая создает постоянный "канал связи" между браузером пользователя и сервером, позволяя им обмениваться сообщениями в реальном времени.

Давайте разберемся с терминами:

  • Клиент – это то, чем пользуется человек: веб-страница в браузере, приложение на телефоне или компьютере

  • Сервер – это удаленный компьютер, который хранит и обрабатывает данные нашего приложения

Ключевые преимущества WebSocket:

  1. Постоянное соединение: как телефонный звонок – установили связь один раз и общаетесь

  2. Быстрая работа: не тратится время на повторное соединение

  3. Двусторонняя связь: и клиент, и сервер могут начать общение первыми

  4. Экономия ресурсов: служебная информация передается только в начале соединения

Сравнение подходов к реализации чата

Классический способ (HTTP):

Представьте, что вы отправляете письма:

  1. Пишете сообщение и отправляете его на сервер

  2. Сервер сохраняет письмо

  3. Другие участники чата должны постоянно проверять "почтовый ящик" (делать запросы к серверу)

  4. Только после проверки они увидят новое сообщение

Даже если проверка происходит автоматически (например, через AJAX), всё равно приходится постоянно "заглядывать в почтовый ящик", что создает лишнюю нагрузку.

Недостатки:

  • Сервер устает от постоянных проверок

  • Сообщения доходят с задержкой

  • Тратится много интернет-трафика

Способ с использованием WebSocket:

Теперь представьте групповой звонок:

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

  2. Когда кто-то говорит, все сразу это слышат

  3. Не нужно постоянно проверять, есть ли новые сообщения

  4. Общение происходит мгновенно

Преимущества:

  • Сообщения приходят моментально

  • Сервер меньше нагружен

  • Экономится интернет-трафик

  • Настоящее общение в реальном времени

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

Дальше мы создадим простой чат на FastAPI, чтобы увидеть, как это работает на практике. Для простоты примера мы сделаем и серверную, и клиентскую части в одном приложении, хотя в реальном чате "Тет-а-Тет" интерфейс будет отдельным приложением на VueJS3.

Какой проект мы будем разрабатывать?

Сегодня мы создадим полноценное FullStack-приложение – групповой чат, в котором пользователи смогут:

  • Создавать комнаты для общения

  • Подключаться к существующим комнатам

  • Обмениваться сообщениями в реальном времени

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

Технологический стек

Для реализации проекта мы будем использовать современные и удобные инструменты:

  • Python + FastAPI + WebSockets – для серверной части и организации реального времени

  • JavaScript – для установки соединений по веб-сокетам и динамики на странице

  • TailwindCSS – для быстрой и удобной стилизации интерфейса

  • HTML + Jinja2 – для рендеринга страниц с данными

  • Amvera Cloud – для быстрого и простого деплоя

Этапы разработки

1. Серверная часть (Backend)

  • Разработка класса для управления веб-сокетами

  • Создание эндпоинта для обработки подключений через WebSocket

  • Реализация маршрутов (эндпоинтов) для рендеринга HTML-страниц

2. Клиентская часть (Frontend)

  • Разработка двух HTML-страниц

  • Добавление JavaScript-логики для подключения к WebSocket

  • Реализация динамического обновления интерфейса

3. Деплой проекта

Групповой чат бессмыслен без доступа извне, поэтому в завершение мы развернём проект в интернете.

Для деплоя будем использовать Amvera Cloud – сервис, который позволяет развернуть FastAPI-приложение буквально за пару минут. Плюсы этого решения:

  • Автоматическое HTTPS-сертификаты

  • Бесплатное доменное имя

  • Поддержка вебхуков (например, для интеграции с Telegram-ботами)

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

Подготовка проекта

Начнем с подготовки проекта. Напоминаю, что писать мы будем на Python фреймворке FastAPI, который отлично подходит для реализации WebSocket благодаря своей асинхронной природе и простому API.

  • Открываем IDE и создаем новый проект

  • Создаем файл requirements.txt и заполняем его следующим образом:

fastapi==0.115.8    # Сам веб-фреймворк
websockets==15.0    # Библиотека для работы с WebSocket
uvicorn==0.34.0     # ASGI сервер для запуска приложения
jinja2==3.1.5       # Шаблонизатор для рендеринга HTML
  • Устанавливаем библиотеки:

pip install -r requirements.txt
  • Подготовим структуру проекта:

my_chat_project/
├── requirements.txt         # Файл зависимостей проекта
├── app/                     # Главная директория приложения
│   ├── templates/           # Директория с HTML-шаблонами
│   │   ├── home.html        # Шаблон домашней страницы
│   │   └── index.html       # Основной шаблон приложения
│   ├── static/              # Директория со статическими файлами (JS, CSS)
│   │   └── index.js         # JavaScript для index.html
│   ├── api/                 # Директория с API-роутами
│   │   ├── router_page.py   # Роуты для страниц (HTML)
│   │   └── router_socket.py # Роуты для WebSocket-соединений
│   └── main.py              # Основной файл приложения (запуск)

Пишем серверную часть

Основную логику серверной части нашего приложения мы опишем в файле router_socket.py. Этот файл будет отвечать за управление WebSocket-соединениями и передачу сообщений между пользователями.

Импортируем необходимые модули

from fastapi import APIRouter, WebSocket, WebSocketDisconnect
from typing import Dict
  • FastAPI — используется для создания веб-приложения.

  • WebSocket, WebSocketDisconnect — классы для работы с WebSocket-соединениями. Первый позволяет устанавливать соединения, второй — обрабатывать их разрыв.

  • Dict — тип данных для удобной аннотации структуры хранения подключений пользователей.

Инициализация маршрутизатора

FastAPI использует маршрутизаторы (APIRouter) для организации эндпоинтов в логические блоки. В данном случае, создадим маршрутизатор с префиксом /ws/chat:

router = APIRouter(prefix="/ws/chat")

Этот маршрут будет обрабатывать все WebSocket-запросы, связанные с чатом.

Создание менеджера подключений

Для удобного управления соединениями создадим класс ConnectionManager. Он будет отвечать за подключение и отключение пользователей, а также за рассылку сообщений.

Полный код класса:

class ConnectionManager:
    def __init__(self):
        # Хранение активных соединений в виде {room_id: {user_id: WebSocket}}
        self.active_connections: Dict[int, Dict[int, WebSocket]] = {}

    async def connect(self, websocket: WebSocket, room_id: int, user_id: int):
        """
        Устанавливает соединение с пользователем.
        websocket.accept() — подтверждает подключение.
        """
        await websocket.accept()
        if room_id not in self.active_connections:
            self.active_connections[room_id] = {}
        self.active_connections[room_id][user_id] = websocket

    def disconnect(self, room_id: int, user_id: int):
        """
        Закрывает соединение и удаляет его из списка активных подключений.
        Если в комнате больше нет пользователей, удаляет комнату.
        """
        if room_id in self.active_connections and user_id in self.active_connections[room_id]:
            del self.active_connections[room_id][user_id]
            if not self.active_connections[room_id]:
                del self.active_connections[room_id]

    async def broadcast(self, message: str, room_id: int, sender_id: int):
        """
        Рассылает сообщение всем пользователям в комнате.
        """
        if room_id in self.active_connections:
            for user_id, connection in self.active_connections[room_id].items():
                message_with_class = {
                    "text": message,
                    "is_self": user_id == sender_id
                }
                await connection.send_json(message_with_class)

Разбор кода

  1. Конструктор класса

    • self.active_connections — словарь, который хранит активные соединения, сгруппированные по комнатам (room_id).

    • В каждой комнате (room_id) подключенные пользователи хранятся в виде {user_id: WebSocket}.

  2. connect

    • Принимает WebSocket-соединение, идентификатор комнаты (room_id) и пользователя (user_id).

    • Подтверждает соединение (websocket.accept()).

    • Добавляет WebSocket в self.active_connections.

  3. disconnect

    • Удаляет WebSocket пользователя из self.active_connections.

    • Если в комнате не осталось пользователей, удаляет комнату.

  4. broadcast

    • Отправляет сообщение всем пользователям в комнате.

    • Дополнительно добавляет флаг is_self, чтобы клиент мог визуально выделять свои сообщения.

Инициализация менеджера соединений

Создадим экземпляр класса ConnectionManager, который будем использовать в дальнейшем:

manager = ConnectionManager()

Создание WebSocket-эндпоинта

Теперь создадим WebSocket-эндпоинт, который будет управлять подключениями пользователей и передачей сообщений в чате.

@router.websocket("/{room_id}/{user_id}")
async def websocket_endpoint(websocket: WebSocket, room_id: int, user_id: int, username: str):
    await manager.connect(websocket, room_id, user_id)
    await manager.broadcast(f"{username} (ID: {user_id}) присоединился к чату.", room_id, user_id)
    
    try:
        while True:
            data = await websocket.receive_text()
            await manager.broadcast(f"{username} (ID: {user_id}): {data}", room_id, user_id)
    except WebSocketDisconnect:
        manager.disconnect(room_id, user_id)
        await manager.broadcast(f"{username} (ID: {user_id}) покинул чат.", room_id, user_id)

Разбор кода

  1. Маршрут

    • Эндпоинт принимает три параметра из URL: room_id, user_id, username.

    • Каждый пользователь подключается по URL /ws/chat/{room_id}/{user_id}.

  2. Подключение

    • await manager.connect(...) — добавляет пользователя в список активных соединений.

    • await manager.broadcast(...) — уведомляет всех пользователей комнаты о новом участнике.

  3. Прием и передача сообщений

    • Бесконечный цикл (while True) слушает входящие сообщения через websocket.receive_text().

    • После получения сообщения оно рассылается всем пользователям комнаты через manager.broadcast(...).

  4. Отключение

    • Если соединение прерывается (WebSocketDisconnect), вызывается manager.disconnect(...).

    • Отправляется сообщение в чат о выходе пользователя.

В этом разделе мы создали серверную часть чата на WebSocket с использованием FastAPI. Мы:

  • Разобрались с импортами и маршрутизаторами.

  • Создали ConnectionManager для управления соединениями.

  • Написали WebSocket-эндпоинт для приема и рассылки сообщений.

В следующем разделе разберем, как подключить клиентскую часть и протестировать WebSocket-соединения.

Пишем клиентскую часть

Клиентская часть условно будет состоять из двух основных этапов:

  1. Описание эндпоинтов для рендеринга HTML-страниц

  2. Написание самой фронтенд-части: HTML + CSS + JS

В этом разделе мы опишем эндпоинты, которые будут обслуживать HTML-страницы. Реализуем их в файле app/router_page.py.

Импорты

Сначала импортируем необходимые модули:

from fastapi import APIRouter, Request, Form
from fastapi.templating import Jinja2Templates
from fastapi.responses import HTMLResponse
import random

Разберёмся, зачем нам каждый импорт:

  • APIRouter – позволяет организовать маршрутизацию в приложении.

  • Request – объект запроса, который передаётся в шаблон при рендеринге.

  • Form – используется для обработки данных, переданных через HTML-форму.

  • Jinja2Templates – нужен для работы с HTML-шаблонами.

  • HTMLResponse – указывает, что эндпоинт возвращает HTML-страницу.

  • Random - для генерации случайного ID пользователя

Инициализация роута и рендерера

Перед тем как описывать эндпоинты, создадим объект templates, который укажет, где хранятся HTML-шаблоны, и инициализируем роутер:

templates = Jinja2Templates(directory='app/templates')
router = APIRouter()

Описание эндпоинтов

Теперь создадим два эндпоинта. Первый будет отвечать за рендеринг главной страницы, а второй — за страницу комнаты (нашего группового чата).

Эндпоинт для главной страницы

@router.get("/", response_class=HTMLResponse)
async def home_page(request: Request):
    return templates.TemplateResponse("home.html", {"request": request})

Этот эндпоинт возвращает главную страницу home.html. Она будет содержать форму для входа в чат, которую разберём позже.

Эндпоинт для входа в чат

@router.post("/join_chat", response_class=HTMLResponse)
async def join_chat(request: Request, username: str = Form(...), room_id: int = Form(...)):
    # Простая генерация user_id
    user_id = random.randint(100, 100000)
    return templates.TemplateResponse("index.html",
                                      {"request": request,
                                       "room_id": room_id,
                                       "username": username,
                                       "user_id": user_id}
                                      )

Этот эндпоинт выполняет несколько задач:

  1. Получает username и room_id из формы (с помощью Form(...)).

  2. Генерирует случайный идентификатор пользователя в диапазоне от 100 до 100000

  3. Возвращает HTML-страницу index.html с переданными в шаблон параметрами:

    • room_id – ID комнаты, куда заходит пользователь.

    • username – имя пользователя.

    • user_id – сгенерированный идентификатор пользователя.

Зачем используется Form(...)?

В FastAPI существует несколько способов передавать данные в эндпоинт. В данном случае Form(...) указывает, что параметры username и room_id передаются через HTML-форму методом POST. Это удобно для обработки данных, введённых пользователем на веб-странице.

Если бы мы передавали данные в URL (как параметры запроса), нам пришлось бы использовать Query(...), а если бы передавали JSON – Body(...).

На этом этапе у нас готовы эндпоинты для работы с HTML-страницами. В следующем разделе мы реализуем фронтенд: создадим HTML-шаблоны, стили и подключим WebSocket.

Реализация фронтенда: HTML, CSS и JavaScript

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

  • HTML – для структуры страниц,

  • CSS (TailwindCSS) – для стилизации,

  • JavaScript – для обработки событий и работы с WebSocket.

1. Структура HTML

Мы создадим два основных HTML-файла:

  • home.html – главная страница с формой входа в чат.

  • index.html – страница чата, где будет происходить общение в реальном времени.

Главная страница (home.html)

<!DOCTYPE html>
<html lang="ru">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Вход в чат</title>
    <script src="https://cdn.tailwindcss.com"></script>
</head>
<body class="bg-gray-100 flex flex-col items-center justify-center min-h-screen p-4">
<h1 class="text-3xl font-bold mb-6">Добро пожаловать в чат</h1>

<form action="/join_chat" method="post" class="bg-white p-6 rounded-lg shadow-md w-full max-w-md">
    <label class="block text-gray-700">Введите ваше имя:</label>
    <input type="text" name="username" required
           class="w-full p-2 border border-gray-300 rounded-lg mt-2 focus:ring-2 focus:ring-blue-500">

    <label class="block text-gray-700 mt-4">Введите ID комнаты:</label>
    <input type="number" name="room_id" required min="1"
           class="w-full p-2 border border-gray-300 rounded-lg mt-2 focus:ring-2 focus:ring-blue-500">
    <button type="submit" class="w-full bg-blue-500 text-white px-4 py-2 rounded-lg mt-4 hover:bg-blue-600">Войти в
        чат
    </button>
</form>
</body>
</html>

Разбор кода

Этот шаблон делает следующее:

  1. Выводит заголовок с приветствием.

  2. Показывает форму входа, состоящую из двух полей:

    • Имя пользователя (username) – обычное текстовое поле.

    • ID комнаты (room_id) – поле для ввода числового идентификатора.

  3. После нажатия на кнопку "Войти в чат", данные отправляются методом POST на сервер (/join_chat).

Почему просто ввод ID комнаты?

В боевых проектах можно было бы сделать выбор комнаты из списка, но в нашем случае проще дать пользователю возможность самому ввести ID. Если комната существует – он подключится, если нет – создаст новую.

Страница чата (index.html)

<!DOCTYPE html>
<html lang="ru">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Чат на WebSocket - Комната {{ room_id }}</title>
    <script src="https://cdn.tailwindcss.com"></script>
</head>
<body class="bg-gray-100 flex flex-col items-center p-4">
<h1 class="text-2xl font-bold mb-4">Чат на WebSocket - Комната {{ room_id }}</h1>

<!-- Скрытый элемент для хранения данных комнаты -->
<div id="room-data"
     data-room-id="{{ room_id }}"
     data-username="{{ username }}"
     data-user-id="{{ user_id }}"
     class="hidden">
</div>

<!-- Область сообщений -->
<div id="messages"
     class="w-full max-w-lg h-96 overflow-y-auto border border-gray-300 bg-white p-4 rounded-lg shadow-md">
</div>

<!-- Поле ввода и кнопка -->
<div class="flex mt-4 w-full max-w-lg">
    <input id="messageInput"
           type="text"
           placeholder="Введите сообщение"
           class="flex-1 p-2 border border-gray-300 rounded-l-lg focus:outline-none focus:ring-2 focus:ring-blue-500"/>
    <button onclick="sendMessage()"
            class="bg-blue-500 text-white px-4 py-2 rounded-r-lg hover:bg-blue-600">Отправить
    </button>
</div>
<script src="/static/index.js"></script>
</body>
</html>

Разбор кода

Этот шаблон создаёт интерфейс чата:

  1. Выводит заголовок с номером комнаты ({{ room_id }}).

  2. Скрытый блок #room-data

    • Хранит данные, переданные сервером (room_id, username, user_id).

    • Сделан скрытым с помощью class="hidden".

    • В будущем JavaScript будет извлекать эти данные.

  3. Область сообщений (#messages) – здесь будут появляться сообщения пользователей.

  4. Поле ввода и кнопка "Отправить"

    • Поле (#messageInput) для ввода текста.

    • Кнопка onclick="sendMessage()" отправляет сообщение.

  5. Подключение index.js

    • В файле /static/index.js будет описана логика взаимодействия с WebSocket.

Зачем скрытый блок #room-data?

Передавать серверные переменные напрямую в JS-код не всегда удобно. С Jinja2 их можно вывести в HTML и затем прочитать их в JavaScript.

2. Подключение стилей

Мы используем TailwindCSS, который подключается через CDN:

<script src="https://cdn.tailwindcss.com"></script>

Это позволяет нам писать компактный и гибкий CSS-код прямо в HTML, например:

<body class="bg-gray-100 flex flex-col items-center p-4">

Почему TailwindCSS?

  • Избавляет от необходимости писать кастомные CSS-файлы.

  • Позволяет быстро стилизовать элементы.

  • Хорошо подходит для прототипирования.

3. Как страницы взаимодействуют с сервером

  1. Пользователь заходит на главную страницу сайта (/).

  2. Вводит имя и ID комнаты.

  3. Нажимает "Войти в чат" – запрос отправляется на /join_chat.

  4. Сервер возвращает index.html с переданными в него параметрами.

  5. Страница загружается, а JS-код начинает работу с WebSocket.

Что дальше?

Мы подготовили интерфейс, но пока чат не умеет отправлять сообщения. В следующем разделе реализуем JavaScript-логику, которая позволит подключаться к WebSocket, отправлять и получать сообщения.

Реализация WebSocket в JavaScript

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

Небольшое, но важное отступление

Хочу, чтобы у вас сложилось правильное понимание: реализация WebSocket, а особенно чатов, на стороне бэкенда — не такая уж сложная задача.

Для нас, бэкенд-разработчиков, достаточно написать несколько десятков строк кода, чтобы создать WebSocket-соединение, обрабатывать подключение клиентов и пересылать сообщения всем участникам чата, комнаты или, например, онлайн-игры.

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

Почему?

  • Помимо визуальной составляющей интерфейса, им нужно правильно установить соединение с сервером.

  • Обрабатывать события WebSocket: подключение, отключение, получение и отправку сообщений.

  • Организовать удобное и динамическое отображение сообщений в режиме реального времени.

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

Далее я покажу на простом примере, как реализовать WebSocket-соединение на JavaScript и как с ним работать на клиентской стороне.

Дальнейший код описываем в файле app/static/index.js.

1. Инициализация WebSocket

Вначале необходимо получить данные о пользователе и комнате. Так как мы передавали их через Jinja2 в скрытом HTML-блоке (#room-data), извлечём их с помощью JavaScript:

// Получаем данные из скрытого элемента
const roomData = document.getElementById("room-data");
const roomId = roomData.getAttribute("data-room-id");
const username = roomData.getAttribute("data-username");
const userId = roomData.getAttribute("data-user-id");

Эти данные понадобятся нам для установки WebSocket-соединения.

2. Установка WebSocket-соединения

Создадим соединение с сервером:

const ws = new WebSocket(`ws://localhost:8000/ws/chat/${roomId}/${userId}?username=${username}`);

На этапе деплоя на сервис Amvera Cloud нам предстоит заменить эту ссылку на боевой https домен.

Что происходит здесь?

  • Мы создаём WebSocket-соединение по адресу ws://localhost:8000/ws/chat/.

  • В URL передаём ID комнаты (roomId), ID пользователя (userId) и имя пользователя (username).

  • Сервер использует эти данные, чтобы идентифицировать подключение.

Дополнительно, мы добавляем обработчики событий, чтобы отслеживать состояние соединения:

ws.onopen = () => {
    console.log("Соединение установлено");
};

ws.onclose = () => {
    console.log("Соединение закрыто");
};

3. Получение сообщений

Когда сервер отправляет сообщение, срабатывает обработчик onmessage. Разбираем JSON-данные и добавляем их в область чата:

ws.onmessage = (event) => {
    const messages = document.getElementById("messages");
    const messageData = JSON.parse(event.data);
    const message = document.createElement("div");

    // Определяем стили в зависимости от отправителя
    if (messageData.is_self) {
        message.className = "p-2 my-1 bg-blue-500 text-white rounded-md self-end max-w-xs ml-auto";
    } else {
        message.className = "p-2 my-1 bg-gray-200 text-black rounded-md self-start max-w-xs";
    }

    message.textContent = messageData.text;
    messages.appendChild(message);
    messages.scrollTop = messages.scrollHeight; // Автопрокрутка вниз
};

Разбор кода:

  • JSON.parse(event.data) – преобразует JSON-строку в объект.

  • Если is_self == true, значит сообщение отправил текущий пользователь, и оно отображается справа (синим цветом).

  • Иначе – сообщение от другого пользователя, и оно отображается слева (серым цветом).

  • Автопрокрутка (messages.scrollTop = messages.scrollHeight) – чтобы новые сообщения всегда были видны.

4. Отправка сообщений

Создадим функцию для отправки сообщений:

function sendMessage() {
    const input = document.getElementById("messageInput");
    if (input.value.trim()) {
        ws.send(input.value);
        input.value = '';
    }
}

Как это работает?

  1. Берём текст из поля ввода (#messageInput).

  2. Если сообщение не пустое, отправляем его через ws.send().

  3. Очищаем поле ввода.

Дополнительно, чтобы отправлять сообщения по Enter, добавляем обработчик события:

document.getElementById("messageInput").addEventListener("keypress", (e) => {
    if (e.key === "Enter") {
        sendMessage();
    }
});

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

5. Полный цикл работы чата

  1. Подключаемся к WebSocket-серверу при загрузке страницы.

  2. Ждём входящие сообщения и отображаем их в окне чата.

  3. При отправке сообщения пользователь вводит текст и нажимает кнопку или Enter.

  4. Сервер передаёт сообщение всем участникам комнаты.

  5. Чат обновляется в реальном времени без перезагрузки страницы.

Благодаря WebSocket и небольшому количеству JavaScript-кода у нас получился полноценный чат, работающий в режиме реального времени.

Теперь нам остается настроить main-файл (файл запуска) и можно начинать тестировать наше веб-приложение.

Финальные настройки и запуск приложения

На этом этапе нам осталось выполнить финальные настройки и запустить наше приложение.

Работать будем с файлом app/main.py, где реализуем три ключевые задачи:

  • Инициализируем FastAPI-приложение

  • Добавим обработку статических файлов (например, JS, CSS)

  • Зарегистрируем все маршруты (роуты)

Настраиваем main.py

Добавляем нужные импорты и описываем базовую конфигурацию:

from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles
from app.api.router_page import router as router_page
from app.api.router_socket import router as router_socket

app = FastAPI()

# Подключаем папку со статическими файлами
app.mount('/static', StaticFiles(directory='app/static'), 'static')

# Регистрируем маршруты
app.include_router(router_socket)
app.include_router(router_page)

Так как наше веб-приложение само отвечает за рендеринг HTML-страниц, CORS-настройки нам не требуются.

Запуск приложения

Для старта используем uvicorn — это легковесный ASGI-сервер, который мы установили ранее.

Базовая команда:

uvicorn main:app

По умолчанию сервер поднимется на http://127.0.0.1:8000.

Если нужно настроить параметры запуска, добавляем флаги:

  • --host 0.0.0.0 – делает сервер доступным из внешней сети

  • --port 8005 – указывает конкретный порт

  • --reload – включает автоперезапуск при изменении кода (полезно в разработке)

Пример: Запуск на порту 8005 с автообновлением кода

uvicorn main:app --port 8005 --reload

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

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

Всё это замечательно, но общаться в чате с самим собой не так уж увлекательно. Было бы здорово сделать этот чат доступным для всех желающих. И в этом нам поможет сервис Amvera Cloud.

Почему Amvera?

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

  1. Зарегистрироваться на сервисе.

  2. Нажать на кнопку «Создать проект».

  3. Загрузить файлы проекта на сервис. Это можно сделать как через интерфейс на сайте, так и с помощью команд Git.

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

  5. Прикрепить к проекту бесплатное https-доменное имя или зарегистрировать собственное.

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

Выполним деплой.

  1. Регистрируемся на сайте Amvera Cloud, если регистрации ещё не было

  2. Кликаем на "Приложения"

  3. Затем выбираем "Создать приложение" и далее следуем пошаговой инструкции.

Даем имя проекту и выбираем тарифный план (для учебных целей подойдет пробный)
Даем имя проекту и выбираем тарифный план (для учебных целей подойдет пробный)
Загружаем файлы удобным способом
Загружаем файлы удобным способом
Заполняем конфигурации. Команда для запуска будет: uvicorn app.main:app --host 0.0.0.0 --port 8000
Заполняем конфигурации. Команда для запуска будет: uvicorn app.main:app --host 0.0.0.0 --port 8000

Далее, чтобы активировать бесплатный https-домен, нам необходимо перейти в созданный проект, затем перейти на вкладку "Домены" и активировать бесплатный домен, как показано ниже:

Теперь нам нужно внести некоторые изменения в файл static/index.js. В частности, мы должны заменить строку подключения к веб-сокетам.

Было:

const ws = new WebSocket(`ws://localhost:8000/ws/chat/${roomId}/${userId}?username=${username}`);

Стало:

const ws = new WebSocket(`wss://easysocketfastap-yakvenalex.amvera.io/ws/chat/${roomId}/${userId}username=${username}`);

Обратите внимание, что мы не только заменили ссылку на доменное имя, но и исправили формат с ws на wss. Будьте внимательны!

Теперь осталось перезаписать файл index.js в Amvera и пересобрать проект.

Работающий код можно посмотреть тут: https://easysocketfastap-yakvenalex.amvera.io. Полный исходный код проекта, как и прочий эксклюзивный контент, который я не публикую на Хабре вы найдете в моем бесплатном телеграмм канале "Легкий путь в Python".

К слову, я разобрал проект, о котором сегодня шла речь, в своём получасовом видео, доступном на YouTube и RuTube.

Заключение

Сегодняшний материал – это методическая основа, которая подготовит вас к разбору более сложного и масштабного проекта: Telegram-бота с MiniApp для анонимного чата "Тет-а-Тет". В следующей статье мы детально разберём этот проект, а знания, полученные здесь, помогут вам легче его освоить.

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

А если хотите ещё больше практики и полезных разборов, подписывайтесь на мой Telegram-канал «Легкий путь в Python» – там уже почти 3000 участников, и мы регулярно обсуждаем интересные темы по разработке.

На этом пока всё. До встречи в следующих материалах!

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Интересна ли вам тема монетизации своих приложений?
66.67% Конечно!6
11.11% Возможно…1
22.22% Нет!2
Проголосовали 9 пользователей. Воздержался 1 пользователь.
Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Будете ждать большую статью про разработку телеграмм бота с MiniApp (чат «Тет-А-Тет»)
70% Конечно!7
20% Возможно…2
10% Нет!1
Проголосовали 10 пользователей. Воздержались 2 пользователя.