При помощи этого руководства вы научитесь добавлять функции чата в реальном времени в ваше веб-приложение 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, это позволит вам расширить возможности приложения.

Комментарии (3)


  1. donpadlo
    22.06.2022 17:16

    С раздела "Просмотр данных пользователей при помощи Arctype" идет про MySQL вместо PostgreSQL опечатка или так и задумано?


    1. mc2
      23.06.2022 04:06

      В оригинале так указано. А вот переводчик не оценил это.


  1. aio350
    24.06.2022 20:22
    +1

    Кое-что упущено:
    1. Для того, чтобы иметь возможность использовать useStaticAssets и другие методы для обработки статики, приложение Nest необходимо создать следующим образом:
    import { NestExpressApplication } from '@nestjs/platform-express'
    const app = await NestFactory.create<NestExpressApplication>(AppModule)
    2. Для того, чтобы рендерить статику с помощью EJS, требуется установить соответствующий пакет: npm i ejs
    3. Для того, чтобы открыть сокет на порту 3002, приложение Nest должно быть запущено на этом порту:
    await app.listen(3002)
    4. Директории views и static должны находиться в корне приложения, а не в директории src.
    5. Файл app.js должен находиться в директории static.