Concept

A couple of weeks ago, I was asked to create a simple communicator for one of our clients. I had to dig deeper into the topic and I decided to create a very basic proof of concept, without all the shiny features, a straightforward chat application that implements “one-to-many” communication. I wanted to use technology that I already knew, so the client application was created in Vue.js using Vue-cli 3, and the server was created in Node.js. For development purposes, I used the Express web server, which is flexible and fast, just enough for MVP. What I’d like to show you is how fast we can achieve this goal in terms of configuration and coding.

Getting started

Let’s start with our server. I assume we already have Node.js and package manager like Npm or Yarn, I will be using Yarn in this example. All we really need is two packages, let’s get them:

yarn add express
yarn add socket.io

Now, add a new `server.js` file, where we will create a new instance of the Express server.

'use strict';

const express = require('express');
const io = require('socket.io');

class Server {
  constructor() {
    this.port = 8081;
    this.app = express();
    this.server = this.app.listen(this.port, () => {
      console.log(`server running http://localhost:${this.port}`);
    });
    this.io = io(this.server);
  }
}

new Server();

At the beginning of the snippet, we include two previously installed packages, then we create a new instance of the Express server in our constructor that will listen to localhost communication on port 8081. We don’t need to specify the port, Express will automatically choose a free port for us. However, in this case we want to explicitly set the port for our server, so that it stays the same all the time. Finally, we hook up Socket.io to our server with the io() function. At this point, the server doesn’t do anything spectacular, it’s only listening to connections.

We can start the server by running:

node server

Let’s now jump to our client application. First, we need to globally install Vue-cli:

yarn global add @vue/cli

Now, we can create a new application by typing:

vue create chat-client

We will be asked to pick up a preset, I chose the default babel, eslint. Our project will be set up and we can run it by typing:

yarn serve

We will also need the Socket.io package for the client, we can add it by typing:

yarn add socket.io-client

I personally like working with sass, so I additionally added two packages required to write scss code in the Vue environment:

yarn add node-sass --dev
yarn add sass-loader --dev

At this point, we are ready for some serious coding. Let’s get straight into it!

Diving deeper into the server

Now, we need to create a new file with all the listeners. I created three events that are passed from our client to the server:

  • connected we store user data and emit a users event that keeps all users data for all connected clients
  • disconnect we remove a user from users object and also emit a users event to update clients
  • message we inform all connected clients about a new message that has been sent

Here is what I ended up with:

'use strict';

class Events {
  constructor(app, io){
    this.io = io;
    this.users = {};
  }

  socketEvents(io) {
    io.on('connection', (socket) => {
      socket.on('connected', (user) => {
        this.users[socket.id] = user;
        io.emit('users', this.users);
      });

      socket.on('disconnect', () => {
        delete this.users[socket.id];
        io.emit('users', this.users);
      });

      socket.on('message', (message) => {
        io.emit('message', message);
      });
    });
  }

  eventsConfig() {
    this.socketEvents(this.io);
  }
}

module.exports = Events;

Now, we need to add our events to the server.js file, as below:

const events = require('./events');

Also, we should evoke the constructor, passing our socket object, and then trigger the eventsConfig function:

new events(this.io).eventsConfig();

so that the final server.js file looks like this:

'use strict';

const express = require('express');
const io = require('socket.io');
const events = require('./events');

class Server {
  constructor() {
    this.port = 8081;
    this.app = express();
    this.server = this.app.listen(this.port, () => {
      console.log(`server running http://localhost:${this.port}`);
    });
    this.io = io(this.server);
    new events(this.io).eventsConfig();
  }
}

new Server();

That is all we need for the server application.

Client application

For our client application, I created three components:

  • ChatUserList.vue for keeping chat participants list
  • ChatConversation.vue for keeping all chat messages
  • App.vue that binds everything together

ChatUserList.vue

It consist of a simple “for” loop with users, and when a component is mounted we also create two listeners users and disconnect that will react to events emitted by the server. We must pass a socket object as a property to this component, as soon as the socket is created. This is what the file looks like in its final shape:

