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!).
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 for our project like so:
npminit# setup our projectnpminstall--saveexpress# install express, which we will attach Primus tonpminstall--saveprimus# install Primusnpminstall--savews# the WebSockets library we will sue with 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:
'use strict'// Requiresconstexpress=require('express')constPrimus=require('primus')constpathUtil=require('path')// Applicationconstapp=require('express')()constserver=require('http').createServer(app)constprimus=newPrimus(server, { transformer:'websockets' })// Middlewaresapp.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')})// Socketprimus.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 messagesspark.on('data',function (message) {console.log('connection',spark.id,'sends',message.toString()) })// Send messagesprocess.stdin.on('data',function (message) {spark.write('The server has spoken: '+message.toString()) })// Send an initial hellospark.write('Hello user. I am the server communicating to you.')})// Listenserver.listen(8080)
<!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>
A more useful example of this, is your basic chat app, which allows clients to broadcast to other clients:
'use strict'// Requiresconstexpress=require('express')constPrimus=require('primus')constpathUtil=require('path')// Applicationconstapp=require('express')()constserver=require('http').createServer(app)constprimus=newPrimus(server, { transformer:'websockets' })// Middlewaresapp.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')})// Socketprimus.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 messagesspark.on('data',function (message) {// Broadcast them back to everyoneprimus.write('user '+spark.id +' sends '+message.toString()) })// Send an initial hellospark.write('Hello user '+spark.id +'. I am the server communicating to you.')})// Listenserver.listen(8080)
<!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>
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