Привет, друзья!
В данном туториале мы разработаем чат с использованием следующих технологий:
- TypeScript — статический типизатор;
- NestJS — сервер;
- Socket.IO — библиотека для работы в [веб-сокетами]();
- React — клиент;
- TailwindCSS — библиотека для стилизации;
- PostgreSQL — база данных (далее — БД);
- Prisma — ORM;
- Docker — платформа для разработки, доставки и запуска приложений в изолированной среде — контейнере.
Функционал чата будет таким:
- фейковая регистрация пользователей:
- хранение имен пользователей в памяти (объекте) на сервере;
- хранение имен и идентификаторов пользователей в localStorage на клиенте;
- регистрация подключений и отключений пользователей на сервере и передача этой информации подключенным клиентам;
- запись, обновление и удаление сообщений из БД в реальном времени на сервере и передача этой информации клиентам.
Если вам это интересно, прошу под кат.
Материалы для изучения (опционально):
- Карманная книга по TS;
- Шпаргалка по TS;
- Шпаргалка по React + TS;
- Руководство по NestJS;
- Руководство по Socket.IO;
- Руководство по Prisma;
- Руководство по Docker.
Полезные расширения для VSCode (опционально):
- Docker;
- ES7+ React/Redux/React-Native/JS snippets;
- ESLint;
- Prettier — Code formatter;
- Prisma;
- Tailwind CSS IntelliSense.
Подготовка и настройка проекта
Обратите внимание: для успешного прохождения туториала на вашей машине должны быть установлены Node.js и Docker.
Для работы с зависимостями будет использоваться Yarn.
Создаем директорию, переходим в нее и инициализируем Node.js-проект
:
mkdir react-nest-postgres-chat
cd react-nest-postgres-chat
yarn init -yp
База данных
Создаем файл docker-compose.yml
следующего содержания:
services:
# название сервиса
postgres:
# образ
image: postgres
# политика перезапуска
restart: on-failure
# файл с переменными среды окружения
env_file:
- .env
# порты
ports:
- 5432:5432
# тома для постоянного хранения данных
volumes:
- postgres-data:/var/lib/postgresql/data
volumes:
postgres-data:
Создаем файл .env
и определяем в нем переменные среды окружения для Postgres
:
POSTGRES_USER=postgres
POSTGRES_PASSWORD=postgres
POSTGRES_DB=chat
Запускаем сервер Postgres
в контейнере:
docker compose up -d
Это приводит к созданию и запуску контейнера react-nest-postgres-chat-1
с сервером Postgres
в сервисе react-nest-postgres-chat
.
БД доступна по адресу http://localhost:5432/chat
.
ORM
Устанавливаем Prisma
в качестве зависимости для разработки и инициализируем ее:
yarn add -D prisma
prisma init
Это приводит к созданию файла prisma/schema.prisma
. Определяем в нем модель сообщения:
model Message {
id Int @id @default(autoincrement())
userId String
userName String
text String
createdAt DateTime @default(now())
}
Определяем переменную со строкой для подключения к БД в файле .env
:
DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@localhost:5432/${POSTGRES_DB}?schema=public
Выполняем миграцию:
yarn prisma migrate dev --name init
Это приводит к созданию директории migrations
с выполненной миграцией в формате SQL.
Устанавливаем клиента Prisma
и генерируем типы:
yarn add @prisma/client
yarn prisma generate
Сервер и клиент
Глобально устанавливаем Nest CLI и инициализируем Nest-проект
:
yarn global add @nestjs/cli
# выбираем `yarn` для работы с зависимостями
nest new server
Создаем шаблон React + TS
приложения с помощью create-vite:
# client - название приложения (и директории)
# react-ts - используемый шаблон
yarn create vite client --template react-ts
Устанавливаем concurrently:
yarn add concurrently
Определяем команду для одновременного запуска сервера и клиента в режиме разработки в файле package.json
:
"scripts": {
"dev:client": "yarn --cwd client dev",
"dev:server": "yarn --cwd server start:dev",
"dev": "concurrently \"yarn dev:client\" \"yarn dev:server\""
}
Работоспособность приложения можно проверить, выполнив команду yarn dev
.
На этом подготовка и настройка проекта завершены. Переходим к разработке сервера.
Сервер
Переходим в директорию сервера и устанавливаем модули для работы с сокетами:
cd server
yarn add @nestjs/websockets @nestjs/platform-socket.io
Генерируем шлюз (gateway) для модуля App
:
# g ga - generate gateway
nest g ga app
Приводим файлы сервера к следующей структуре:
- src
- app.gateway.ts
- app.module.ts
- app.service.ts
- main.ts
- prisma.service.ts
- constants.ts
- types.ts
- ...
Определяем сервис Prisma
в файле prisma.service.ts
:
import { INestApplication, Injectable, OnModuleInit } from "@nestjs/common";
import { PrismaClient } from "@prisma/client";
@Injectable()
export class PrismaService extends PrismaClient implements OnModuleInit {
async onModuleInit() {
await this.$connect();
}
async enableShutdownHooks(app: INestApplication) {
this.$on("beforeExit", async () => {
await app.close();
});
}
}
Настраиваем данный сервис в файле main.ts
:
import { NestFactory } from "@nestjs/core";
import { AppModule } from "./app.module";
// !
import { PrismaService } from "./prisma.service";
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// !
const prismaService = app.get(PrismaService);
await prismaService.enableShutdownHooks(app);
// обратите внимание на порт
await app.listen(3001);
}
bootstrap();
И подключаем его в качестве провайдера в модуле App
(app.module.ts
):
import { Module } from "@nestjs/common";
import { AppService } from "./app.service";
// !
import { PrismaService } from "./prisma.service";
@Module({
imports: [],
controllers: [],
// !
providers: [PrismaService, AppService]
})
export class AppModule {}
Определяем адрес клиента в файле constants.ts
:
export const CLIENT_URI = "http://localhost:3000";
И тип полезной нагрузки для обновления сообщения в файле types.ts
:
import { Prisma } from "@prisma/client";
// { id?: number, text?: string }
export type MessageUpdatePayload = Prisma.MessageWhereUniqueInput &
Pick<Prisma.MessageUpdateInput, "text">;
Определяем методы для работы с сообщениями в файле app.service.ts
:
import { Injectable } from "@nestjs/common";
import { Message, Prisma } from "@prisma/client";
import { MessageUpdatePayload } from "types";
import { PrismaService } from "./prisma.service";
@Injectable()
export class AppService {
// инициализация сервиса `Prisma`
constructor(private readonly prisma: PrismaService) {}
// получение всех сообщений
async getMessages(): Promise<Message[]> {
return this.prisma.message.findMany();
}
// удаление всех сообщений - для отладки в процессе разработки
async clearMessages(): Promise<Prisma.BatchPayload> {
return this.prisma.message.deleteMany();
}
// создание сообщения
async createMessage(data: Prisma.MessageCreateInput) {
return this.prisma.message.create({ data });
}
// обновление сообщения
async updateMessage(payload: MessageUpdatePayload) {
const { id, text } = payload;
return this.prisma.message.update({ where: { id }, data: { text } });
}
// удаление сообщения
async removeMessage(where: Prisma.MessageWhereUniqueInput) {
return this.prisma.message.delete({ where });
}
}
Осталось реализовать обработку событий сокетов в файле app.gateway.ts
.
Импортируем зависимости и прочее, а также определяем переменную для хранения записей "идентификатор сокета — имя пользователя":
import {
MessageBody,
OnGatewayConnection,
OnGatewayDisconnect,
OnGatewayInit,
SubscribeMessage,
WebSocketGateway,
WebSocketServer
} from "@nestjs/websockets";
import { Prisma } from "@prisma/client";
import { Server, Socket } from "Socket.IO";
import { MessageUpdatePayload } from "types";
import { CLIENT_URI } from "../constants";
import { AppService } from "./app.service";
const users: Record<string, string> = {};
Инициализируем сокет-соединение, определяем шлюз App
и инициализируем в нем сервис App
:
@WebSocketGateway({
cors: {
origin: CLIENT_URI // можно указать `*` для отключения `CORS`
},
serveClient: false,
// название пространства может быть любым, но должно учитываться на клиенте
namespace: "chat"
})
export class AppGateway
implements OnGatewayInit, OnGatewayConnection, OnGatewayDisconnect
{
constructor(private readonly appService: AppService) {}
/**
* @todo
*/
}
Инициализируем сокет-сервер и регистрируем инициализацию:
@WebSocketServer() server: Server;
afterInit(server: Server) {
console.log(server);
}
Обрабатываем подключение и отключение клиентов:
// подключение
handleConnection(client: Socket, ...args: any[]) {
// обратите внимание на структуру объекта `handshake`
const userName = client.handshake.query.userName as string;
const socketId = client.id;
users[socketId] = userName;
// передаем информацию всем клиентам, кроме текущего
client.broadcast.emit("log", `${userName} connected`);
}
// отключение
handleDisconnect(client: Socket) {
const socketId = client.id;
const userName = users[socketId];
delete users[socketId];
client.broadcast.emit("log", `${userName} disconnected`);
}
Наконец, обрабатываем события сокетов:
// получение всех сообщений
@SubscribeMessage("messages:get")
async handleMessagesGet(): Promise<void> {
const messages = await this.appService.getMessages();
this.server.emit("messages", messages);
}
// удаление всех сообщений
@SubscribeMessage("messages:clear")
async handleMessagesClear(): Promise<void> {
await this.appService.clearMessages();
}
// создание сообщения
@SubscribeMessage("message:post")
async handleMessagePost(
@MessageBody()
payload: // { userId: string, userName: string, text: string }
Prisma.MessageCreateInput
): Promise<void> {
const createdMessage = await this.appService.createMessage(payload);
// можно сообщать клиентам о каждой операции по отдельности
this.server.emit("message:post", createdMessage);
// но мы пойдем более простым путем
this.handleMessagesGet();
}
// обновление сообщения
@SubscribeMessage("message:put")
async handleMessagePut(
@MessageBody()
payload: // { id: number, text: string }
MessageUpdatePayload
): Promise<void> {
const updatedMessage = await this.appService.updateMessage(payload);
this.server.emit("message:put", updatedMessage);
this.handleMessagesGet();
}
// удаление сообщения
@SubscribeMessage("message:delete")
async handleMessageDelete(
@MessageBody()
payload: // { id: number }
Prisma.MessageWhereUniqueInput
) {
const removedMessage = await this.appService.removeMessage(payload);
this.server.emit("message:delete", removedMessage);
this.handleMessagesGet();
}
import {
MessageBody,
OnGatewayConnection,
OnGatewayDisconnect,
OnGatewayInit,
SubscribeMessage,
WebSocketGateway,
WebSocketServer
} from "@nestjs/websockets";
import { Prisma } from "@prisma/client";
import { Server, Socket } from "socket.io";
import { MessageUpdatePayload } from "types";
import { CLIENT_URI } from "../constants";
import { AppService } from "./app.service";
const users: Record<string, string> = {};
@WebSocketGateway({
cors: {
origin: CLIENT_URI
},
serveClient: false,
namespace: "chat"
})
export class AppGateway
implements OnGatewayInit, OnGatewayConnection, OnGatewayDisconnect
{
constructor(private readonly appService: AppService) {}
@WebSocketServer() server: Server;
@SubscribeMessage("messages:get")
async handleMessagesGet(): Promise<void> {
const messages = await this.appService.getMessages();
this.server.emit("messages", messages);
}
@SubscribeMessage("messages:clear")
async handleMessagesClear(): Promise<void> {
await this.appService.clearMessages();
}
@SubscribeMessage("message:post")
async handleMessagePost(
@MessageBody()
payload: // { userId: string, userName: string, text: string }
Prisma.MessageCreateInput
): Promise<void> {
const createdMessage = await this.appService.createMessage(payload);
this.server.emit("message:post", createdMessage);
this.handleMessagesGet();
}
@SubscribeMessage("message:put")
async handleMessagePut(
@MessageBody()
payload: // { id: number, text: string }
MessageUpdatePayload
): Promise<void> {
const updatedMessage = await this.appService.updateMessage(payload);
this.server.emit("message:put", updatedMessage);
this.handleMessagesGet();
}
@SubscribeMessage("message:delete")
async handleMessageDelete(
@MessageBody()
payload: // { id: number }
Prisma.MessageWhereUniqueInput
) {
const removedMessage = await this.appService.removeMessage(payload);
this.server.emit("message:delete", removedMessage);
this.handleMessagesGet();
}
afterInit(server: Server) {
console.log(server);
}
handleConnection(client: Socket, ...args: any[]) {
const userName = client.handshake.query.userName as string;
const socketId = client.id;
users[socketId] = userName;
client.broadcast.emit("log", `${userName} connected`);
}
handleDisconnect(client: Socket) {
const socketId = client.id;
const userName = users[socketId];
delete users[socketId];
client.broadcast.emit("log", `${userName} disconnected`);
}
}
Подключаем шлюз в качестве провайдера в модуле App
(app.module.ts
):
import { Module } from "@nestjs/common";
import { AppService } from "./app.service";
// !
import { AppGateway } from "./app.gateway";
import { PrismaService } from "./prisma.service";
@Module({
imports: [],
controllers: [],
// !
providers: [PrismaService, AppService, AppGateway]
})
export class AppModule {}
На этом разработка сервера завершена. Переходим к разработке клиента.
Клиент
Переходим в директорию клиента и инициализируем Tailwind
:
cd client
yarn add -D tailwindcss postcss autoprefixer
yarn tailwindcss init -p
Редактируем файл tailwind.config.js
:
/** @type {import('tailwindcss').Config} */
module.exports = {
// !
content: ['./src/**/*.{ts,tsx}'],
theme: {
extend: {}
},
plugins: []
}
Добавляем директивы @tailwind
в файл App.css
:
@tailwind base;
@tailwind components;
@tailwind utilities;
Переопределяем дефолтный шрифт и добавляем несколько переиспользуемых стилей (reused styles):
@layer base {
html {
font-family: "Montserrat", sans-serif;
}
}
@layer components {
#root {
width: 100vw;
height: 100vh;
overflow: hidden;
}
.title {
@apply mb-4 text-2xl text-center font-bold;
}
.btn {
@apply py-2 px-4 text-white rounded-md shadow-md focus:outline-none focus:ring-2 focus:ring-opacity-75 transition-all duration-150;
}
.btn-primary {
@apply btn bg-blue-500 hover:bg-blue-600 focus:ring-blue-400;
}
.btn-success {
@apply btn bg-green-500 hover:bg-green-600 focus:ring-green-400;
}
.btn-error {
@apply btn bg-red-500 hover:bg-red-600 focus:ring-red-400;
}
.input {
@apply py-2 px-4 border border-blue-500 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-400 transition-all duration-150;
}
}
Подключаем шрифт в файле index.html
:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="data:." />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>React Nest Postgres Chat</title>
<!-- ! -->
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Montserrat&display=swap"
rel="stylesheet"
/>
<!-- ! -->
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
Устанавливаем несколько библиотек:
yarn add socket.io-client react-icons react-timeago react-toastify
-
socket.io-client — клиент
Socket.IO
; - react-icons — иконки в виде компонентов;
- react-timeago — компонент для форматирования даты и времени с обновлением в реальном времени;
- react-toastify — библиотека для реализации всплывающих уведомлений.
Приводим файлы клиента к следующей структуре:
- src
- components
- ChatScreen.tsx
- index.ts
- WelcomeScreen.tsx
- hooks
- useChat.ts
- App.css
- App.tsx
- constants.ts
- main.ts
- types.ts
- utils.ts
- postcss.config.js
- tailwind.config.js
- ...
Определяем константы в файле constants.ts
:
// ключ для `localStorage`
export const USER_INFO = "user-info";
// адрес шлюза на сервере
export const SERVER_URI = "http://localhost:3001/chat";
Определяем типы в файле types.ts
:
import { Prisma } from "@prisma/client";
export type UserInfo = {
userId: string;
userName: string;
};
export type MessageUpdatePayload = Prisma.MessageWhereUniqueInput &
Pick<Prisma.MessageUpdateInput, "text">;
И утилиты в файле utils.ts
:
// утилита для генерации идентификатора пользователя
export const getId = () => Math.random().toString(36).slice(2);
// утилита для работы с `localStorage`
export const storage = {
set<T>(key: string, value: T) {
localStorage.setItem(key, JSON.stringify(value));
},
get<T>(key: string): T | null {
return localStorage.getItem(key)
? JSON.parse(localStorage.getItem(key) as string)
: null;
}
};
Начнем, пожалуй, с основного компонента приложения (App.tsx
):
import "react-toastify/dist/ReactToastify.css";
import "./App.css";
import { ChatScreen, WelcomeScreen } from "./components";
import { USER_INFO } from "./constants";
import { UserInfo } from "./types";
import { storage } from "./utils";
function App() {
const userInfo = storage.get<UserInfo>(USER_INFO);
return (
<section className="w-[480px] h-full mx-auto flex flex-col py-4">
{userInfo ? <ChatScreen /> : <WelcomeScreen />}
</section>
);
}
export default App;
Если в локальном хранилище содержится информация о пользователе, рендерится экран чата. В противном случае, рендерится экран приветствия.
Экран приветствия (ChatScreen.tsx
):
import React, { useState } from "react";
import { FiUser } from "react-icons/fi";
import { USER_INFO } from "../constants";
import { UserInfo } from "../types";
import { getId, storage } from "../utils";
export const WelcomeScreen = () => {
const [userName, setUserName] = useState("");
const changeUserName = (e: React.ChangeEvent<HTMLInputElement>) => {
setUserName(e.target.value);
};
const setUserInfo = (e: React.FormEvent) => {
e.preventDefault();
const trimmed = userName.trim();
if (!trimmed) return;
// генерируем идентификатор пользователя
const userId = getId();
// сохраняем информацию о пользователе в локальном хранилище
storage.set<UserInfo>(USER_INFO, { userName: trimmed, userId });
// и перезагружаем локацию
location.reload();
};
return (
<section>
<h1 className="title">Welcome, friend!</h1>
<form onSubmit={setUserInfo} className="flex flex-col items-center gap-4">
<div className="flex flex-col gap-2">
<label
htmlFor="username"
className="text-lg flex items-center justify-center"
>
<span className="mr-1">
<FiUser />
</span>
<span>What is your name?</span>
</label>
<input
type="text"
id="username"
name="userName"
value={userName}
onChange={changeUserName}
required
autoComplete="off"
className="input"
/>
</div>
<button className="btn-success">Start chat</button>
</form>
</section>
);
};
Инкапсулируем логику обработки событий сокетов в кастомном хуке (useChat.ts
):
import { Message, Prisma } from "@prisma/client";
import { useCallback, useEffect, useMemo, useState } from "react";
import { io, Socket } from "Socket.IO-client";
import { SERVER_URI, USER_INFO } from "../constants";
import { MessageUpdatePayload, UserInfo } from "../types";
import { storage } from "../utils";
// экземпляр сокета
let socket: Socket;
export const useChat = () => {
const userInfo = storage.get<UserInfo>(USER_INFO) as UserInfo;
// это важно: один пользователь - один сокет
if (!socket) {
socket = io(SERVER_URI, {
// помните сигнатуру объекта `handshake` на сервере?
query: {
userName: userInfo.userName
}
});
}
const [messages, setMessages] = useState<Message[]>();
const [log, setLog] = useState<string>();
useEffect(() => {
// подключение/отключение пользователя
socket.on("log", (log: string) => {
setLog(log);
});
// получение сообщений
socket.on("messages", (messages: Message[]) => {
setMessages(messages);
});
socket.emit("messages:get");
}, []);
// отправка сообщения
const send = useCallback((payload: Prisma.MessageCreateInput) => {
socket.emit("message:post", payload);
}, []);
// обновление сообщения
const update = useCallback((payload: MessageUpdatePayload) => {
socket.emit("message:put", payload);
}, []);
// удаление сообщения
const remove = useCallback((payload: Prisma.MessageWhereUniqueInput) => {
socket.emit("message:delete", payload);
}, []);
// очистка сообщения - для отладки при разработке
// можно вызывать в консоли браузера, например
window.clearMessages = useCallback(() => {
socket.emit("messages:clear");
location.reload();
}, []);
// операции
const chatActions = useMemo(
() => ({
send,
update,
remove
}),
[]
);
return { messages, log, chatActions };
};
Наконец, экран чата (ChatScreen.tsx
):
import React, { useEffect, useState } from "react";
import { FiEdit2, FiSend, FiTrash } from "react-icons/fi";
import { MdOutlineClose } from "react-icons/md";
import TimeAgo from "react-timeago";
import { Slide, toast, ToastContainer } from "react-toastify";
import { USER_INFO } from "../constants";
import { useChat } from "../hooks/useChat";
import { UserInfo } from "../types";
import { storage } from "../utils";
// уведомление о подключении/отключении пользователя
const notify = (message: string) =>
toast.info(message, {
position: "top-left",
autoClose: 1000,
hideProgressBar: true,
transition: Slide
});
export const ChatScreen = () => {
const userInfo = storage.get<UserInfo>(USER_INFO) as UserInfo;
const { userId, userName } = userInfo;
// получаем сообщения, лог и операции
const { messages, log, chatActions } = useChat();
const [text, setText] = useState("");
// индикатор состояния редактирования сообщения
const [editingState, setEditingState] = useState(false);
// идентификатор редактируемого сообщения
const [editingMessageId, setEditingMessageId] = useState(0);
const changeText = (e: React.ChangeEvent<HTMLInputElement>) => {
setText(e.target.value);
};
const sendMessage = (e: React.FormEvent) => {
e.preventDefault();
const trimmed = text.trim();
if (!trimmed) return;
const message = {
userId,
userName,
text
};
// если компонент находится в состоянии редактирования
if (editingState) {
// обновляем сообщение
chatActions.update({ id: editingMessageId, text });
setEditingState(false);
// иначе
} else {
// отправляем сообщение
chatActions.send(message);
}
setText("");
};
const removeMessage = (id: number) => {
chatActions.remove({ id });
};
// эффект для отображения уведомлений при изменении лога
useEffect(() => {
if (!log) return;
notify(log);
}, [log]);
return (
<>
<h1 className="title">Let's Chat</h1>
<div className="flex-1 flex flex-col">
{messages &&
messages.length > 0 &&
messages.map((message) => {
// определяем принадлежность сообщения пользователю
const isMsgBelongsToUser = message.userId === userInfo.userId;
return (
<div
key={message.id}
// цвет фона сообщения зависит от 2 факторов:
// 1) принадлежность пользователю;
// 2) состояние редактирования
className={[
"my-2 p-2 rounded-md text-white w-1/2",
isMsgBelongsToUser
? "self-end bg-green-500"
: "self-start bg-blue-500",
editingState ? "bg-gray-300" : ""
].join(" ")}
>
<div className="flex justify-between text-sm mb-1">
<p>
By <span>{message.userName}</span>
</p>
<TimeAgo date={message.createdAt} />
</div>
<p>{message.text}</p>
{/* пользователь может редактировать и удалять только принадлежащие ему сообщения */}
{isMsgBelongsToUser && (
<div className="flex justify-end gap-2">
<button
disabled={editingState}
className={`${
editingState ? "hidden" : "text-orange-500"
}`}
// редактирование сообщения
onClick={() => {
setEditingState(true);
setEditingMessageId(message.id);
setText(message.text);
}}
>
<FiEdit2 />
</button>
<button
disabled={editingState}
className={`${
editingState ? "hidden" : "text-red-500"
}`}
// удаление сообщения
onClick={() => {
removeMessage(message.id);
}}
>
<FiTrash />
</button>
</div>
)}
</div>
);
})}
</div>
{/* отправка сообщения */}
<form onSubmit={sendMessage} className="flex items-stretch">
<div className="flex-1 flex">
<input
type="text"
id="message"
name="message"
value={text}
onChange={changeText}
required
autoComplete="off"
className="input flex-1"
/>
</div>
{editingState && (
<button
className="btn-error"
type="button"
// отмена редактирования
onClick={() => {
setEditingState(false);
setText("");
}}
>
<MdOutlineClose fontSize={18} />
</button>
)}
<button className="btn-primary">
<FiSend fontSize={18} />
</button>
</form>
{/* контейнер для уведомлений */}
<ToastContainer />
</>
);
};
На этом разработка клиента также завершена. Посмотрим, как выглядит наш чат и убедимся в его работоспособности.
Результат
Находясь в корневой директории проекта, выполняем команду yarn dev
и открываем 2 вкладки браузера по адресу http://localhost:3000
(хотя бы одну вкладку необходимо открыть в режиме инкогнито):
Вводим имя пользователя, например, Bob
в одной из вкладок и нажимаем Start chat
:
Делаем тоже самое (только с другим именем, например, Alice
) в другой вкладке:
Получаем в первой вкладке сообщение о подключении Alice
.
Данные пользователя можно найти в разделе Storage -> Local Storage
вкладки Application
инструментов разработчика в браузере:
Обмениваемся сообщениями:
Как проверить, что сообщения записываются в БД? С Prisma
— сделать это проще простого. Выполняем команду yarn prisma studio
и в открывшейся по адресу http://localhost:5555
вкладке выбираем модель Message
:
Пробуем редактировать и удалять сообщения — все работает, как ожидается:
Закрываем одну из вкладок:
Получаем во второй вкладке сообщение об отключении Alice
.
Открываем консоль инструментов разработчика и вызываем метод clearMessages
. Все сообщения удаляются, вкладка перезагружается:
Пожалуй, это все, чем я хотел поделиться с вами в этой статье. Надеюсь, вы нашли для себя что-то интересное и не зря потратили время. Благодарю за внимание и happy coding!
Комментарии (4)
Alexandroppolus
04.08.2022 14:57-1Если кто-то набрасывает мессагу на чатик, то всем коннектам рассылается полный список сообщений? Ну такое..
aio350 Автор
04.08.2022 19:53+1Какое такое?) Я специально сделал оговорку, что мы пойдем простым путем. Разумеется, в реальном приложение каждое событие должно обрабатываться отдельно.
rqdkmndh
Конечно, не зря. Всегда, с удовольствием читаю ваши статьи.
aio350 Автор
Спасибо, друг.