Real-Time Data Transfer Using Vue and Socket.IO [Part 2 of 3] | A²I² Artificial Intelligence at Deakin Skip to content About Research Publications Themes Grants Awards Industry Projects Careers Study with us News Blog Contact . Home About Research Themes Grants Awards Publications Industry Projects Study with us News Blog Contact Home/Blog Real-Time Data Transfer Using Vue and Socket.IO [Part 2 of 3] Blog / Rob Hyndman / December 10, 2020 This project uses Node.js v14.15.1. In the last post we covered setting up a simple server and client which communicate with each other using websockets. This post will focus on the creation of a chat room. The third and final post in this series will build upon the techniques that we’ll cover in this post to create a simple multiplayer game “Fill Frenzy!”. Add Real-Time Chat The first of the two main components we will cover will be the chat room. The chat room will have the following features: Support for multiple users, A list of all connected users, A history of all chat messages, and Notifications for when other users are typing. Fig.1 – A preview of the chat component The chat component will be lengthy and touch multiple files, so I will break it down into manageable sections as best I can. Update the Server Code Having all the server code in one file will become painful to manage, especially since we’ll be adding new code to handle more components of our app. Let’s try to structure it a little better, and then add our chat component. Create a new folder named server in the project root. Move server.js into the server folder, then rename it to index.js. Create server/chat.js and add the following code: let socketio = undefined;
const chatHistory = [];
const userList = [];
const userTypingStatus = {};
const timers = {};
const typingStatusTime = 2000;
const id = {
message: 0,
user: 0,
unique: {}
}; Firstly, we create a variable socketio to use as a reference to our main Socket.IO object, and arrays to contain our chat messages and list of users. Next, we add some properties to keep track of the User is typing notifications. We also create an object to keep track of the IDs that we’ll attach to messages and users. const initialise = (io) => {
socketio = io;
} Next we create an initialise function which we can use to set our socketio reference. const run = (socket) => {
// new socket connected, send user list and chat history
socket.emit('userList', userList);
socket.emit('chatHistory', chatHistory);
handleUserConnected(socket);
handleChatMessage(socket);
handleTypingStatus(socket);
} The run function will handle the main functionality of the chat room for each individual socket which is passed to the function as an argument. The function will be called only once per socket when a new connection is made, so we will take advantage of this to send the current state of the app to the socket by emitting the events userList and chatHistory which both pass the corresponding information. const handleUserConnected = (socket) => {
// listen for 'userConnected' events
socket.on('userConnected', (username) => {
// create a new user object
let newUser = {
userID: getID('user'),
uniqueID: getID('unique', username),
name: username
};
// determine user unique name
newUser.uniqueName =
newUser.uniqueID > 0 ?
`${newUser.name}#${newUser.uniqueID}` :
newUser.name;
// add the user to the userList
userList.push(newUser);
// send a login event to this user
socket.emit('userLogin', newUser);
// create a new message about the connection
let connectedMessage = {
sender: 'Server',
text: `${newUser.uniqueName} connected`,
id: getID('message'),
time: Date.now()
};
// add the message to the chatHistory
chatHistory.push(connectedMessage);
// send the user connection to all connected sockets
socketio.emit('userConnected', newUser);
socketio.emit('chatMessage', connectedMessage);
});
} The handleUserConnected function listens for the userConnected event, and will respond appropriately. First, we create a new user object and determine what the unique version of the username would be. For example, if two users named Rob connected, one user would keep the username Rob while the other would change to Rob#1. Once the user is created, we store it in the userList then send an event notifying the user that they have now logged in. This event also sends the user object, so that the client knows which user it is, and has all the relevant information. Next, we create a chat message that will notify all users of the new connection and update the clients with this new information. You may have noticed that in this code snippet events were emitted using the socketio object, while in the previous snippet emitted events used the socket object. It is important to note the difference—socket.emit() is sending an event to that specific socket, whereas socketio.emit() is sending an event to all connected sockets, much like a broadcast. const handleChatMessage = (socket) => {
// listen for 'chatMessage' events
socket.on('chatMessage', (chatMessage) => {
// attach a message id and timestamp
chatMessage.id = getID('message');
chatMessage.time = Date.now();
// add the message to the chat history
chatHistory.push(chatMessage);
// clear typing status for this user
clearTypingStatus(chatMessage.sender.userID);
// send the message to all connected sockets
socketio.emit('chatMessage', chatMessage);
});
} The handleChatMessage function listens for the chatMessage event, and will respond appropriately. We receive a chat message from the client, add an ID and timestamp, then save it to our chat history. Then we ensure that we clear the typing status so that clients stop seeing this user as typing a message. Lastly, update all clients with the new message. const handleTypingStatus = (socket) => {
// listen for 'setTypingStatus' events
socket.on('setTypingStatus', (typingStatus) => {
// set typing status to true for this user
userTypingStatus[typingStatus.user.userID] = {
user: typingStatus.user,
typing: true
};
// set a timer to reset the typing status
setStatusTimer(typingStatus.user.userID);
// broadcast the typing status'
socketio.emit('typingStatus', userTypingStatus);
});
} The handleTypingStatus function listens for the setTypingStatus event, and will respond appropriately. We receive an event from the client, then set the typing status of the user to true and store that within an object using the user id as a key. Next we start a timer that will revert the typing status back to false after a certain amount of time. Lastly, we broadcast the current status of all our users, including the newly updated one. const clearTypingStatus = (userID) => {
// set typing status to false for this user
userTypingStatus[userID].typing = false;
// if a timer exists for this user, remove it
removeStatusTimer(userID);
// broadcast the typing status'
socketio.emit('typingStatus', userTypingStatus);
}
const removeStatusTimer = (userID) => {
// if this user id is a key of timers, clear the timer
if (timers.hasOwnProperty(userID)) {
clearTimeout(timers[userID]);
}
}
const setStatusTimer = (userID) => {
// if a timer exists for this user, remove it
removeStatusTimer(userID);
// set a timer to clear the typing status
timers[userID] = setTimeout(() => {
userTypingStatus[userID].typing = false;
// broadcast the typing status'
socketio.emit('typingStatus', userTypingStatus);
}, typingStatusTime);
} These three functions assist in managing the typing status and associated timers of each user. The function clearTypingStatus sets the status to false and removes the timer immediately, whereas the function setStatusTimer sets the status to false after typingStatusTime time has elapsed. const getID = (type, username = undefined) => {
let newID;
if (username) {
if (!id[type].hasOwnProperty(username)) {
// this is a new username
newID = id[type][username] = 0;
} else {
// this is a duplicate username
newID = id[type][username];
}
id[type][username] += 1;
} else {
// return the next id
newID = id[type];
id[type] += 1;
}
return newID;
}
// export these functions for external use
module.exports = { initialise, run }; The getID function manages the distribution of IDs for our messages and users. Finally, we export the two functions that we want to access from our main server/index.js file. Update server/index.js to include our new chat functionality, replacing the code we wrote in part 1 of this series. const express = require('express');
const http = require('http').Server(express);
const socketio = require('socket.io')(http, { pingTimeout: 60000 });
const chat = require('./chat');
const port = 3030;
chat.initialise(socketio);
socketio.on('connection', (socket) => {
// new socket connected
chat.run(socket);
});
http.listen(port, () => {
console.log('Server started on port', port);
}); After including the chat.js file we created, we pass the socketio instance to the initialise function. Then, when the connection event triggers we pass the new socket connection through to the run function. Update the Client Functionality Now that the server has been updated to handle our chat component, we need to update the client to store our chat data and respond to server events. Create src/store/chat.js and add the following code. This is the Vuex store for our chat room component. The store is a managed collection of data that we use as the middleman between the app front-end and the server. The front-end tells the store to send an event through the socket connection. When the app receives an event from the server through the socket connection, the store is updated with the new data. The app front-end only updates when there is new data in the store. import Vue from 'vue';
export default {
strict: false,
namespaced: true,
state: () => ({
users: [],
history: []
}), state is the data that the store is holding. In this case we have users and history being kept in the store. getters: {
GET_USERS: (state) => {
return state.users;
},
GET_HISTORY: (state) => {
return state.history;
}
}, The front-end uses getters to retrieve data from the store. mutations: {
SET_USERS(state, users) {
state.users = users;
},
ADD_USER(state, user) {
Vue.set(state.users, user.userID, user);
},
SET_HISTORY(state, history) {
state.history = history;
},
ADD_MESSAGE(state, message) {
Vue.set(state.history, message.id, message);
}
}, mutations are used to change the data in the store somehow. For example, we can set an object collection entirely using SET_HISTORY, or add a new element to a collection with ADD_USER. actions: {
RECEIVE_USERS({ commit }, users) {
commit('SET_USERS', users);
},
RECEIVE_USER({ commit }, user) {
commit('ADD_USER', user);
},
RECEIVE_HISTORY({ commit }, history) {
commit('SET_HISTORY', history);
},
RECEIVE_MESSAGE({ commit }, message) {
commit('ADD_MESSAGE', message);
}
}
}; actions are functions that can be called from the app front-end to perform a task, such as sending an event or receiving data. In our case, we would be calling an action from the front-end which in turn mutates our data in some fashion. Open src/store/index.js and edit the code until you have the following. import Vue from 'vue';
import Vuex from 'vuex';
import * as socketio from '../plugins/socketio';
import chat from './chat';
Vue.use(Vuex);
export default new Vuex.Store({
strict: false,
actions: {
SEND_EVENT({}, event) {
socketio.sendEvent(event);
}
},
modules: {
chat
}
}); Loading our new file into the store as a module allows us to maintain good object separation and the usage of namespaces to access particular parts of the store. We also added a SEND_EVENT action in this file, as all of our future components will need to send information to the server. Create src/mixins/addEventListener.js and add the following code. import * as socketio from '../plugins/socketio';
export const addEventListener = {
methods: {
addEventListener(eventType, eventCallback) {
socketio.addEventListener({
type: eventType,
callback: eventCallback
});
}
}
}; Mixins are essentially a reusable code snippet that can be loaded into any of our future components without the need to rewrite the code. This mixin defines a function addEventListener which allows our components to specify which server events they will listen for, and how to react. Create Chat Components That takes care of the app-specific functionality. Now we can jump into building the front-end for our chat room. How should we tackle this? First let’s lay out the structure that we are aiming for. We’ll make it so that the app asks you to connect with a username before you begin, and once you are connected, you’ll see the chat room with components in this order: User Login Chat log Messages Typing notifications Send a message Text field Send button List of connected users User chips User Login When a user first loads the app, they’ll be confronted with a simple login field for them to select a username to enter the chatroom with. Create src/components/ChatLogin.vue and add the following code:
Connect
This component contains a text field which takes the desired username, and handles emitting the data to the parent component. New Dependencies We can’t have a really nice chat room without some scrolling functionality. I think it would be ideal for the chat log to automatically scroll down when a new message arrives, but it would be annoying if you were manually scrolling back up to read an old message when that happened. Well, luckily we can save time and use an existing library to handle this for us! A bit of searching online found this nice little package called VueChatScroll. Install VueChatScroll: npm install --save vue-chat-scroll Import into src/main.js: import Vue from 'vue';
import App from './App.vue';
import store from './store';
import vuetify from './plugins/vuetify';
import moment from 'moment';
import VueChatScroll from 'vue-chat-scroll';
Vue.use(VueChatScroll);
Vue.config.productionTip = false;
Vue.prototype.moment = moment;
new Vue({
store,
vuetify,
render: (h) => h(App)
}).$mount('#app'); Chat Log The chat log will consist of messages and user typing notifications. It will be easiest if we work from the bottom up, so let’s start with individual messages. The easiest way to create a nice looking chat message is probably to use the v-alert component from Vuetify as a base and change up some settings. Create src/components/ChatMessage.vue and add the following code.
{{ message.sender.uniqueName }}
{{ message.text }}
{{ formatTime(message.time) }}
First up, we’ll create a template that uses the v-alert component. We’ll get some easy wins by setting the props to style the message component. The message will contain text and a timestamp, and show the sender name in some circumstances.
The getSetting method handles retrieving the appropriate setting based on the message type, and the formatTime method provides a nicely formatted timestamp. Create src/components/ChatLog.vue and add the following code.
keyboard_arrow_down
{{ formattedTypingStatus }}
The ChatLog component handles the display of all chat messages. The v-chat-scroll property of the div element enables the scroll functionality provided by the VueChatScroll package we installed earlier. There are also two conditional elements that may be rendered—a button which will scroll back to the bottom of the chat log, and the notifications for when another user is typing a message. There are three methods in the ChatLog component, one to determine the type of the message, and two involving the scrolling behaviour. onScroll triggers when the user scrolls the view, and calculates how far the log has scrolled from the bottom. Lastly, we’ll add a small amount of styling. The scroll-behavior css property will ensure the manual scrolling we added is smooth. Send a Message The messaging component of the app is fairly straightforward, with a simple v-textarea component. Create src/components/ChatSend.vue and add the following code:
Send
First we create the template with a text area component and button, then handle the send and is-typing message events. List of Connected Users Once the number of users increases a simple text list would become insufficient, so let’s use a v-chip to display each user and help keep things easier to read. Create src/components/ChatUserChip.vue and add the following code:
{{ chipSettings[user.type].icon }}
{{ user.uniqueName }}
First, create the template—a chip with a username and icon with colour determined by chipSettings. The chip takes one prop, a user object which contains relevant data. Create src/components/ChatUsers.vue and add the following code:
Connected Users
The template will display a chip for each user in the usersToDisplay computed property. The same computed property will also determine if the user is the current user, or one of the other users, which will change the way the chip is rendered. Bringing It All Together Now we’ve built all of the components that we need to make the chat log work, let’s put the pieces together to create the final component. Create src/components/Chat.vue and add the following code.
Connect to chat
Connected as {{ user.uniqueName }}
This template first checks to see if the user exists, and if not will display the ChatLogin component. If the user is logged in, we’ll display their username and then show the ChatLog, ChatSend, and ChatUsers components. Lastly, add some methods to handle sending messages to the server. Update src/components/RealTimeDemo.vue with the following code.
Finally, we can add our newly created Chat component to the RealTimeDemo component. We can now test our new functionality. Start the server with the command node server, and open the app in your browser. Enter a username and send some chat messages. Open the app multiple times in different browser tabs to see how it handles multiple connections. Stop the server by pressing the keys ctrl+c. You can find this project in its entirety on GitHub. Header image courtesy of NASA on Unsplash Related Articles Blog / Personalisation in Design Blog / Lessons learned from implementing Face Recognition for a Virtual Receptionist Blog / Coding Standard conformance in Open-Source Data Science projects Artificial intelligence built by humans, for humans We believe in the revolutionary impact of humans empowered by AI. Home About Research Publications Industry Projects Work with us Study with us News Blog Contact Deakin University CRICOS Provider Code: 00113B