There are a lot of new fancy web applications out there that are using real-time functionality. You can see examples of this used to great effect in Google Docs' multi-user editing, and the way all the social media sites update you of whats happening in real time.
All of these applications are a far-cry from the old "request-response" pages of the early 90s, in fact the only way to achieve realtime web communication back then was to use Java Applets, and I believe you could use Flash for it too.
I couldn't really say how easy this was to use. I was 8 years old in 1996. All I cared about back then was Sonic the Hedgehog and Nickolodeon.
Luckily I'm not 8 years old in 2015, and today we have Web Sockets that allow us to maintian persistent connections between client and server!
We also have Ember, Express and SocketIO to help us make a real-time app of our own! Why don't we do that now?
What are we building this time? A Chat Application?
No! We're building a helpdesk ticket system. Which works more or less the same way, but its semantically different so... yay!
...I'm still working on my chat application.
Setting up the Server
First we need to create the project folder, add a server folder to that, and then create the package.json file for our server module.
> mkdir helpdesk-ticketer
> cd helpdesk-ticketer
> mkdir server
> cd server
> touch package.json
// Package.json
{
name: 'helpdesk-ticketer-server',
description: 'The server code for helpdesk ticketing system'
}
Then we just need to add our dependencies using NPM
> npm install --save express socket.io
Next we'll add an app.js
file to the server directory, and create a basic express application.
var express = require(express);
var socketio = require('socket.io');
var app = express();
var io = socketio.listen(app.listen(3000));
io.sockets.on('connection', function (socket) {
console.log('A user has connected');
});
Here we are telling both express, and socket.io to listen to requests made at port 3000.
Were also logging the text "A user has connected" out to the console whenever a user connects to socket.io. This will help us see that everything is working correctly when we set up our ember application next. We'll be coming back to this file near the end of this post to set up the rest of our socket.io endpoints.
Setting up the Client
We're going to use ember-cli to set up the client code, so in the root directory of our project, run ember new client
and ember-cli will go ahead and create all of the necessary boilerplate code for us.
> cd ../
> ember new client
Once this is done, I want to remove all of the un-necessary folders from the client directory. In this application we are going to be using the Pod Structure, as this is, in my opinion, a better way to lay out your files.
> cd client/app
> rm -rf templates controllers routes views
We also need to add the following line to the .ember-cli
file.
"usePods": true
Hopefully this structure is made default in the future!
Next we'll add the socket io client as a dependency using bower
> bower install --save socker.io-client
and update our brocfile.js
so it is imported into our vendor file on build.
app.import('bower_components/socket.io-client/socket.io.js');
Adding the Route, Template, and the Controller
In the router.js
file. Lets add the only route we need. The helpdesk route.
Router.map(function() {
this.route('helpdesk', {path: '/'});
});
Next we'll create a directory inside of client/app
called helpdesk, this is our helpdesk pod that holds the controller and template, named respectively.
Lets set those up now.
First, just grab the Styles from GitHub and put them in your app/styles/app.css
file. I don't think I need to explain how to write CSS here ;)
In the helpdesk folder you created earlier create a template.hbs, and paste the following code:
<header class="page-header">
<h1>Helpdesk Jobs</h1>
</header>
{{#each tickets as |ticket|}}
<section class="job">
<header>
<h2>{{ticket.name}}</h2>
</header>
<article>
<p>{{ticket.description}}</p>
</article>
<footer>
<p>
<strong>Created On</strong> {{ticket.createdAt}}
</p>
</footer>
</section>
{{/each}}
<form class="new-job" {{action "submitTicket" on="submit"}}>
<h2>Submit a Job</h2>
{{input placeholder="Your name" value=name}}
{{textarea placeholder="What's wrong?" value=description}}
<button type="submit">Submit Job</button>
</form>
Next create a controller.js
file in the helpdesk directory and paste the following into it:
import Ember from 'ember';
export default Ember.Controller.extend({
tickets: [],
actions: {
submitTicket() {
let ticket = {
name: this.get('name'),
description: this.get('description'),
createdAt: new Date()
};
this.get('tickets').addObject(ticket);
this.set('name', '');
this.set('description', '');
}
}
});
I'm going to assume already that you understand the code above. If you don't, well, let me know and i'll explain it!
If you run the server with ember-cli at this point you'll notice that we have a perfectly functioning ember application. You can read and submit job tickets. But this isnt really what we want is it? If you were to open this up in two browsers consecutively you'll see that both instances of the applicaiton arent updated when new tickets are submitted. Only the browser from where the ticket was submitted is updated.
That's because we haven't yet used Socket.io for anything. Let's fix that!
Adding Socket.io
We'll create an Ember Service to handle the connection to socket.io as well as emitting events. This way we can have a singleton object that holds the web socket we are connected to that persists for the life of the application, or until the user explicitly disconnects.
Create a file called helpdesk.js
in the app/services/
directory (you'll probably need to create the services folder yourself).
Then add the following code:
export default Ember.Service.extend({
socket: {},
setup: function () {
this.set('socket', io())
}.on('init')
});
The setup function, which is called when the service is initialized, calls the global io() function given to us by the socket.io-client library we installed earlier. By default this function looks at the same location where the files are being hosted for the server. In ember-cli's case this will be http://localhost:4200. Which isn't what we want, we will change this later.
The io() function also returns a connected socket that we can use to emit and listen for events, we set this to the socket property for re-use.
Next we'll create a method on this service that handles submitted a ticket to the server via a socket.io event. Add the following code snippet
submitTicket(ticket) {
this.get('socket').emit('ticket-submitted', ticket);
}
This function uses the socket.io connection, grabbed through this.get('socket')
, to emit a socket.io event to the server called ticket-submitted
. Socket.io events can be called whatever you like.
Next we need to listen for this event on the server, and handle it.
Back in our app/server.js
file replace all of the socket.io code we wrote earlier with the following:
io.sockets.on('connection', function (socket) {
console.log('A user has connected');
socket.on('ticket-submitted', function (ticket) {
console.log('A ticket has been submitted');
socket.broadcast.emit('ticket-submitted', ticket);
});
});
You've seen the connection logic before, but we have also added a listener in the form of socket.on()
here we pass in the name of the event that we specified back on the client. Any data that is passed into the event from the client is also available here in the callback event.
Inside this callback it may be a good idea to manipulate the data however you need, maybe persist it to a database, and so on.
Importantly, we are calling socket.broadcast.emit()
, giving the event a name and passing the ticket back into it. We need to handle this on our client.
socket.broadcast.emit()
sends the named event out to all listeners, except the sender. If you wanted to send the event out to all sockets including the listener you would call io.sockets.emit()
instead, with the same arguments.
At this point you can go ahead and open up a command line at our server directory and run node app.js to start up the server.
The last bit of code we need to write will listen for this event from the server, so back on our client, in the helpdesk controller, add the following code:
setup: function () {
var socket = this.get('helpdesk.socket');
socket.on('ticket-submitted', (ticket) => {
this.get('tickets').pushObject(ticket);
});
}.on('init'),
When the controller is initialized we're setting up a listener with socket.on
(were grabbing socket from our instance of the service) which is listening for the ticket-submitted
event from the server, the callback will be passed the ticket which we are simply adding to the tickets array, thus updating our view in real time!
Run our app with ember server
from the command line (use a new terminal, to keep the server running), and navigate to http://localhost:4200 and ... see how were getting lots of 404 errors.
Telling ember-cli about our server
Right now all of the socket.io calls are going to http://localhost:4200/socket.io/. The problem is, our server is running at http://localhost:3000. Not a problem though, we can tell ember-cli to proxy all requests wherever we want.
You do this with the following command:
> ember server --proxy http://localhost:3000
Go back to http://localhost:4200 in a browser now and no more errors!
Hooray!
Submit a ticket using the form and it'll be added to the list of tickets right away. Do this with two separate browser windows open and you should see both browsers update in real time. You could even go all out and try it from different computers on the same network.
While this is a pretty simple example, hopefully you've got some good ideas floating around in your head now about how this technology can be used to greatly improve the responsiveness of your own projects.
I've made the source code for this app available on Github. Do with it what you will.