Chat-app Creation in the Real-Time Mode Using Vue.js, Nuxt.js, Node.js (Express), Socket.IO, Vue-Socket.IO, Vuetify.js Technologies.

Chat-app Creation in the Real-Time Mode Using Vue.js, Nuxt.js, Node.js (Express), Socket.IO, Vue-Socket.IO, Vuetify.js Technologies.

Hello everybody. Recently I’ve got a desire to master the Socket.IO library and create a chat application, to strengthen theoretical knowledge with practice, so to say. I actively use the technological stack which is implemented in the application in my work on commercial projects, but for Socket,IO.

It’s easy to inject the above-mentioned library into the already working project, but today I am going to speak about creating an app from scratch.

Let’s proceed, I do not like long forewords myself.

Setting up and installation of the Nuxt.js generic template.

  • You need to have Node.js installed, otherwise - install it.
  • If your NPM version is under 5.2 - install npx globally, using the rights of admin
  • $sudo npm install -g npx.

  • After that create a project with the help of the following command:
  • $npx create-nuxt-app < project-name >

    (here you will find more information on project creation).

  • Then a project configuration menu will appear (I used my project details):
    1. Project name - “Nuxt-chat-app”
    2. Description - “Simple chat with Nuxt.js”
    3. Author’s name - “Petr Borul”
    4. Package manager - “NPM”
    5. UI framework - “Vuetify.js”
    6. Server Framework - “Express”
    7. Nuxt.js modules - “PWA”
    8. Linting tools - “ESLint”
    9. Test framework - “none”
    10. Rendering mode - “Universal”
  • Let’s install SOCKET.IO:
  • $npm install socket.io --save

  • I also used a wrapper for SOCKET.IO - Vue.SOCKET.IO.
  • In this library, you can call the websocket events and subscribe to them through the Vuex store, but for the hands-on review of the library, it is too implicit. That’s why I implemented the logic at the component level (here is a description of the two possible ways of its implementation).

    $npm install vue-socket.io --save

The full information on Nuxt.js folders’ structure you can find here.

.

The main points:

  • The folder pages contains views and routes. The framework reads all .vue files inside the folder and creates a router for the application.
  • The folder plugins contains JavaScript-plugins, which are run before the root application Vue.js creation (here our plugin socket.io will be resided).
  • The folder middleware contains the intermediate processing functions (the named ones are created in this folder, and if you want to specify the anonymous ones - you can declare them inside the component).
  • The file nuxt.config.js contains Nuxt.js user configuration.
  • The folder store contains the files of Vuex container. After index.js file creation in this folder, the container is activated automatically.

So, we have dealt with the main notions, let’s proceed to the app development itself. The folder contains the file index.js — we’ll change it a bit and take the server configuration to a separate file app.js.

const app = require('express')();
const server = require('http').createServer(app);
const io = require('socket.io')(server);

We’ll add server configuration to index.js:

index.js

const { app, server } = require('./app');

Then we’ll order Node.js to listen to the server configured:

server.listen(port, () => {
   consola.ready({
     message: `Server listening on http://${host}:${port}`,
     badge: true
   })
 })

Further on we create the file socket.client.js and add it to the folder plugins, we indicated the file extension ‘client’, because we need it only on the client-side (Here you can find all the info as to the plugin’s adjustments).

socket.client.js

import Vue from 'vue'
import VueSocketIO from 'vue-socket.io'
 
export default function () {
 Vue.use(new VueSocketIO({
   debug: false,
   connection: '/',
 }))
}

Now we’ll register it in the nuxt.config.js file:

 plugins: [
   { src: '~/plugins/socket.client.js' }
 ],

From this point on you can refer to it in any component, using only the name of the file this.$socket.emit().

In the app.js file we’ll create two models of working with the data:

const users = require('../utils/users')();
const Message = require('../utils/message')();

message.js

class Message {
 constructor(name, text, id) {
   this.name = name;
   this.text = text;
   this.id = id;
   this.time = new Date().toString().slice(15, 24);
 }
}
 
