Создайте чат с помощью Socket.io и Express

Создайте чат с помощью 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) в своем браузере, чтобы проверить, работает ли приложение

welcome-home

Инициализация 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/)

Чтобы присоединиться к комнате, мы должны присоединиться к комнате с уникальным идентификатором комнаты.

В нашем серверном приложении, когда новые пользователи присоединяются к соединению, Room.joinRoom() возвращает уникальный идентификатор, который является нашим уникальным идентификатором комнаты. Таким образом, мы можем присоединиться и освободить место в наших комнатах следующим образом.

Отправка и получение сообщений

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

<цитата>

ПРИМЕЧАНИЕ. В нашем приложении чата мы напрямую добавляем сообщение от пользователя в окно чата, не подтверждая, получено ли сообщение сервером сокетов. Обычно это не так.

Затем в нашем экспресс-приложении.

// 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 = "";
  }
});

Тестирование нашего приложения для чата

Чтобы отправить текстовое сообщение в наше приложение для чата, мы откроем четыре разных браузера, чтобы убедиться, что созданы две комнаты. ezgif.com-gif-maker (1).gif

Заключение

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

Код из этой статьи можно найти в этом репозитории GitHub.

Чтобы добавить больше задач, эти функции можно включить в приложение чата

  • Сообщать пользователям, если кто-то вышел из комнаты или присоединился к ней.
  • Рефакторинг массива состояний комнат для более эффективной структуры данных.
  • Разрешить создание пар на основе выбранной темы (это необходимо настроить в объекте "Комната")

Чтобы узнать больше о socket.io, посетите официальную документацию.

Если вам понравилась эта статья, вы можете угостить меня кофе.

Также опубликовано здесь


Оригинал