Создайте чат с помощью Socket.io и Express
7 ноября 2022 г.В этой статье мы собираемся создать приложение для чата, которое анонимно соединяет людей в разных комнатах попарно. Приложение чата будет использовать Express.js для кода на стороне сервера, прослушивать связь через веб-сокет, используя < strong>Socket.io, а клиентская часть будет разработана с помощью стандартного JavaScript.
Настройка нашего проекта
- Мы создадим каталог с именем
chat-app
и изменим каталог на каталог с помощью команды.
$ mkdir chat-app && cd chat-app
- Инициализируйте наше приложение Node, выполнив команду.
$ yarn init -y
- Установите экспресс в нашем проекте с помощью Yarn, выполнив команду.
$ yarn add express
- Мы создадим файл JavaScript и имя, а также создадим простой HTTP-сервер Node.
- Далее мы импортируем экспресс в наше приложение, создадим экспресс-приложение и запустим сервер для прослушивания запросов через порт
8001
.
// app.js
const http = require("http")
const express = require("express")
const app = express()
app.get("/index", (req, res) => {
res.send("Welcome home")
})
const server = http.createServer(app)
server.on("error", (err) => {
console.log("Error opening server")
})
server.listen(8001, () => {
console.log("Server working on port 8001")
})
- Теперь мы можем запустить приложение, выполнив команду.
$ node app.js
Вы можете посетить [http://localhost:8001/index](http://localhost:8001/index)
в своем браузере, чтобы проверить, работает ли приложение
Инициализация socket.io на стороне сервера
Чтобы инициализировать сокет на стороне сервера, выполните следующие действия.
- Установите зависимость socket.io в наше приложение, выполнив команду.
баш
$ пряжа добавить socket.io
* Импортируйте socket.io в наш код, создайте новый сервер сокетов, а затем добавьте прослушиватель событий в сокет, чтобы прослушивать установленное соединение.
```javascript // app.js константа http = требуется ("http"); const {Сервер} = require("socket.io"); константный экспресс = требуется("экспресс");
постоянное приложение = экспресс();
app.get("/index", (req, res) => { res.send("Добро пожаловать домой"); });
константный сервер = http.createServer(app);
const io = новый сервер(сервер);
io.on("соединение", (сокет) => { console.log("подключено"); });
server.on("ошибка", (ошибка) => { console.log("Ошибка открытия сервера"); });
server.listen(8001, () => { console.log("Сервер работает на порту 3000"); }); ```
Инициализация socket.io на стороне клиента
Мы будем создавать простой пользовательский интерфейс с использованием ванильного JavaScript, а веб-страницу будем использовать в виде статического файла из нашего экспресс-приложения.
Мы создали каталог public, содержащий файлы для создания нашего пользовательского интерфейса, чтобы структура нашего проекта выглядела следующим образом.
chat-app/
|- node_modules/
|- public/
|- index.html
|- main.js
|- app.js
|- package.json
|- yarn.lock
Мы собираемся использовать Tailwind CSS для стилизации пользовательского интерфейса клиента, чтобы уменьшить объем пользовательского CSS, который мы будем писать.
В index.html
создайте шаблон для нашего окна чата.
<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script src="https://cdn.tailwindcss.com"></script>
<title>Anon Chat App</title>
</head>
<body>
<div class="flex-1 p:2 sm:p-6 justify-between flex flex-col h-screen">
<div id="messages" class="flex flex-col space-y-4 p-3 overflow-y-auto scrollbar-thumb-blue scrollbar-thumb-rounded scrollbar-track-blue-lighter scrollbar-w-2 scrolling-touch">
</div>
<div class="border-t-2 border-gray-200 px-4 pt-4 mb-2 sm:mb-0">
<div class="relative flex">
<input type="text" placeholder="Write your message!" class="w-full focus:outline-none focus:placeholder-gray-400 text-gray-600 placeholder-gray-600 pl-12 bg-gray-200 rounded-md py-3">
<div class="absolute right-0 items-center inset-y-0 hidden sm:flex">
<button type="button" class="inline-flex items-center justify-center rounded-lg px-4 py-3 transition duration-500 ease-in-out text-white bg-blue-500 hover:bg-blue-400 focus:outline-none">
<span class="font-bold">Send</span>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="h-6 w-6 ml-2 transform rotate-90">
<path d="M10.894 2.553a1 1 0 00-1.788 0l-7 14a1 1 0 001.169 1.409l5-1.429A1 1 0 009 15.571V11a1 1 0 112 0v4.571a1 1 0 00.725.962l5 1.428a1 1 0 001.17-1.408l-7-14z"></path>
</svg>
</button>
</div>
</div>
</div>
</div>
<script src="/socket.io/socket.io.js"></script>
<script src="./main.js"></script>
</body>
</html>
В HTML-файл выше мы включили два файла JavaScript, первый для инициализации socket.io на стороне клиента, а другой main.js< /code> для записи собственного кода JavaScript.
Затем в файле main.js
мы создадим функцию, которая может добавить сообщение в окно чата. Функция createMessage
будет ожидать два аргумента. Первый аргумент — это строка сообщения, а второй аргумент — логическое значение, определяющее, исходит ли сообщение от пользователя или
другой внешний пользователь.
// main.js
const messageBox = document.querySelector("#messages");
function createMessage(text, ownMessage = false) {
const messageElement = document.createElement("div");
messageElement.className = "chat-message";
const subMesssageElement = document.createElement("div");
subMesssageElement.className =
"px-4 py-4 rounded-lg inline-block rounded-bl-none bg-gray-300 text-gray-600";
if (ownMessage) {
subMesssageElement.className += " float-right bg-blue-800 text-white";
}
subMesssageElement.innerText = text;
messageElement.appendChild(subMesssageElement);
messageBox.appendChild(messageElement);
}
createMessage("Welcome to vahalla");
createMessage("Who are you to talk to me", true);
Измените код в серверном приложении, app.js
, и используйте статические файлы для отображения пользовательского интерфейса клиента.
// app.js
const http = require("http");
const { Server } = require("socket.io");
const express = require("express");
const path = require("path");
const app = express();
app.use(express.static(path.join(__dirname, "public")));
const server = http.createServer(app);
const io = new Server(server);
io.on("connection", (socket) => {
console.log("connected");
});
server.on("error", (err) => {
console.log("Error opening server");
});
server.listen(8001, () => {
console.log("Server working on port 8001");
});
ПРИМЕЧАНИЕ. Чтобы просмотреть изменения, внесенные в наше приложение, нам нужно остановить запущенное серверное приложение и перезапустить его, чтобы новые изменения вступили в силу. Поэтому мы используем nodemon
для автоматизации этого процесса.
Установите nodemon, запустив.
$ npm install -g nodemon
Затем запустите приложение node с помощью nodemon.
$ nodemon ./app.js
Откройте [http://localhost:8001](http://localhost:3000)
в браузере, чтобы посмотреть, как будет выглядеть приложение чата.
Создание разных комнат для связи через Web Socket
Чтобы отслеживать созданные комнаты и количество пользователей, подключенных к каждой комнате, мы создадим класс Room
для управления этими данными.
Мы создадим новый файл с именем room.js
в корневом каталоге нашего проекта. Затем мы создаем класс Room
, а конструктор инициализирует свойство для сохранения состояния нашей комнаты.
// room.js
// the maximum number of people allowed in each room
const ROOM_MAX_CAPACITY = 2;
class Room {
constructor() {
this.roomsState = [];
}
}
module.exports = Room;
roomsState
— это массив объектов, в котором хранится информация о каждом созданном идентификаторе комнаты и количестве пользователей в этой комнате. Таким образом, типичный roomsState
будет выглядеть следующим образом.
// rooms state
[
{
roomID: "some id",
users: 1
},
{
roomID: "a different id",
users: 2
}
]
Затем добавьте метод для присоединения к комнате. Метод будет проходить по комнате, чтобы проверить, есть ли в каких-либо комнатах несколько пользователей, число которых меньше, чем максимальное количество участников, разрешенное в каждой комнате. Если вся комната в списке занята, будет создана новая комната и инициализировано количество пользователей в этой комнате, равное 1.
Чтобы сгенерировать уникальный идентификатор, мы будем использовать пакет, известный как UUID в нашем приложении.
Установите uuid, выполнив эту команду в нашем терминале.
$ yarn add uuid
Затем импортируйте пакет в наше приложение, выполнив следующие действия.
// room.js
const { v4: uuidv4 } = require("uuid");
class Room {
constructor() {
/**/
}
joinRoom() {
return new Promise((resolve) => {
for (let i = 0; i < this.roomsState.length; i++) {
if (this.roomsState[i].users < ROOM_MAX_CAPACITY) {
this.roomsState[i].users++;
return resolve(this.roomsState[i].id);
}
}
// else generate a new room id
const newID = uuidv4();
this.roomsState.push({
id: newID,
users: 1,
});
return resolve(newID);
});
}
}
module.exports = Room;
ПРИМЕЧАНИЕ. Очевидно, что использование массива для управления состоянием комнат — не лучший способ. Представьте, что в вашем приложении есть тысячи комнат, и вам нужно перебирать каждую комнату для каждого запроса на присоединение. Он будет выполняться за O(n). В рамках данного руководства мы будем придерживаться этого подхода.
Мы бы добавили в класс Room
еще один метод, leaveRoom()
, чтобы уменьшить количество пользователей в определенной комнате.
// room.js
class Room {
constructor() {
/**/
}
joinRoom() {}
leaveRoom(id) {
this.roomsState = this.roomsState.filter((room) => {
if (room.id === id) {
if (room.users === 1) {
return false;
} else {
room.users--;
}
}
return true;
});
}
}
module.exports = Room;
Метод leaveRoom()
принимает идентификатор комнаты и перебирает массив комнат, чтобы найти, соответствует ли какая-либо из комнат идентификатору, указанному в аргументе.
Если он находит подходящую комнату, он проверяет, является ли пользователь в комнате тем, кто может удалить это конкретное состояние комнаты. Если количество пользователей в комнате больше 1, метод leaveRoom()
просто вычитает количество пользователей в этой комнате на единицу.
Наконец, наш код room.js
должен быть похож на этот.
// room.js
const { v4: uuidv4 } = require("uuid");
// the maximum number of people allowed in a room
const ROOM_MAX_CAPACITY = 2;
class Room {
constructor() {
this.roomsState = [];
}
joinRoom() {
return new Promise((resolve) => {
for (let i = 0; i < this.roomsState.length; i++) {
if (this.roomsState[i].users < ROOM_MAX_CAPACITY) {
this.roomsState[i].users++;
return resolve(this.roomsState[i].id);
}
}
const newID = uuidv4();
this.roomsState.push({
id: newID,
users: 1,
});
return resolve(newID);
});
}
leaveRoom(id) {
this.roomsState = this.roomsState.filter((room) => {
if (room.id === id) {
if (room.users === 1) {
return false;
} else {
room.users--;
}
}
return true;
});
}
}
module.exports = Room;
Вход в комнаты и выход из них.
Чтобы создать разные каналы для пользователей в нашем приложении чата, мы создадим для них комнаты.
socket.io позволяет нам создавать произвольные каналы, к которым могут присоединяться и выходить сокеты. Его можно использовать для трансляции событий подмножеству клиентов.
(источник: https://socket.io/docs/v3/rooms/ а>)
Чтобы присоединиться к комнате, мы должны присоединиться к комнате с уникальным идентификатором комнаты.
io.on("connection", socket => {
// join a room
socket.join("some room id");
socket.to("some room id").emit("some event");
});
В нашем серверном приложении, когда новые пользователи присоединяются к соединению, Room.joinRoom()
возвращает уникальный идентификатор, который является нашим уникальным идентификатором комнаты. Таким образом, мы можем присоединиться и освободить место в наших комнатах следующим образом.
// app.js
io.on("connection", async (socket) => {
const roomID = await room.joinRoom();
// join room
socket.join(roomID);
socket.on("disconnect", () => {
// leave room
room.leaveRoom(roomID);
});
});
Отправка и получение сообщений
Теперь мы вернемся к нашему коду на стороне клиента, чтобы генерировать события для сообщений, отправленных от клиента. А также прослушивать сообщения, поступающие с сервера, и писать эти сообщения в наш чат.
// main.js
socket.on("receive-message", (message) => {
createMessage(message);
});
sendButton.addEventListener("click", () => {
if (textBox.value != "") {
socket.emit("send-message", textBox.value);
createMessage(textBox.value, true);
textBox.value = "";
}
});
ПРИМЕЧАНИЕ. В нашем приложении чата мы напрямую добавляем сообщение от пользователя в окно чата, не подтверждая, получено ли сообщение сервером сокетов. Обычно это не так.
Затем в нашем экспресс-приложении.
// app.js
io.on("connection", async (socket) => {
const roomID = await room.joinRoom();
// join room
socket.join(roomID);
socket.on("send-message", (message) => {
socket.to(roomID).emit("receive-message", message);
});
socket.on("disconnect", () => {
// leave room
room.leaveRoom(roomID);
});
});
Наконец-то сделали код нашего экспресс-приложения таким.
// app.js
const http = require("http");
const { Server } = require("socket.io");
const express = require("express");
const path = require("path");
const Room = require("./room");
const app = express();
app.use(express.static(path.join(__dirname, "public")));
const server = http.createServer(app);
const io = new Server(server);
const room = new Room();
io.on("connection", async (socket) => {
const roomID = await room.joinRoom();
// join room
socket.join(roomID);
socket.on("send-message", (message) => {
socket.to(roomID).emit("receive-message", message);
});
socket.on("disconnect", () => {
// leave room
room.leaveRoom(roomID);
});
});
server.on("error", (err) => {
console.log("Error opening server");
});
server.listen(8001, () => {
console.log("Server working on port 8001");
});
И наш клиентский JavaScript выглядит так.
// main.js
const messageBox = document.querySelector("#messages");
const textBox = document.querySelector("input");
const sendButton = document.querySelector("button");
function createMessage(text, ownMessage = false) {
const messageElement = document.createElement("div");
messageElement.className = "chat-message";
const subMesssageElement = document.createElement("div");
subMesssageElement.className =
"px-4 py-4 rounded-lg inline-block rounded-bl-none bg-gray-300 text-gray-600";
if (ownMessage) {
subMesssageElement.className += " float-right bg-blue-800 text-white";
}
subMesssageElement.innerText = text;
messageElement.appendChild(subMesssageElement);
messageBox.appendChild(messageElement);
}
const socket = io();
socket.on("connection", (socket) => {
console.log(socket.id);
});
socket.on("receive-message", (message) => {
createMessage(message);
});
sendButton.addEventListener("click", () => {
if (textBox.value != "") {
socket.emit("send-message", textBox.value);
createMessage(textBox.value, true);
textBox.value = "";
}
});
Тестирование нашего приложения для чата
Чтобы отправить текстовое сообщение в наше приложение для чата, мы откроем четыре разных браузера, чтобы убедиться, что созданы две комнаты.
Заключение
Если вы видите это, это означает, что мы прочитали до сих пор и, вероятно, приложение чата работает на нашем компьютере.
Код из этой статьи можно найти в этом репозитории GitHub.
Чтобы добавить больше задач, эти функции можно включить в приложение чата
- Сообщать пользователям, если кто-то вышел из комнаты или присоединился к ней.
- Рефакторинг массива состояний комнат для более эффективной структуры данных.
- Разрешить создание пар на основе выбранной темы (это необходимо настроить в объекте "Комната")
Чтобы узнать больше о socket.io, посетите официальную документацию.
Если вам понравилась эта статья, вы можете угостить меня кофе.
Также опубликовано здесь
Оригинал