module.exports = () => {
 return Message
}

users.js

class Users {
 constructor() {
   this.users = [];
 }
 
 addUser(user) {
   this.users = [...this.users, user]
 }
 
 getUser(id) {
   return this.users.find(user => user.id === id);
 }
 
 getUsersByRoom(room) {
   return this.users.filter(user => user.room === room);
 }
 
 removeUser(id) {
   this.users = this.users.filter(user => user.id !== id);
 }
}
 
module.exports = () => {
 return new Users()
}

We have finished with the server at this point and now we’ll proceed to the client side. In the folder store we’ll create index.js file and add the store

index.js

export const state = () => ({
 user: {},
 messages: [],
 users: []
})
 
export const mutations = {
 setUser(state, user) {
   state.user = user;
 },
 newMessage(state, msg) {
   state.messages = [...state.messages, msg];
 },
 updateUsers(state, users) {
   state.users = users;
 },
 clearData(state) {
   state.user = {};
   state.messages = [];
   state.users = [];
 },
}

Further on we’ll add a layout to the file index.js in folder layouts (I use UI library Vuetify.js because of the Material Design, which I like very much).

index.js

<template>
 <v-layout column justify-center align-center>
   <v-flex xs12 sm8>
     <v-card min-width="370">
       <v-snackbar v-model="snackbar" :timeout="3000" top>
         {{ message }}
         <v-btn dark text @click="snackbar = false">Close</v-btn>
       </v-snackbar>
 
       <v-card-title>
         <h1>Login</h1>
       </v-card-title>
       <v-card-text>
         <v-form ref="form" v-model="valid" lazy-validation @submit.prevent="submit">
           <v-text-field
             v-model="name"
             :counter="16"
             :rules="nameRules"
             label="Name"
             required
           ></v-text-field>
           <v-text-field
             v-model="room"
             :rules="roomRules"
             label="Enter the room"
             required
           ></v-text-field>
           <v-btn :disabled="!valid" color="primary" class="mr-4" type="submit">Submit</v-btn>
         </v-form>
       </v-card-text>
     </v-card>
   </v-flex>
 </v-layout>
</template>
 
<script>
import { mapMutations } from "vuex";
 
export default {
 name: "index",
 layout: "login",
 head: {
   title: "Nuxt-chat"
 },
 data: () => ({
   valid: true,
   name: "",
   message: "",
   id: null,
   nameRules: [
     v => !!v || "Name is required",
     v => (v && v.length <= 16) || "Name must be less than 16 characters"
   ],
   room: "",
   roomRules: [v => !!v || "Enter the room"],
   snackbar: false
 }),
 mounted() {
   const { message } = this.$route.query;
   if (message === "noUser") {
     this.message = "Enter your name and room";
   } else if (message === "leftChat") {
     this.message = "You leaved chat";
   }
   this.snackbar = !!this.message;
 },
 
 methods: {
   ...mapMutations(["setUser"]),
   submit() {
     if (this.$refs.form.validate()) {
       const user = {
         name: this.name,
         room: this.room,
         id: 0
       };
       this.$socket.emit("createUser", user, data => {
         user.id = data.id;
         this.setUser(user);
         this.$router.push("/chat");
       });
     }
   }
 }
};
</script>

When the submit () method is called, the form is validated, and in case of success, we send the event to the server this.$socket.emit().

We send a String with the name of the event to the server, and a callback function, after the fulfillment of which we get an ID and assign it to the user’s object, then we write it down to the state and send it to the chat page.

Let’s describe the event processing on the server:

io.on('connection', socket => {
 socket.on("createUser", (user, cb) => {
   users.addUser({
     ...user,
     id: socket.id
   })
   cb({ id: socket.id })
 });
})
  1. The event “connection” is called when the user gets the connection with the server.
  2. Then we subscribe to the event received from the client with the help of socket.on().
  3. This function accepts the String and the callback function.
  4. We add a new user to the users’ list and assign ID the corresponding ID socket for connection.
  5. We pass the ID on to the client’s side.

