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

Chat-app Creation in the Real-Time Mode Using Vue.js, Nuxt.js, Node.js, 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 >

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.

$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 https://${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.

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.

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

Let’s add a 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 intermediate 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!

Originally published at Stfalcon.com.

要查看或添加评论,请登录

Stepan Tanasiychuk的更多文章

  • How to Build Property Management Software for Real Estate

    How to Build Property Management Software for Real Estate

    According to Grand View Research, the global proptech market is expected to grow at a CAGR of 8.9% from 2024 to 2030.

  • Top Software Development Risks and How to Remove Them | Stfalcon

    Top Software Development Risks and How to Remove Them | Stfalcon

    Software development risks can jeopardize a project's success. Common risks include schedule delays, budget overruns…

  • How to Make a Real Estate Website | Stfalcon

    How to Make a Real Estate Website | Stfalcon

    Real estate has traditionally been all about location, location, location. However, in today's digital age, having an…

  • How To Build A Custom CRM System for Business | Stfalcon

    How To Build A Custom CRM System for Business | Stfalcon

    Are you exhausted from managing multiple spreadsheets and losing track of customer data? You're not alone. According to…

    2 条评论
  • How to Make a Language Learning Application | Stfalcon

    How to Make a Language Learning Application | Stfalcon

    Modern language learning is increasingly moving away from traditional classrooms, teachers, and textbooks. Many…

    1 条评论
  • How to Create a Fitness App

    How to Create a Fitness App

    Fitness mobile apps aim to promote and encourage physical activity. These tools can be downloaded onto mobile devices…

    2 条评论
  • How to Build a Billing Software

    How to Build a Billing Software

    Ensuring a smooth and enjoyable customer journey from their first visit to your store through billing and invoicing is…

    1 条评论
  • How to Create a Delivery App

    How to Create a Delivery App

    In today's rapidly evolving world, convenience reigns supreme, and the surging popularity of delivery applications is a…

    2 条评论
  • How to Create an Investment App: Detailed Guide

    How to Create an Investment App: Detailed Guide

    Are you curious about how those sleek investment apps on your phone operate? The ones that allow you to track your…

    2 条评论
  • How to Build a Personal Finance App Like Mint

    How to Build a Personal Finance App Like Mint

    If managing your budget drives you as crazy as it does most people, and you struggle to keep track of your earnings…

    2 条评论

社区洞察

其他会员也浏览了