JavaScript: разрабатываем чат с помощью Socket.io, Express и React с акцентом на работе с медиа

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.



Привет, друзья!


В данной статье я хочу показать вам, как разработать простое приложение для обмена сообщениями в режиме реального времени с использованием Socket.io, Express и React с акцентом на работе с медиа.


Функционал нашего приложения будет следующим:


  • при первом запуске приложение предлагает пользователю ввести свое имя;
  • имя пользователя и его идентификатор записываются в локальное хранилище;
  • при повторном запуске приложения имя и идентификатор пользователя извлекаются из локального хранилища (имитация системы аутентификации/авторизации);
  • выполняется подключение к серверу через веб-сокеты и вход в комнату main_room (при желании можно легко реализовать возможность выбора или создания других комнат);
  • пользователи обмениваются сообщениями в реальном времени;
  • типом сообщения может быть текст, аудио, видео или изображение;
  • передаваемые файлы сохраняются на сервере;
  • путь к сохраненному на сервере файлу добавляется в сообщение;
  • сообщение записывается в базу данных;
  • пользователи могут записывать аудио и видеосообщения;
  • после прикрепления файла и записи аудио или видео сообщения, отображается превью созданного контента;
  • пользователи могут добавлять в текст сообщения эмодзи;
  • текстовые сообщения могут озвучиваться;
  • и т.д.

Репозиторий с исходным кодом проекта.


Если вам это интересно, прошу под кат.


Справедливости ради следует отметить, что я уже писал о разработке чата на Хабре. Будем считать, что это новая (продвинутая) версия.


Подготовка и настройка проекта


Создаем директорию, переходим в нее и инициализируем Node.js-проект:


mkdir chat-app
cd chat-app

yarn init -yp
# or
npm init -y

Создаем директорию для сервера и шаблон для клиента с помощью Create React App:


mkdir server

yarn create react-app client
# or
npx create-react-app client

Нам потребуется одновременно запускать два сервера (для клиента и самого сервера), поэтому установим concurrently — утилиту для одновременного выполнения нескольких команд, определенных в файле package.json:


yarn add concurrently
# or
npm i concurrently

Определяем команды в package.json:


"scripts": {
  "dev:client": "yarn --cwd client start",
  "dev:server": "yarn --cwd server dev",
  "dev": "concurrently \"yarn dev:client\" \"yarn dev:server\""
}

Или, если вы используете npm:


"scripts": {
  "dev:client": "npm run start --prefix client",
  "dev:server": "npm run dev --prefix server",
  "dev": "concurrently \"npm run dev:client\" \"npm run dev:server\""
}

В качестве БД мы будем использовать MongoDb Atlas Database.


Переходим по ссылке, создаем аккаунт, создаем проект и кластер и получаем строку для подключения вида mongodb+srv://<user>:<password>@cluster0.f7292.mongodb.net/<database>?retryWrites=true&w=majority*, где <user>, <password> и <database> — данные, которые вы указали при создании проекта и кластера.


  • Для получения адреса БД необходимо нажать Connect рядом с названием кластера (Cluster0) и затем Connect your application.
  • Если у вас, как и у меня, динамический IP, во вкладке Network Access раздела Security надо прописать 0.0.0.0/0







Можно приступать к разработке сервера.


Сервер


Переходим в директорию server и устанавливаем зависимости:


cd server

# производственные зависимости
yarn add express socket.io mongoose cors multer
# or
npm i ...

# зависимость для разработки
yarn add -D nodemon
# or
npm i -D nodemon

  • expressNode.js-фреймворк для разработки веб-серверов;
  • socket.io — библиотека, облегчающая работу с веб-сокетами;
  • mongoose — ORM для работы с MongoDB;
  • cors — утилита для работы с CORS;
  • multer — утилита для разбора (парсинга) данных в формате multipart/form-data (для сохранения файлов на сервере);
  • nodemon — утилита для запуска сервера для разработки.

Определяем тип кода сервера (модуль) и команду для запуска сервера для разработки в файле package.json:


"type": "module",
"scripts": {
  "dev": "nodemon"
}

Структура директории server будет следующей:


- files - директория для хранения файлов
- models
  - message.model.js - модель сообщения для `Mongoose`
- socket_io
  - handlers
    - message.handlers.js - обработчики для сообщений
    - user.handler.js - обработчики для пользователей
  - onConnection.js - обработка подключения
- utils
  - file.js - утилиты для работы с файлами
  - onError.js - обработчик ошибок
  - upload.js - утилита для сохранения файлов
- config.js - настройки (в репозитории имеется файл `config.example.js` с примером настроек)
- index.js - основной файл сервера

Определяем настройки в файле config.js (не забудьте добавить его в .gitignore):


// разрешенный источник
export const ALLOWED_ORIGIN = 'http://localhost:3000'
// адрес БД
export const MONGODB_URI =
  'mongodb+srv://<user>:<password>@cluster0.f7292.mongodb.net/<database>?retryWrites=true&w=majority'

Определяем модель в файле models/message.model.js:


import mongoose from 'mongoose'

const { Schema, model } = mongoose

const messageSchema = new Schema(
  {
    messageId: {
      type: String,
      required: true,
      unique: true
    },
    messageType: {
      type: String,
      required: true
    },
    textOrPathToFile: {
      type: String,
      required: true
    },
    roomId: {
      type: String,
      required: true
    },
    userId: {
      type: String,
      required: true
    },
    userName: {
      type: String,
      required: true
    }
  },
  {
    timestamps: true
  }
)