Now we’ll create the layout of the default.vue file in the layouts folder, it’s set by default for all the components in the folder pages if the layout is not indicated (here you’ll find the detailed information).

default.vue

<template>
 <v-app>
   <v-navigation-drawer app v-model="drawer" mobile-break-point="650">
     <v-list subheader>
       <v-subheader>Users in room</v-subheader>
 
       <v-list-item v-for="(u, index) in users" :key="`user-${index}`" @click.prevent>
         <v-list-item-content>
           <v-list-item-title v-text="u.name"></v-list-item-title>
         </v-list-item-content>
 
         <v-list-item-icon>
           <v-icon :color="u.id === user.id ? 'primary' : 'grey'">mdi-account-circle-outline</v-icon>
         </v-list-item-icon>
       </v-list-item>
     </v-list>
   </v-navigation-drawer>
 
   <v-app-bar app>
     <v-app-bar-nav-icon @click="drawer = !drawer"></v-app-bar-nav-icon>
     <v-toolbar-title>
       Room
       <v-chip color="grey">{{ user.room }}</v-chip>
     </v-toolbar-title>
     <v-spacer></v-spacer>
     <v-btn icon class="mx-1" @click="exit">
       <v-icon>mdi-exit-to-app</v-icon>
     </v-btn>
   </v-app-bar>
 
   <v-content>
     <v-container fluid style="height: 100%">
       <nuxt />
     </v-container>
   </v-content>
 </v-app>
</template>
 
<script>
import { mapState, mapMutations } from "vuex";
 
export default {
 data: () => ({
   drawer: true
 }),
 sockets: {
   updateUsers(users) {
     this.updateUsers(users);
   },
   newMessage(msg) {
     this.newMessage(msg);
   },
 },
 computed: {
   ...mapState(["user", "users"])
 },
 middleware: "auth",
 methods: {
   ...mapMutations(["clearData", "updateUsers", "newMessage"]),
   exit() {
     this.$socket.emit("userLeft", () => {
       this.$router.push("/?message=leftChat");
       this.clearData();
     });
   }
 },
 created() {
   this.$socket.emit("joinRoom", this.user)
 }
};
</script>

The tag is responsible for views on various routes

(read here more detailed info about the views in Nuxt.js).

The object sockets is responsible for processing of the events, which are called on the server side.

Let’s add subscription for 2 events “updateUsers” and “newMessage”. Then we’ll add the method exit(), which will be called with an exit button click and in which we will send the event “leftChat” to the server. Then the user will be redirected to the registration form from the query on the route for message display in the snackbar.

Let’s process this event on the server:

app.js

socket.on('leftChat', (cb) => {
   const id = socket.id;
   const user = users.getUser(id);
   if (user) {
     users.removeUser(id);
     socket.leave(user.room);
     io.to(user.room).emit('updateUsers', users.getUsersByRoom(user.room));
     io.to(user.room).emit('newMessage', new Message('admin', `User ${user.name} left chat`))
   }
   cb()
 });

Now we’ll create a file auth.js in the middleware folder and add an ntermediate processing function to the component, so that only an authorized user could get on the chat page.

auth.js (open and close on a click on the file name):

export default function({ store, redirect }) {
 if(!Object.keys(store.state.user).length) {
   redirect('/?message=noUser')
 }
}

Also, with the initialization of the component we send the event “joinRoom” to the server and send the user data as a payload into the feedback function.

Let’s process this event on the server:

app.js

 socket.on("joinRoom", user => {
   socket.join(user.room);
   io.to(user.room).emit('updateUsers', users.getUsersByRoom(user.room));
   socket.emit('newMessage', new Message('admin', `Hello, ${user.name}`));
   socket.broadcast
     .to(user.room)
     .emit('newMessage', new Message('admin', `User ${user.name} connected to chat`));
 });
  • We add the user to the room, which he indicated during the authorization;
  • then we call the event “updateUsers” for all the users of the room;
  • and call the event “newMessage” only for the user, who has called the event “joinRoom”;
  • We call the event “newMessage” for all the users, except for the current user ( notify the other users about the new user, who joined).