<template>
  <div class="chat-user-list">
    <div class="chat-user-list__user" v-for="user in users" v-bind:key="user.name">
      {{ user.name }}
    </div>
  </div>
</template>

<script>
export default {
  name: 'chat-user-list',
  props: {
    socket: {
      type: Object,
      required: true
    }
  },
  data() {
    return {
      users: {}
    };
  },
  mounted() {
    this.socket.on('users', (users) => {
      this.users = users;
    });
    this.socket.on('disconnect', () => {
      this.users = {};
    });
  }
}
</script>

ChatConversation.vue

This component aggregates all messages sent from every user in a messages array. As previously, it also requires the socket object to be passed via props. It mounts two listeners as well: message, which pushes a new message to the messages array, and disconnect that clears the messages array after the client socket is disconnected.

<template>
  <div class="chat-conversation">
    <div
      class="chat-conversation__message"
      v-for="message in messages"
      :message="message"
      v-bind:key="message.id">
      <strong>{{ message.user.name }}: </strong>{{ message.message }}
    </div>
  </div>
</template>

<script>
export default {
  name: 'ChatConversation',
  props: {
    socket: {
      type: Object,
      required: true
    }
  },
  data() {
    return {
      messages: []
    };
  },
  mounted() {
    this.socket.on('message', (data) => {
      this.messages.push(data);
    });
    this.socket.on('disconnect', () => {
      this.messages = [];
    });
  }
}
</script>

App.vue

This is the most important part of the client application. First of all, it ties up our components, but what is more important we create a connection to our server. When the component is created, we call the native javascript function prompt() and ask for a username.

native javascript prompt function

When the username is provided, we create a connection to the server and broadcast the connect event. The server can respond to it and broadcast the new user list that needs to be updated to all users. After the username is provided and we connected to the server, the chat window is displayed. At the bottom, there is a text input that will transport our message to the server and a send button. Additionally, I handled the enter key event so that the messages can be sent either by pressing the enter key or by clicking the send button.

Before the component is destroyed, we broadcast a disconnected event in order to let the server know that we are no longer connected and it’s the right time to update other users with that information.

<template>
  <div class="chat">
    <div class="chat__wrapper" v-if="user.name">
      <div class="chat__conversation">
        <chat-conversation :socket="socket">
        </chat-conversation>
        <div class="chat__controls">
          <input @keyup="onKeyUp" class="chat__input" v-model="message" />
          <button class="chat__button" @click="sendMessage">Send</button>
        </div>
      </div>
      <div class="chat__participants">
        <chat-user-list :socket="socket"></chat-user-list>
      </div>
    </div>
    <div class="chat__error" v-else>
      Error, reload page and provide valid username!
    </div>
  </div>
</template>

<script>
import io from 'socket.io-client';
import ChatConversation from './components/ChatConversation.vue';
import ChatUserList from './components/ChatUserList.vue';

export default {
  name: 'app',
  components: {
    ChatConversation,
    ChatUserList
  },
  data() {
    return {
      message: '',
      socket: null,
      user: {
        name: null
      }
    };
  },
  created() {
    this.user.name = prompt('Please enter your username:', '');
    if (this.user.name) {
      this.socket = io('http://localhost:8081');
      this.socket.on('connect', () => {
        this.connect();
      });
    }
  },
  methods: {
    connect() {
      this.socket.emit('connected', this.user);
    },
    sendMessage() {
      this.socket.emit('message', {
        user: this.user,
        message: this.message
      });
      this.message = '';
    },
    onKeyUp(event) {
      if (event.keyCode === 13) {
        this.sendMessage();
      }
    }
  },
  beforeDestroy() {
    if (this.socket) {
      this.socket.emit('disconnected', {
        user: this.user
      });
    }
  },
}
</script>

And that is pretty much it. As you can see, it’s straightforward and self-explanatory. I hope that after reading this article you will be able to create something even more fascinating using the Socket.io library. Here are two GitHub repositories with the client and server applications, check them out and feel free to ask me any questions at marcin.kunysz@fingo.pl