Make A Real-Time Chat Room using Node Webkit, Socket.io, MEAN

Make A Real-Time Chat Room using Node Webkit, Socket.io, MEAN

  • 2016-08-04
  • 592

Overview

Development folks work tirelessly to make building programs as easy as possible. The JavaScript, Web and Mobile app developers communities increased drastically since Node and Cordova were introduced. Developers who had web design skills could, with less effort, roll out a server using JavaScript for their applications, through the help of Node.js.

Mobile lovers can with the help of Cordova now build rich hybrid apps using just JavaScript. Today, although it is old news, I am excited to share the ability of using JavaScript to build desktop standalone applications.

Node Webkit normally written “node-webkit” or “NW.js” is an app runtime based on Node.js and Chromium and enables us to develop OS native apps using just HTML, CSS and JavaScript.

Simply put, Node Webkit just helps you utilize your skill as a web developer to build native application that runs comfortably on Mac, Windows and Linux with just a grunt/gulp (if preferred) build command.

This article concentrates a lot more on using Node Webkit, but in order to make things more interesting, we will be including other amazing solutions and they will include:

  • Socket.io A realtime library for Node.js
  • Angular Material: Angular’s implementation of Google’s Material Design
  • MEAN: MEAN is just a concept of combining the features of Mongo, Express, Angular and Node to build powerful apps

Furthermore, the application has three sections:

  • The server
  • The desktop (client)
  • The web (client)

The web section will not be covered here, but it will serve as a test platform but don’t worry, the code will be provided.

Prerequisites

Level: Intermediate (Knowledge of MEAN is required)

Installation

We need to grab node-webkit and every other dependencies for our application. Fortunately, there are frameworks that make workflow easy and we will be using one of them to scaffold our application and concentrate more on the implementation.

Yo and Slush are popular generators and any of these will work. I am going to be using Slush, but feel free to use Yo if you prefer to. To install Slush make sure you have node and npm installed and run

$ npm install -g slush gulp bower slush-wean

The command will install the following globally on our system.

  • slush: a scaffolding tool
  • slush-wean: the generator for Node Webkit
  • gulp: our task runner
  • bower: for frontend dependencies

Just like YO, make your directory and scaffold your app using:

$ mkdir scotch-chat
$ cd scotch-chat
$ slush wean

Running the below command will give us a glance of what we have been waiting for:

 $ gulp run
Automate JavaScript workflow with Gulp

The image shows our app loading. The author of the generator was generous enough to provide a nice template with simple loading animation. To look cooler, I replaced the loading text with Scotch’s logo.

If you are not comfortable with Slush automating things you can head right to node webkit on GitHub or watch the beginners video series.

Now that we have setup our app, though empty, we will give it a break and prepare our server now.

The Server

The server basically consists of our model, routes and socket events. We will keep it as simple as possible and you can feel free to extend the app as instructed at the end of the article.

Directory Structure

Setup a folder in your PC at your favorite directory, but make sure the folder content looks like the below:

|- public
        |- index.html
    |- server.js
    |- package.json

Dependencies

In the package.json file located on your root directory, create a JSON file to describe your application and include the application’s dependencies.

{
      "name": "scotch-chat",
      "main": "server.js",
      "dependencies": {
        "mongoose": "latest",
        "morgan": "latest",
        "socket.io": "latest"
      }
    }

That will do. It is just a minimal setup and we are keeping things simple and short. Run npm install on the directory root to install the specified dependencies.

$ npm install

Starting Our Server Setup

Huuugh. It is time to get our hands dirty! The first thing is to setup global variables in server.js which will hold the applications dependencies that are already installed.

// server.js

    // Import all our dependencies
    var express  = require('express');
    var mongoose = require('mongoose');
    var app      = express();
    var server   = require('http').Server(app);
    var io       = require('socket.io')(server);

Ok, I didn’t keep to my word. The variables are not only holding the dependencies, but some are configuring it for use.

To serve static files, express exposes a method to help configure the static files folder. It is simple:

// server.js

    ...

    // tell express where to serve static files from
    app.use(express.static(__dirname + '/public'));