Further on we’ll add the chat layout.

chat.vue

<template>
 <div class="chat-wrapper">
   <div class="chat" ref="chat">
     <Message
       v-for="(message,index) in messages"
       :key="`message-${index}`"
       :name="message.name"
       :text="message.text"
       :time="message.time"
       :owner="message.id === user.id"
     />
   </div>
   <div class="chat__form">
     <ChatForm />
   </div>
 </div>
</template>
 
<script>
import { mapState, mapMutations } from "vuex";
import Message from "@/components/message";
import ChatForm from "@/components/ChatForm";
 
export default {
 components: {
   Message,
   ChatForm
 },
 head() {
   return {
     title: `Room ${this.user.room}`
   };
 },
 methods: {
   ...mapMutations(["newMessage"])
 },
 computed: {
   ...mapState(["user", "messages"])
 },
 watch: {
   messages() {
     setTimeout(() => {
       if (this.$refs.chat) {
         this.$refs.chat.scrollTop = this.$refs.chat.scrollHeight;
       }
     }, 0);
   }
 }
};
</script>

I have omitted the section with styles, for you to concentrate on the logic. The component, which is responsible for message rendering is

Message.vue

<template>
 <div>
   <div v-if="name === 'admin'" class="system">
     <p class="text-center font-italic">{{ text }}</p>
   </div>
   <div v-else class="msg-wrapper">
     <div class="msg" :class="{owner}">
       <div class="msg__information">
         <span class="msg__name">{{ name }}</span>
         <span class="msg__date">{{ time }}</span>
       </div>
       <p class="msg__text">{{ text }}</p>
     </div>
   </div>
 </div>
</template>
 
<script>
export default {
 props: {
   name: String,
   text: String,
   time: String,
   owner: Boolean
 }
};
</script>

The styles are adjusted in the same way as the previous component.

The component for message realization and sending is

ChatForm.vue

<template>
 <v-text-field
   ref="msg"
   label="Message..."
   outlined
   v-model="text"
   @click:append="send"
   @keydown.enter="send"
   append-icon="mdi-send-circle-outline"
 />
</template>
 
<script>
import { mapState } from "vuex";
 
export default {
 data: () => ({
   text: "",
   roomRules: [v => !!v || "Enter the room"]
 }),
 computed: {
   ...mapState(["user"])
 },
 methods: {
   send() {
     if (this.text.length) {
       this.$socket.emit(
         "createMessage",
         {
           text: this.text,
           id: this.user.id
         },
         data => {
           this.text = "";
         }
       );
     }
   }
 }
};
</script>

When a form is verified - we send an event “createMessage” to the server, send the message text and the user ID, after the feedback function, we clear the field.

Now we’ll process this event on the server:

app.js

socket.on('createMessage', (data, cb) => {
   const user = users.getUser(data.id);
   if (user) {
     io.to(user.room).emit('newMessage', new Message(user.name,     data.text, data.id))
   }
   cb()
 });

We’ll add the subscription in case the connection fails and it will be possible to add the reconnect possibility later on.

app.js

socket.on('disconnect', () => {
   const id = socket.id;
   const user = users.getUser(id);
   if (user) {
     users.removeUser(id);
     socket.leave(user.room);
     io.to(user.room).emit('updateUsers', users.getUsersByRoom(user.room));
     io.to(user.room).emit('newMessage', new Message('admin', `User ${user.name} left chat`))
   }
 });

By now it’s the final part of the app. You can launch the local server with the help of the command:

$npm run dev

Preview

Github

As you can see the Socket.IO library is very simple and easy to use. After the development had been finished I had a desire to deploy the app and share the demo version of it with you. I spent some time on the search of the suitable free service, which supports WebSockets. Finally, I chose Heroku. Nuxt.js manuals have a detailed guide about how to deploy an app onto this service.

Demo

Thanks for your attention.

See you next time!