-
Notifications
You must be signed in to change notification settings - Fork 571
Going Realtime with Socket.IO
The goal of this page is to explain how to add realtime functionality to your Drywall project. We'll be using the npm/socket.io
and npm/passport.socketio
modules. We are not Socket.IO experts by any stretch. There is surely room for improvement. We really want to demonstrate how to simply connect the dots in a Drywall app. If you have suggestions or feedback please let us know.
Socket.IO is a Node.JS project that makes WebSockets and realtime possible in all browsers. It also enhances WebSockets by providing built-in multiplexing, horizontal scalability, automatic JSON encoding/decoding, and more.
https://npmjs.org/package/socket.io
Access Passport.js user information from socket.io connection.
https://npmjs.org/package/passport.socketio
$ npm install socket.io --save
$ npm install passport.socketio --save
Right after we create our express app and web server, we connect Socket.IO.
...
//create express app
var app = express();
//setup the web server
app.server = http.createServer(app);
//setup socket.io
app.io = require('socket.io').listen(app.server);
//setup session store
app.sessionStore = new mongoStore({ url: config.mongodb.uri });
...
Make sure that your session store is then set app.sessionStore
:
...
app.use(session({
resave: true,
saveUninitialized: true,
secret: config.cryptoKey,
store: app.sessionStore // set session store here
}));
...
Now where we configure passport, we need to extend the arguments, so that the express
, cookieParser
, and config
are in scope for /passport.js
. Then after we define our normal express routes, we'll define our socket routes (more on this in Step #4).
...
//config passport
require('./passport')(app, passport, express, cookieParser, config);
//route requests
require('./routes')(app, passport);
//route sockets
require('./sockets')(app);
...
Remember in Step #2 how we added the express
argument so it was in scope here? Well don't forget to define that argument like so:
'use strict';
exports = module.exports = function(app, passport, express, cookieParser, config) {
...
Now before our passport.serializeUser
method we setup the passport.socketio
module, linking the express.cookieParser
, app.get('crypto-key')
and app.sessionStore
. This makes it possible to access our Passport sessions during our socket handling logic.
...
var socketPassport = require('passport.socketio');
app.io.set('authorization', socketPassport.authorize({
cookieParser: cookieParser,
key: 'connect.sid',
secret: config.cryptoKey,
store: app.sessionStore,
fail: function(data, message, error, accept) {
accept(null, false);
},
success: function(data, accept) {
accept(null, true);
}
}));
passport.serializeUser(function(user, done) {
...
From here, we're going to create a super simple chat room feature on the /about/
page of Drywall.
'use strict';
exports = module.exports = function(app) {
app.io.sockets.on('connection', function(socket) {
socket.on('/about/#join', require('./events/about/index').join(app, socket));
socket.on('/about/#send', require('./events/about/index').send(app, socket));
});
};
It's important to realize that sockets are event based, not path based like URLs. It is easy though, to think about compartments of functionality in terms of file paths. So don't confuse our naming convention for actual routes. You could have named the event about-chat-join
instead of /about/#join
.
Create the file /events/about/index.js
with the following code:
'use strict';
exports.join = function(app, socket){
return function() {
socket.visitor = 'guest';
if (socket.request.user) {
socket.visitor = socket.request.user.username;
}
socket.join('/about/');
socket.broadcast.to('/about/').emit('/about/#newVisitor', socket.visitor);
};
};
exports.send = function(app, socket){
return function(message) {
socket.broadcast.to('/about/').emit('/about/#incoming', socket.visitor, message);
};
};
Thanks to the npm/passport.socketio
module we setup eariler, we can use socket.request.user
to get access to our Passport session.
It's worth noting that we're putting this code in the /events/
directory and not the /views/
directory. These are not views, they don't render any templates and they shouldn't cloud that part of our app logic.
/* global app:true, io:false */
var socket;
(function() {
'use strict';
var addChatMessage = function(data) {
$('<div/>', { text: data }).appendTo('#chatBox');
$("#chatBox").animate({ scrollTop: $('#chatBox')[0].scrollHeight}, 500);
};
socket = io.connect();
socket.on('connect', function() {
socket.emit('/about/#join');
addChatMessage('you joined the chat room');
});
socket.on('/about/#newVisitor', function(visitor) {
addChatMessage(visitor +': joined');
});
socket.on('/about/#incoming', function(visitor, message) {
addChatMessage(visitor +': '+ message);
});
app = app || {};
app.ChatForm = Backbone.View.extend({
el: '#chatForm',
template: _.template( $('#tmpl-chatForm').html() ),
events: {
'submit form': 'preventSubmit',
'click .btn-chat': 'chat'
},
initialize: function() {
this.render();
},
render: function() {
this.$el.html(this.template());
},
preventSubmit: function(event) {
event.preventDefault();
},
chat: function() {
var newMessage = this.$el.find('[name="message"]').val();
if (newMessage) {
addChatMessage('me : '+ newMessage);
socket.emit('/about/#send', newMessage);
this.$el.find('[name="message"]').val('');
}
}
});
$(document).ready(function() {
app.chatForm = new app.ChatForm();
});
}());
We're simply including /socket.io/socket.io.js
and our new /views/about/index.min.js
script to the feet
block and adding some markup for the chat form.
extends ../../layouts/default
block head
title About Us
block neck
link(rel='stylesheet', href='/views/about/index.min.css?#{cacheBreaker}')
block feet
script(src='/socket.io/socket.io.js')
script(src='/views/about/index.min.js?#{cacheBreaker}')
block body
div.row
div.col-sm-6
div.page-header
h1 About Us
div.media
a.pull-left(href='#')
img.media-object(src='data:image/png;base64,iVBORw0KGgoAAAANSU...', style='width: 64px; height: 64px;')
div.media-body
h4.media-heading Leo Damon
p Cras sit amet nibh libero, in gravida nulla. Nulla vel metus scelerisque ante sollicitudin commodo. Cras purus odio, vestibulum in vulputate at, tempus viverra turpis.
div.media.text-right
a.pull-right(href='#')
img.media-object(src='data:image/png;base64,iVBORw0KGgoAAAANSU...', style='width: 64px; height: 64px;')
div.media-body
h4.media-heading Mathew DiCaprio
p Cras sit amet nibh libero, in gravida nulla. Nulla vel metus scelerisque ante sollicitudin commodo. Cras purus odio, vestibulum in vulputate at, tempus viverra turpis.
div.media
a.pull-left(href='#')
img.media-object(src='data:image/png;base64,iVBORw0KGgoAAAANSU...', style='width: 64px; height: 64px;')
div.media-body
h4.media-heading Nick Jackson
p Cras sit amet nibh libero, in gravida nulla. Nulla vel metus scelerisque ante sollicitudin commodo. Cras purus odio, vestibulum in vulputate at, tempus viverra turpis.
div.col-sm-6.special
div.page-header
h1 Prestige Worldwide
div#chatBox.well
div#chatForm
script(type='text/template', id='tmpl-chatForm')
form
div.form-group
div.input-group
input.form-control(type='text', name='message', placeholder='enter a message')
span.input-group-btn
button.btn.btn-primary.btn-chat Send
Add styles for the #chatBox
element.
...
#chatBox {
height: 250px;
text-align: left;
overflow: scroll;
}
If all went well your app should start without error and you'll see the chat components on the /about/
page of your Drywall app. Immediately when you visit the page the #chatBox
should have the text you joined the chat room
. Now open another browser (or an incognito window) and you can have a little chat with yourself.
In one browser, go login to the system and come back to the /about/
page. When you send chat messages as the logged in user, other users on the /about/
page will see your username instead of guest
.
I hope this was helpful. If you have questions or think this page should be expanded please contribute by opening an issue or updating this page.