About Bevry

Bevry's Learning Centre

  doing everything we can to empower developers

Building a Chat Web Application

It's now time to move onto building a proper web application. Rather than the simple todo application you would build in other systems, for node the standard is a chat application (which is a lot more exciting!).

To do this, we will use Primus and ws both by Arnout Kazemier.

Primus is a wrapper library over many different WebSockets libraries. WebSockets is a technology that allows browser clients to communicate with servers. Until WebSockets became a thing in 2011, such functionality was achieved via several dodgy workarounds.

We can install primus like so:

npm install --save primus

Server to Browser Relay

The most basic example of this is the following, which will allow the server to broadcast to all clients, while receiving responses from clients:

// Requires
var express = require('express')
var primus = require('primus')
var pathUtil = require('path')

// Application
var app = require('express')()
var server = require('http').createServer(app)
var primus = new require('primus')(server, { transformer: 'websockets' })

// Middlewares
app.get('/', function (req, res) {
    require('fs').createReadStream(pathUtil.join(__dirname, 'socket-client.html')).pipe(res)
})
app.use(function (req, res) {
    res.status(404).send('404 Not Found. 🙁 \n')
})

// Socket
primus.on('connection', function (spark) {
    console.log('connection has the following headers', spark.headers);
    console.log('connection was made from', spark.address);
    console.log('connection id', spark.id);

    // Receive messages
    spark.on('data', function (message) {
        console.log('connection', spark.id, 'sends', message.toString())
    })

    // Send messages
    process.stdin.on('data', function (message) {
        spark.write('The server has spoken: ' + message.toString())
    })

    // Send an initial hello
    spark.write('Hello user. I am the server communicating to you.')
})

// Listen
server.listen(8000)
<!DOCTYPE html>
<html>
<head>
    <title>My Web App</title>
</head>
<body>
    <!-- Scripts -->
    <script src="/primus/primus.js"></script>
    <script>
        // Tell primus to create a new connect to the current domain/port/protocol
        var primus = new Primus()

        // Ask for a response if we receive data
        primus.on('data', function (message) {
            alert(message)
            var response = prompt('What would you like to say?')
            if ( response ) {
                primus.write(response)
                alert('Sent')
            }
        })
    </script>
    Hello.
</body>
</html>

Test it: http://localhost:8000/

Browser to Browser Broadcasting

A more useful example of this, is your basic chat app, which allows clients to broadcast to other clients:

// Requires
var express = require('express')
var primus = require('primus')
var pathUtil = require('path')

// Application
var app = require('express')()
var server = require('http').createServer(app)
var primus = new require('primus')(server, { transformer: 'websockets' })

// Middlewares
app.get('/', function (req, res) {
    require('fs').createReadStream(pathUtil.join(__dirname, 'chat-client.html')).pipe(res)
})
app.use(function (req, res) {
    res.status(404).send('404 Not Found. 🙁 \n')
})

// Socket
primus.on('connection', function (spark) {
    console.log('connection has the following headers', spark.headers);
    console.log('connection was made from', spark.address);
    console.log('connection id', spark.id);

    // Receive messages
    spark.on('data', function (message) {
        // Broadcast them back to everyone
        primus.write('user ' + spark.id + ' sends ' + message.toString())
    })

    // Send an initial hello
    spark.write('Hello user ' + spark.id + '. I am the server communicating to you.')
})

// Listen
server.listen(8000)
<!DOCTYPE html>
<html>
<head>
    <title>My Web App</title>
    <style>
        .messages {
            max-height: 500px
            border: 1px solid gray
            overflow: auto
        }
        .messageInput {
            width: 100%
            padding: 1em
        }
    </style>
</head>
<body>
    <!-- App -->
    <div class="app">
        <ul class="messages"></ul>
        <input type="text" disabled class="messageInput" placeholder="Enter your message here" />
    </div>

    <!-- Scripts -->
    <script src="//cdnjs.cloudflare.com/ajax/libs/jquery/3.0.0-beta1/jquery.min.js"></script>
    <script src="/primus/primus.js"></script>
    <script>
        // Tell primus to create a new connect to the current domain/port/protocol
        var primus = new Primus()

        // Ready
        $(function(){
            // Fetch
            var $app = $('.app')
            var $messages = $app.find('.messages')
            var $messageInput = $app.find('.messageInput')

            // Enable input once connection is open
            primus.on('open', function () {
                $messageInput.removeAttr('disabled').focus()
            })

            // Receive message
            primus.on('data', function (message) {
                $message = $('<li>', {
                    'class': 'message',
                    text: message
                })
                $messages.append($message)
            })

            // Send message
            $messageInput.on('keypress', function (event) {
                if ( event.keyCode === 13 ) {  // enter
                    var message = $messageInput.val().trim()
                    primus.write(message)
                    $messageInput.val('')  // clear input
                }
            })
        })
    </script>
</body>
</html>

Test it: http://localhost:8000/

Where can this go?

It is now your turn to have a go and mash up your own solution. To help you get started, here are a bunch of ideas on how you can extend the chat application:

  • Chat Rooms
    • Add support for two chat rooms
    • Add support for unlimited chat rooms
    • Allow users to change the names of the chat rooms
  • Users
    • Give each user a randomly generated name - e.g. "User #{Math.random()}"
    • Next to each message, show the user who sent it - e.g. "#{user.name} says: #{message.text}"
    • Show user connection and disconnection events as messages in the chat - e.g. "#{user.name} joined the chat"
    • Remember the user's details if they leave the page and come back - e.g. localstorage or sessions
    • Allow the user to change their name
    • Show user name change events as messages in the chat - e.g. "#{oldName} changed their name to #{newName}"
    • Give each user their own randomly selected color - e.g. style="color: hsl(50,50,50);"
    • Create a sidebar that lists all active members in the chat room
    • Allow users to specify their email
    • If a user has an email specified, display their avatar next to their username in the message listing
    • When a user changes their details, automatically update all prior mentions of their details
  • Abstractions
    • Experiment with pre-processors - docpad could help with this
  • Messages
    • Relative times
    • Markdown support
    • Webkit notifications