В статье я расскажу, как можно создать чат-приложение в режиме реального времени с помощью Vue.js, Node.js, Express и SOCKET.IO.
Вот скриншот того, что должно получиться:
PS: Я не специалист по фронтенду, так что не смейтесь над дизайном. Данный макет используется только в качестве примера.
Установка
Для работы нам понадобятся Node.js и NPM. Если у вас еще нет установленного Node.JS, то качайте его отсюда.
- Вам потребуются базовые знания JavaScript.
- Пригодятся небольшие знания по Vue.js (но это не принципиально).
Если все готово, то можно начинать.
Создайте директорию для приложения и откройте ее в любимом редакторе. Я пользуюсь Visual Studio Code.
Если хотите, можете работать в терминале.
mkdir ChatApp && cd ChatApp && code
Давайте инициализируем директорию через NPM.
npm init
Если вам предлагают ввести какую-то информацию, смело ее вводите или нажимайте Enter для настроек по умолчанию. Такая информация используется для настройки пакета .json файла.
Установка зависимостей
Давайте установим зависимости приложения. Я покажу список нужных зависимостей и познакомлю с процессом их установки. Чтобы ничего не усложнять, ограничимся двумя зависимостями.
i. EXPRESS
npm install express --save
ii.SOCKET.IO
npm install --save socket.io
После установки всех зависимостей запустите
npm install
Так установятся все необходимые пакеты.
Фронтенд с Vue.js (разметка)
Интерфейс приложения мы сделаем в Vue.js. Его нужно установить в нашу директорию и добавить bootstrap 4.3.1.
Создадим файл index.html.
touch index.html
Для включения в проекты Vue.js и bootstrap скопируем CDN и добавим в раздел со скриптами файла index.html.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>ChatApp_Socket</title>
<link href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" rel="stylesheet"
integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.10/vue.js"></script>
<script src="/socket.io/socket.io.js"></script>
</head>
После успешной установки Vue и bootstrap перейдем к созданию разметки.
<body>
<div id="app">
<div class="container">
<div class="col-lg-6 offset-lg-3">
<div v-if="ready">
<p v-for="user in info">
{{user.username}} {{user.type}}
</p>
</div>
<div v-if="!ready">
<h4>Enter your username</h4>
<form @submit.prevent="addUser">
<div class="form-group row">
<input type="text" class="form-control col-9" v-model="username"
placeholder="Enter username here">
<input type="submit" value="Join" class="btn btn-sm btn-info ml-1">
</div>
</form>
</div>
<h2 v-else>{{username}}</h2>
<div class="card bg-info" v-if="ready">
<div class="card-header text-white">
<h4>My Chat App <span class="float-right">{{connections}} connections</span></h4>
</div>
<ul class="list-group list-group-flush text-right">
<small v-if="typing" class="text-white">{{typing}} is typing</small>
<li class="list-group-item" v-for="message in messages">
<span :class="{'float-left':message.type === 1}">
{{message.message}}
<small>:{{message.user}}</small>
</span>
</li>
</ul>
<div class="card-body">
<form @submit.prevent="send">
<div class="form-group">
<input type="text" class="form-control" v-model="newMessage"
placeholder="Enter message here">
</div>
</form>
</div>
</div>
</div>
</div>
</div>
</body>
Для подключения Socket.IO сервера к клиенту добавим клиентские JavaScript библиотеки.
<script src="/socket.io/socket.io.js"></script>
Это будет наш файл Vue и bootstrap (HTML) для фронтенда. Можете скопировать код целиком, чтобы не отставать.
Лучший способ научиться чему-то — повторять за другими.
Также можете скачать клиентскую библиотеку Socket.IO.
Можно разграничить функционал, вынеся JavaScript-код из общей разметки. Это решать вам. Но я для удобства этого делать не буду.
Установка сервера
Создадим server.js. Внутри файла создадим и настроим Express-сервер для работы с Socket.IO.
let app = require('express')();
let http = require('http').Server(app);
let io = require('socket.io')(http);
app.get('/', (req, res) => {
res.sendFile(__dirname + '/index.html')
});
http.listen(3000, () => {
console.log('Listening on port *: 3000');
});
Это базовая конфигурация, необходимая для установки Socket.IO в бэкенде.
Как работает Socket.IO
Socket.IO — это JavaScript-библиотека чата в режиме реального времени. Документацию можно почитать здесь (она не относится к тематике данной статьи). Я объясню лишь то, что нам потребуется для статьи.
Работа Socket.IO сводится к добавлению слушателей события для экземпляра http.Server. Именно этим мы сейчас и займемся.
let app = require('express')();
let http = require('http').Server(app);
let io = require('socket.io')(http);
app.get('/', (req, res) => {
res.sendFile(__dirname + '/index.html')
});
http.listen(3000, () => {
console.log('Listening on port *: 3000');
});
Затем будем прослушивать событие о новом подключении.
io.on('connection', (socket) => {
socket.on('disconnect', () => {
console.log("A user disconnected");
});
.....
});
Например, если пользователь посещает 127.0.0.1:3000, то сообщение об этом напечатается в консоли.
В Socket.IO есть много флагов или методов, которые можно вызывать для выполнения различных действий (например, отправка событий, прослушивание событий и т.д.).
Socket.ON(): принимает в качестве параметров название события и обратный вызов. Прослушивает события, отправленные на сервер и с него. Обратный вызов нужен для получения данных, связанных с событием.
Socket.EMIT(): отправляет/посылает событие с данными или без них для прослушивания сокетов (включая себя).
Socket.Broadcast.Emit(): отправляет событие всем подключенным клиентам (кроме себя). Эти методы пригодятся для отправки событий с сервера через чат-приложение.
Настройка кода клиентской части
Откройте index.html. В нижней части файла добавьте следующий код в тег script.
<script>
var socket = io();
let vue = new Vue({
el: '#app',
data: {
newMessage: null,
messages: [],
typing: false,
username: null,
ready: false,
info: [],
connections: 0,
},
created() {
window.onbeforeunload = () => {
socket.emit('leave', this.username);
}
socket.on('chat-message', (data) => {
this.messages.push({
message: data.message,
type: 1,
user: data.user,
});
});
socket.on('typing', (data) => {
this.typing = data;
});
socket.on('stopTyping', () => {
this.typing = false;
});
socket.on('joined', (data) => {
this.info.push({
username: data,
type: 'joined'
});
setTimeout(() => {
this.info = [];
}, 5000);
});
socket.on('leave', (data) => {
this.info.push({
username: data,
type: 'left'
});
setTimeout(() => {
this.info = [];
}, 5000);
});
socket.on('connections', (data) => {
this.connections = data;
});
},
watch: {
newMessage(value) {
value ? socket.emit('typing', this.username) : socket.emit('stopTyping')
}
},
methods: {
send() {
this.messages.push({
message: this.newMessage,
type: 0,
user: 'Me',
});
socket.emit('chat-message', {
message: this.newMessage,
user: this.username
});
this.newMessage = null;
},
addUser() {
this.ready = true;
socket.emit('joined', this.username)
}
},
});
</script>
</html>
Мы добавили var socket = io();
и создали новый экземпляр Vue. Далее внутри этого экземпляра прописали наш элемент как el: ‘#app’
и объявили наш объект данных с помощью пустых массивов и свойств.
Давайте перейдем к объекту methods
. Мы видим методSend()
. Он хранит информацию о чате в массиве message и отправляет события чата на сервер с помощью флага Socket.emit()
.
methods: {
send() {
this.messages.push({
message: this.newMessage,
type: 0,
user: 'Me',
});
socket.emit('chat-message', {
message: this.newMessage,
user: this.username
});
this.newMessage = null;
},
.....
На серверной стороне мы получаем событие с флагом Socket.on()
и через флаг Socket.broadcast.emit()
пересылаем его другим подключенным клиентам.
В хуке Vue CREATED
мы прослушиваем все события, отправленные сервером. Сюда же включено событие chat-message, которое мы пересылали с сервера ранее.
Через трансляцию событий сервер отправляет их всем подключенным клиентам, кроме самого отправителя.
Аналогия
Если пользователь А отправляет сообщение на сервер, а сервер ретранслирует его пользователям B, C, D, E и т.д., то это сообщение получат все пользователи, кроме А, поскольку он является отправителем.
Так что нет ничего страшного в том, что мы прослушиваем событие chat-message из объекта CREATED. Все равно мы, как отправители, не получим это сообщение с сервера.
Именно так все и происходит, когда пользователи выполняют какие-то действия в ChatApp.
После получения с сервера события из объекта CREATED , мы сохраняем его в массиве message с message type
(указывает, что сообщение отправлено с сервера) и user
(тот, кто отправил сообщение).
socket.on('chat-message', (data) => {
this.messages.push({
message: data.message,
type: 1,
user: data.user,
});
});
Подведем итог всему происходящему во фронтенде.
1. Хук Methods. Именно здесь в Vue.js вы создаете все методы/функции для компонента. И при каждом вызове метода в разметке/шаблоне Vue обращается к этому хуку.
У нас есть только два объявленных метода:
i. Send(). Этот метод вызывается, когда пользователь печатает сообщение и нажимает «Отправить». Он сохраняет сообщение в массиве message и отправляет события на сервер (см. выше).
ii. addUser(). Метод отправляет на сервер событие joined и устанавливает значение переменной ready как true, показывая, что пользователь успешно присоединился к чату.
addUser() {
this.ready = true;
socket.emit('joined', this.username)
}
2. Хук Created. Данный хук вызывается при загрузке компонента Vue.js. Это отличное место для прослушивания всех событий с сервера.
Мы прослушиваем 6 событий с сервера и отправляем 1 событие на сервер.
created() {
window.onbeforeunload = () => {
socket.emit('leave', this.username);
}
socket.on('chat-message', (data) => {
this.messages.push({
message: data.message,
type: 1,
user: data.user,
});
});
socket.on('typing', (data) => {
this.typing = data;
});
socket.on('stopTyping', () => {
this.typing = false;
});
socket.on('joined', (data) => {
this.info.push({
username: data,
type: 'joined'
});
setTimeout(() => {
this.info = [];
}, 5000);
});
socket.on('leave', (data) => {
this.info.push({
username: data,
type: 'left'
});
setTimeout(() => {
this.info = [];
}, 5000);
});
socket.on('connections', (data) => {
this.connections = data;
});
},
В коде выше видно, чем занимается каждое событие.
3. Хук Watch. Это очень интересная штука. Он может многое, но используется для прослушивания изменений в блоке сообщений и отправки события typing, которое транслируется сервером для других клиентов.
watch: {
newMessage(value) {
value ? socket.emit('typing', this.username) : socket.emit('stopTyping')
}
},
Еще он применяется для отметки о прочтении в разметках с используемыми директивами Vue.js. Вот полный файл index.html.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>ChatApp_Socket</title>
<link href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" rel="stylesheet"
integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous" />
</head>
<body>
<div id="app">
<div class="container">
<div class="col-lg-6 offset-lg-3">
<div v-if="ready">
<p v-for="user in info">
{{user.username}} {{user.type}}
</p>
</div>
<div v-if="!ready">
<h4>Enter your username</h4>
<form @submit.prevent="addUser">
<div class="form-group row">
<input type="text" class="form-control col-9" v-model="username"
placeholder="Enter username here">
<input type="submit" value="Join" class="btn btn-sm btn-info ml-1">
</div>
</form>
</div>
<h2 v-else>{{username}}</h2>
<div class="card bg-info" v-if="ready">
<div class="card-header text-white">
<h4>My Chat App <span class="float-right">{{connections}} connections</span></h4>
</div>
<ul class="list-group list-group-flush text-right">
<small v-if="typing" class="text-white">{{typing}} is typing</small>
<li class="list-group-item" v-for="message in messages">
<span :class="{'float-left':message.type === 1}">
{{message.message}}
<small>:{{message.user}}</small>
</span>
</li>
</ul>
<div class="card-body">
<form @submit.prevent="send">
<div class="form-group">
<input type="text" class="form-control" v-model="newMessage"
placeholder="Enter message here">
</div>
</form>
</div>
</div>
</div>
</div>
</div>
</body>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.10/vue.js"></script>
<script src="/socket.io/socket.io.js"></script>
<script>
// Клиентская часть объекта Socket.IO
var socket = io();
// Создание нового экземпляра Vue
let vue = new Vue({
// Добавление корневого элемента в приложение Vue
el: '#app',
// Объявление объекта данных через пустые массивы и свойства
data: {
newMessage: null,
messages: [],
typing: false,
username: null,
ready: false,
info: [],
connections: 0,
},
// Создан метод Vue CREATED
created() {
// Отправление события 'leave' (покидает чат) во вкладку закрытых событий.
window.onbeforeunload = () => {
socket.emit('leave', this.username);
}
// Прослушивание события chat-message, отправленного с сервера и добавленного в массив message
socket.on('chat-message', (data) => {
this.messages.push({
message: data.message,
type: 1,
user: data.user,
});
});
// Прослушивание события typing, отправленного с сервера и показывающего данные (имя пользователя) в UI
socket.on('typing', (data) => {
this.typing = data;
});
// Прослушивание события stopTyping, отправленного с сервера и меняющего свойства typing на false
socket.on('stopTyping', () => {
this.typing = false;
});
// Прослушивание события joined, отправленного с сервера и добавляющего данные в массив info
socket.on('joined', (data) => {
this.info.push({
username: data,
type: 'joined'
});
// Установка времени ожидания до обнуления массива info
setTimeout(() => {
this.info = [];
}, 5000);
});
// Прослушивание события leave, отправляемого с сервера и добавляющего данные в массив info
socket.on('leave', (data) => {
this.info.push({
username: data,
type: 'left'
});
// Установка времени ожидания до обнуления массива info
setTimeout(() => {
this.info = [];
}, 5000);
});
// Прослушивание события connections, отправляемого с сервера. Показывает общее количество подключенных клиентов
socket.on('connections', (data) => {
this.connections = data;
});
},
// Vue-хук Watch
watch: {
// Просматривает изменения во входящих сообщениях и отправляет событие typing или stopTyping
newMessage(value) {
value ? socket.emit('typing', this.username) : socket.emit('stopTyping')
}
},
//Vue-хук Methods
methods: {
//Метод send сохраняет сообщение пользователя и отправляет событие на сервер.
send() {
this.messages.push({
message: this.newMessage,
type: 0,
user: 'Me',
});
socket.emit('chat-message', {
message: this.newMessage,
user: this.username
});
this.newMessage = null;
},
// Метод addUser отправляет событие joined с именем пользователя и устанавливает значение свойства ready как true.
addUser() {
this.ready = true;
socket.emit('joined', this.username)
}
},
});
</script>
</html>
Заключение
Вы можете улучшить код, добавить авторизацию, группы, приватные беседы и управление базой данных, либо отслеживать все изменения через управление сеансами.
Перевод статьи Solomon Eseme: Build a real-time chat app with vuejs, socket.IO and Nodejs