export default model('Message', messageSchema)

Каждое наше сообщение будет включать следующую информацию:


  • messageId — идентификатор сообщения;
  • messageType — тип сообщения;
  • textOrPathToFile — текст сообщения или путь к файлу;
  • roomId — идентификатор комнаты;
  • userId — идентификатор пользователя;
  • userName — имя пользователя;
  • createdAt, updatedAt — дата и время создания и обновления сообщения, соответственно (timestamps: true).

Кратко рассмотрим утилиты (директория utils).


Обработчик ошибок (onError.js):


export default function onError(err, req, res, next) {
  console.log(err)

  // если имеется объект ответа
  if (res) {
    // статус ошибки
    const status = err.status || err.statusCode || 500
    // сообщение об ошибке
    const message = err.message || 'Something went wrong. Try again later'
    res.status(status).json({ message })
  }
}

Утилита для работы с файлами (file.js):


import { unlink } from 'fs/promises'
import { dirname, join } from 'path'
import { fileURLToPath } from 'url'
import onError from './onError.js'

// путь к текущей директории
const _dirname = dirname(fileURLToPath(import.meta.url))

// путь к директории с файлами
const fileDir = join(_dirname, '../files')

// утилита для получения пути к файлу
export const getFilePath = (filePath) => join(fileDir, filePath)

// утилита для удаления файла
export const removeFile = async (filePath) => {
  try {
    await unlink(join(fileDir, filePath))
  } catch (e) {
    onError(e)
  }
}

Утилита для сохранения файлов (upload.js):


import { existsSync, mkdirSync } from 'fs'
import multer from 'multer'
import { dirname, join } from 'path'
import { fileURLToPath } from 'url'

// путь к текущей директории
const _dirname = dirname(fileURLToPath(import.meta.url))

const upload = multer({
  storage: multer.diskStorage({
    // директория для записи файлов
    destination: async (req, _, cb) => {
      // извлекаем идентификатор комнаты из HTTP-заголовка `X-Room-Id`
      const roomId = req.headers['x-room-id']
      // файлы хранятся по комнатам
      // название директории - идентификатор комнаты
      const dirPath = join(_dirname, '../files', roomId)

      // создаем директорию при отсутствии
      if (!existsSync(dirPath)) {
        mkdirSync(dirPath, { recursive: true })
      }

      cb(null, dirPath)
    },
    filename: (_, file, cb) => {
      // названия файлов могут быть одинаковыми
      // добавляем к названию время с начала эпохи и дефис
      const fileName = `${Date.now()}-${file.originalname}`

      cb(null, fileName)
    }
  })
})

export default upload

Рассмотрим основной файл сервера (index.js).


Импортируем все и вся:


import cors from 'cors'
import express from 'express'
import { createServer } from 'http'
import mongoose from 'mongoose'
import { Server } from 'socket.io'
import { ALLOWED_ORIGIN, MONGODB_URI } from './config.js'
import onConnection from './socket_io/onConnection.js'
import { getFilePath } from './utils/file.js'
import onError from './utils/onError.js'
import upload from './utils/upload.js'

Создаем экземпляр Express-приложения и подключаем посредников для работы с CORS и парсинга JSON:


const app = express()

app.use(
  cors({
    origin: ALLOWED_ORIGIN
  })
)
app.use(express.json())

Обрабатываем загрузку файлов:


app.use('/upload', upload.single('file'), (req, res) => {
  if (!req.file) return res.sendStatus(400)

  // формируем относительный путь к файлу
  const relativeFilePath = req.file.path
    .replace(/\\/g, '/')
    .split('server/files')[1]

  // и возвращаем его
  res.status(201).json(relativeFilePath)
})

Обрабатываем получение файлов:


app.use('/files', (req, res) => {
  // формируем абсолютный путь к файлу
  const filePath = getFilePath(req.url)

  // и возвращаем файл по этому пути
  res.status(200).sendFile(filePath)
})

Добавляем обработчик ошибок и подключаемся к БД:


app.use(onError)

try {
  await mongoose.connect(MONGODB_URI, {
    useNewUrlParser: true,
    useUnifiedTopology: true
  })
  console.log('						
Источник: https://habr.com/ru/company/timeweb/blog/655143/


Интересные статьи

Интересные статьи

В жизни каждого инженера может произойти ситуация, когда процесс его приложения «завис» и не выполняет ту работу, которую должен. Причины могут быть разные и чтобы узнать их, нужно заглянуть во внутре...
Привет, Хабр!Меня зовут Дмитрий Матлах. Я тимлид в AGIMA. Мы с коллегами обратили внимание, что в сообществе часто возникает вопрос о том, как совместить на одном проекте Bitrix-компоненты и реактивны...
Доброго времени суток, друзья! Данная статья посвящена возможностям JavaScript, которые будут представлены в новой версии спецификации (ECMAScript 2021, ES12). Речь пойдет о сл...
Привет, Хабр! Представляю вашему вниманию перевод статьи «7 Tricks with Resting and Spreading JavaScript Objects» автора Joel Thoms. Всем привет, на днях коллега по работе скинул мне ссылку на...
Мы продолжаем рассказ о том, как внутри Одноклассников с помощью GraalVM нам удалось подружить Java и JavaScript и начать миграцию в огромной системе с большим количеством legacy-кода. Во...