Next up is to create a connection to our database. I am working with a local Mongo DB which obviously is optional as you can find it’s hosted by Mongo databases. Mongoose is a node module that exposes amazing API which makes working with Mongo DB a lot much easier.

 // server.js

    ...

 mongoose.connect("mongodb://127.0.0.1:27017/scotch-chat");

With Mongoose we can now create our database schema and model. We also need to allow CORS in the application as we will be accessing it from a different domain.

// server.js

    ...

    // create a schema for chat
    var ChatSchema = mongoose.Schema({
      created: Date,
      content: String,
      username: String,
      room: String
    });

    // create a model from the chat schema
    var Chat = mongoose.model('Chat', ChatSchema);

    // allow CORS
    app.all('*', function(req, res, next) {
      res.header("Access-Control-Allow-Origin", "*");
      res.header('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE,OPTIONS');
      res.header('Access-Control-Allow-Headers', 'Content-type,Accept,X-Access-Token,X-Key');
      if (req.method == 'OPTIONS') {
        res.status(200).end();
      } else {
        next();
      }
    });

Our server will have three routes in it. A route to serve the index file, another to setup chat data and the last to serve chat messages filtered by room names:

// server.js

    /*||||||||||||||||||||||ROUTES|||||||||||||||||||||||||*/
    // route for our index file
    app.get('/', function(req, res) {
      //send the index.html in our public directory
      res.sendfile('index.html');
    });

    //This route is simply run only on first launch just to generate some chat history
    app.post('/setup', function(req, res) {
      //Array of chat data. Each object properties must match the schema object properties
      var chatData = [{
        created: new Date(),
        content: 'Hi',
        username: 'Chris',
        room: 'php'
      }, {
        created: new Date(),
        content: 'Hello',
        username: 'Obinna',
        room: 'laravel'
      }, {
        created: new Date(),
        content: 'Ait',
        username: 'Bill',
        room: 'angular'
      }, {
        created: new Date(),
        content: 'Amazing room',
        username: 'Patience',
        room: 'socet.io'
      }];

      //Loop through each of the chat data and insert into the database
      for (var c = 0; c < chatData.length; c++) {
        //Create an instance of the chat model
        var newChat = new Chat(chatData[c]);
        //Call save to insert the chat
        newChat.save(function(err, savedChat) {
          console.log(savedChat);
        });
      }
      //Send a resoponse so the serve would not get stuck
      res.send('created');
    });

    //This route produces a list of chat as filterd by 'room' query
    app.get('/msg', function(req, res) {
      //Find
      Chat.find({
        'room': req.query.room.toLowerCase()
      }).exec(function(err, msgs) {
        //Send
        res.json(msgs);
      });
    });

    /*||||||||||||||||||END ROUTES|||||||||||||||||||||*/

The first route I believe is easy enough. It will just send our index.html file to our users.

The second /setup is meant to be hit just once and at the initial launch of the application. It is optional if you don’t need some test data. It basically creates an array of chat messages (which matches the schema), loops through them and inserts them into the database.

The third route /msg is responsible for fetching chat history filtered with room names and returned as an array of JSON objects.

The most important part of our server is the realtime logic. Keeping in mind that we are working towards producing a simple application, our logic will be comprehensively minimal. Sequentially, we need to:

  • Know when our application is launched
  • Send all the available rooms on connection
  • Listen for a user to connect and assign him/her to a default room
  • Listen for when he/she switches room
  • And, finally, listen for a new message and only send the message to those in the room at which it was created

