
При помощи этого руководства вы научитесь добавлять функции чата в реальном времени в ваше веб-приложение Nestjs с использованием веб-сокетов. Мы создадим само приложение для чата, а также сохраним чаты пользователей в базе данных PostgreSQL.
Код этого туториала выложен в моём репозитории GitHub, можете клонировать его.
▍ Что такое NestJS?
NestJS — это фреймворк Node.js для создания быстрых, тестируемых, масштабируемых, слабосвязанных серверных приложений, использующих TypeScript. Он использует такие мощные фреймворки HTTP-серверов, как Express или Fastify. Nest добавляет слой абстракции фреймворкам Node.js и открывает их API разработчикам. Он поддерживает такие системы управления базами данных, как PostgreSQL и MySQL. Также NestJS обеспечивает инъекции зависимостей Websockets и APIGetaways.
▍ Что такое a WebSocket?
WebSocket — это компьютерный протокол связи, обеспечивающий полнодуплексные каналы связи по одному TCP-соединению. В 2011 году IETF стандартизировал протокол WebSocket как RFC 6455. Текущая спецификация называется HTML Living Standard. В отличие от HTTP/HTTPS, WebSockets — это stateful-протоколы, то есть установленное между сервером и клиентом соединение будет существовать, пока его не прервёт сервер или клиент; как только соединение WebSocket закрывается одной стороной, это распространяется на другую сторону.
▍ Необходимые требования
Этот туториал предназначен для практической демонстрации. Чтобы повторять его этапы, вам нужно установить следующее:
▍ Подготовка проекта
Прежде чем приступать к кодингу, давайте подготовим проект NestJS и структуру проекта. Начнём мы с создания папки проекта. Откроем терминал и выполним следующую команду:
mkdir chatapp && cd chatapp
Далее установим NestJS CLI при помощи следующей команды:
npm i -g @nestjs/cli
После завершения установки выполните следующую команду для подготовки проекта NestJS.
nest new chat
Выберите удобный вам менеджер пакетов. В этом туториале мы будем использовать npm и подождём, пока установятся необходимые пакеты. После завершения установки установите WebSocket и Socket.io при помощи такой команды:
npm i --save @nestjs/websockets @nestjs/platform-socket.io
Затем создайте приложение-шлюз:
nest g gateway app
Теперь давайте запустим наш сервер:
npm run start:dev
▍ Настройка базы данных Postgres
Теперь мы можем подготовить базу данных Postgres для хранения записей пользователей на сервере. Для начала мы воспользуемся TypeORM (Object Relational Mapper) для подключения базы данных к нашему приложению. Сначала нам нужно создать базу данных. Переключимся на аккаунт пользователя Postgres.
sudo su - postgres
Далее создадим новый аккаунт пользователя:
createuser --interactive
Теперь создадим новую базу данных. Это можно сделать при помощи следующей команды:
createdb chat
Теперь мы подключим только что созданную базу данных. Для начала откроем файл the app.module.ts и добавим в массив imports[] следующий фрагмент кода:
... import { TypeOrmModule } from '@nestjs/typeorm'; import { Chat } from './chat.entity'; imports: [ TypeOrmModule.forRoot({ type: 'postgres', host: 'localhost', username: '<USERNAME>', password: '<PASSWORD>', database: 'chat', entities: [Chat], synchronize: true, }), TypeOrmModule.forFeature([Chat]), ], ...
В этом фрагменте кода мы подключили наше приложение к базе данных PostgresSQL при помощи метода TypeOrmModule forRoot и передали учётные данные базы данных. Замените <USERNAME> и <PASSWORD> на пользователя и пароль, которые вы создали для базы данных chat.
▍ Создаём первый элемент чата
После того как мы подключили приложение к базе данных, создадим элемент чата для хранения сообщений пользователя. Для этого создадим файл chat.entity.ts в папке
src и добавим следующий фрагмент кода:import { Entity, Column, PrimaryGeneratedColumn, CreateDateColumn, } from 'typeorm'; @Entity() export class Chat { @PrimaryGeneratedColumn('uuid') id: number; @Column() email: string; @Column({ unique: true }) text: string; @CreateDateColumn() createdAt: Date; }
В приведённом выше фрагменте кода мы создали столбцы для наших чатов при помощи декораторов Entity, Column, CreatedDateColumn и PrimaryGenerateColumn, предоставленных TypeOrm.
▍ Настройка WebSocket
Давайте настроим подключение WebSocket к нашему серверу для отправки сообщений в реальном времени. Сначала мы импортируем требуемый модуль:
import { SubscribeMessage, WebSocketGateway, OnGatewayInit, WebSocketServer, OnGatewayConnection, OnGatewayDisconnect, } from '@nestjs/websockets'; import { Socket, Server } from 'socket.io'; import { AppService } from './app.service'; import { Chat } from './chat.entity';
В этом фрагменте кода мы импортировали SubscribeMessage(), чтобы прослушивать события от клиента, и WebSocketGateway(), дающий доступ к socket.io; также мы импортировали экземпляры OnGatewayInit, OnGatewayConnection и OnGatewayDisconnect. Этот экземпляр WebSocket позволяет узнавать состояние приложения. Например, можно выполнять действия на сервере, когда он подключается и отключается от чата. Затем мы импортировали элемент Chat и AppService, раскрывающий методы, которые необходимы для сохранения сообщений пользователя.
@WebSocketGateway({ cors: { origin: '*', }, })
Чтобы клиент мог обмениваться данными с сервером, мы включим CORS, инициализировав WebSocketGateway.
export class AppGateway implements OnGatewayInit, OnGatewayConnection, OnGatewayDisconnect { constructor(private appService: AppService) {} @WebSocketServer() server: Server; @SubscribeMessage('sendMessage') async handleSendMessage(client: Socket, payload: Chat): Promise<void> { await this.appService.createMessage(payload); this.server.emit('recMessage', payload); } afterInit(server: Server) { console.log(server); //Выполняем действия } handleDisconnect(client: Socket) { console.log(`Disconnected: ${client.id}`); //Выполняем действия } handleConnection(client: Socket, ...args: any[]) { console.log(`Connected ${client.id}`); //Выполняем действия } }
Далее в классе AppGateWay мы реализуем импортированные выше экземпляры WebSocket. Мы создали метод конструктора и привязываем AppService, чтобы он имел доступ к его методам. Далее мы создали инстанс сервера из декораторов WebSocketServer.
Далее мы создаём handleSendMessage при помощи экземпляра @SubscribeMessage() и метода handleMessage() для отправки данных на сторону клиента.
Когда из клиента этой функции отправляется сообщение, мы сохраняем его в базу данных и передаём сообщение обратно всем подключенным пользователям на стороне клиента. Также у нас есть множество других методов, с которыми можно экспериментировать, например, afterInit, который вызывается при подключении пользователя, и handleDisconnect, вызываемый при отключении пользователя. Метод handleConnection запускается, когда пользователь устанавливает соединение.
▍ Создание контроллера/сервиса
Теперь давайте создадим сервис и контроллер для сохранения чата и рендеринга статической страницы. Откройте файл app.service.ts и дополните его содержимое следующим фрагментом кода:
import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { Chat } from './chat.entity'; @Injectable() export class AppService { constructor( @InjectRepository(Chat) private chatRepository: Repository<Chat>, ) {} async createMessage(chat: Chat): Promise<Chat> { return await this.chatRepository.save(chat); } async getMessages(): Promise<Chat[]> { return await this.chatRepository.find(); } }
Затем дополним файл app.controller.ts таким кодом:
import { Controller, Render, Get, Res } from '@nestjs/common'; import { AppService } from './app.service'; import { Chat } from './chat.entity'; @Controller() export class AppController { constructor(private readonly appService: AppService) {} @Get('/chat') @Render('index') Home() { return; } @Get('/api/chat') async Chat(@Res() res) { const messages = await this.appService.getMessages(); res.json(messages); } }
В показанном выше фрагменте кода мы создали два маршрута для рендеринга статической страницы и сообщений пользователя.
▍ Передача статической страницы
Давайте теперь сконфигурируем приложение для рендеринга статического файла и страниц. Для этого мы реализуем рендеринг на стороне сервера. Сначала в файле main.ts сконфигурируем приложение для статических файлов сервера следующей командой:
async function bootstrap() { ... app.useStaticAssets(join(__dirname, '..', 'static')); app.setBaseViewsDir(join(__dirname, '..', 'views')); app.setViewEngine('ejs'); ... }
Далее создадим в каталоге src папки static и views. В папке
views создадим файл index.ejs и добавим в него следующий код:<!DOCTYPE html> <html lang="en"> <head> <!-- Required meta tags --> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <!-- Bootstrap CSS --> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous" /> <title>Let Chat</title> </head> <body> <nav class="navbar navbar-light bg-light"> <div class="container-fluid"> <a class="navbar-brand">Lets Chat</a> </div> </nav> <div class="container"> <div class="mb-3 mt-3"> <ul style="list-style: none" id="data-container"></ul> </div> <div class="mb-3 mt-4"> <input class="form-control" id="email" rows="3" placeholder="Your Email" /> </div> <div class="mb-3 mt-4"> <input class="form-control" id="exampleFormControlTextarea1" rows="3" placeholder="Say something..." /> </div> </div> <script src="https://cdn.socket.io/4.3.2/socket.io.min.js" integrity="sha384-KAZ4DtjNhLChOB/hxXuKqhMLYvx3b5MlT55xPEiNmREKRzeEm+RVPlTnAn0ajQNs" crossorigin="anonymous"></script> <script src="app.js"></script> <!-- Option 1: Bootstrap Bundle with Popper --> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM" crossorigin="anonymous"></script> </body> </html>
Чтобы ускорить работу шаблонов, мы использовали Bootstrap для добавления стилизации. Затем мы добавили два поля ввода и неупорядоченный список для отображения сообщений пользователя. Также мы добавили файл app.js, который будет создаваться позже в этом разделе, и ссылку на клиент socket.io.
Теперь создадим файл app.js и добавим следующий фрагмент кода:
const socket = io('http://localhost:3002'); const msgBox = document.getElementById('exampleFormControlTextarea1'); const msgCont = document.getElementById('data-container'); const email = document.getElementById('email'); //Получаем старые сообщения с сервера const messages = []; function getMessages() { fetch('http://localhost:3002/api/chat') .then((response) => response.json()) .then((data) => { loadDate(data); data.forEach((el) => { messages.push(el); }); }) .catch((err) => console.error(err)); } getMessages(); //Когда пользователь нажимает клавишу enter key, отправляем сообщение. msgBox.addEventListener('keydown', (e) => { if (e.keyCode === 13) { sendMessage({ email: email.value, text: e.target.value }); e.target.value = ''; } }); //Отображаем сообщения пользователям function loadDate(data) { let messages = ''; data.map((message) => { messages += ` <li class="bg-primary p-2 rounded mb-2 text-light"> <span class="fw-bolder">${message.email}</span> ${message.text} </li>`; }); msgCont.innerHTML = messages; } //socket.io //Создаём событие sendMessage, чтобы передать сообщение function sendMessage(message) { socket.emit('sendMessage', message); } //Слушаем событие recMessage, чтобы получать сообщения, отправленные пользователями socket.on('recMessage', (message) => { messages.push(message); loadDate(messages); })
В приведённом выше фрагменте кода мы создали экземпляр socket.io и слушаем события на сервере, чтобы отправлять и получать сообщение с сервера. По умолчанию нам нужно, чтобы старые чаты становились доступными, когда пользователь присоединяется к чату. Наше приложение должно выглядеть как на скриншоте:

▍ Просмотр данных пользователей при помощи Arctype
Мы успешно создали чат-приложение. Для начала давайте просмотрим данные пользователей при помощи Arctype. Сначала запустим Arctype, нажмём на вкладку PostgreSQL и введём следующие учётные данные PostgreSQL:

Затем нажмём на chattable, чтобы просмотреть сообщения чата пользователя:

▍ Тестирование приложения
Теперь откроем приложение в двух вкладках или окнах, и попытаемся отправить сообщение с другим адресом электронной почты:

Если взглянуть на консоль, то мы увидим логи того, как пользователь подключается и отключается от сервера, этим занимаются методы handleDisconnect и handleConnection.
▍ Заключение
В этом туториале мы узнали, как создать приложение чата реального времени с помощью Nestjs и PostgreSQL. Мы начали с краткого введения в Nestjs и WebSockets. Затем создали демо-приложение для демонстрации реализации. Надеюсь, вы получили необходимую информацию. Подробнее о реализации WebSocket можно узнать из документации Nestjs, это позволит вам расширить возможности приложения.