Therefore:

 // server.js

    /*||||||||||||||||SOCKET|||||||||||||||||||||||*/
    //Listen for connection
    io.on('connection', function(socket) {
      //Globals
      var defaultRoom = 'general';
      var rooms = ["General", "angular", "socket.io", "express", "node", "mongo", "PHP", "laravel"];

      //Emit the rooms array
      socket.emit('setup', {
        rooms: rooms
      });

      //Listens for new user
      socket.on('new user', function(data) {
        data.room = defaultRoom;
        //New user joins the default room
        socket.join(defaultRoom);
        //Tell all those in the room that a new user joined
        io.in(defaultRoom).emit('user joined', data);
      });

      //Listens for switch room
      socket.on('switch room', function(data) {
        //Handles joining and leaving rooms
        //console.log(data);
        socket.leave(data.oldRoom);
        socket.join(data.newRoom);
        io.in(data.oldRoom).emit('user left', data);
        io.in(data.newRoom).emit('user joined', data);

      });

      //Listens for a new chat message
      socket.on('new message', function(data) {
        //Create message
        var newMsg = new Chat({
          username: data.username,
          content: data.message,
          room: data.room.toLowerCase(),
          created: new Date()
        });
        //Save it to database
        newMsg.save(function(err, msg){
          //Send message to those connected in the room
          io.in(msg.room).emit('message created', msg);
        });
      });
    });
    /*||||||||||||||||||||END SOCKETS||||||||||||||||||*/

Then the traditional server start:

  // server.js
    server.listen(2015);
    console.log('It\'s going down in 2015');

Fill the index.html with any HTML that suites you and run node server.js. localhost:2015 will give you the content of your HTML.

The Node Webkit Client

Time to dig up what we left to create our server which is running currently. This section is quite easy as it just requires your everyday knowledge of HTML, CSS, JS and Angular.

Automate JavaScript workflow with Gulp

Directory Structure

We don’t need to create any! I guess that was the inspiration of generators. The first file you might want to inspect is the package.json.

Node Webkit requires, basically, two major files to run:

  1. an entry point (index.html)
  2. a package.json to tell it where the entry point is located

package.json has the basic content we are used to, except that it’s main is the location of the index.html, and it has a set of configuration under "window": from which we define all the properties of the app’s window including icons, sizes, toolbar, frame, etc.

Dependencies

Unlike the server, we will be using bower to load our dependencies as it is a client application. Update your bower.json dependencies to:

"dependencies": {
      "angular": "^1.3.13",
      "angular-material" : "^0.10.0",
      "angular-socket-io" : "^0.7.0",
      "angular-material-icons":"^0.5.0",
      "animate.css":"^3.0.0"
    }

For a shortcut, just run the following command:

$ bower install --save angular angular-material angular-socket-io angular-material-icons animate.css

Now that we have our frontend dependencies, we can update our views/index.ejs to:

<!-- index.ejs -->    
<html><head>
    <title>scotch-chat-1</title>
    <link rel="stylesheet" href="css/app.css">
    <link rel="stylesheet" href="css/animate.css">
    <link rel="stylesheet" href="libs/angular-material/angular-material.css">

    <script src="libs/angular/angular.js"></script>
    <script src="http://localhost:2015/socket.io/socket.io.js"></script>
    <script type="text/javascript" src="libs/angular-animate/angular-animate.js"></script>
    <script type="text/javascript" src="libs/angular-aria/angular-aria.js"></script>
    <script type="text/javascript" src="libs/angular-material/angular-material.js"></script>
    <script type="text/javascript" src="libs/angular-socket-io/socket.js"></script>
    <script type="text/javascript" src="libs/angular-material-icons/angular-material-icons.js"></script>

    <script src="js/app.js"></script>
</head>
<body ng-controller="MainCtrl" ng-init="usernameModal()">
    <md-content>
        <section>
            <md-list>
                <md-subheader class="md-primary header">Room: {{room}} <span align="right">Userame: {{username}} </span> </md-subheader>

                <md-whiteframe ng-repeat="m in messages" class="md-whiteframe-z2 message" layout layout-align="center center">
                    <md-list-item class="md-3-line">
                        <img ng-src="img/user.png" class="md-avatar" alt="User" />
                        <div class="md-list-item-text">
                            <h3>{{ m.username }}</h3>
                            <p>{{m.content}}</p>
                        </div>
                    </md-list-item>
                </md-whiteframe>

            </md-list>
        </section>

        <div class="footer">


            <md-input-container>
                <label>Message</label>
                <textarea ng-model="message" columns="1" md-maxlength="100" ng-enter="send(message)">