title | tags | author | lead | layout |
---|---|---|---|---|
Weekly Widget 4 - Chat |
open-source can.Model Socket.io |
fara82 |
Sending and receiving messages using Socket.io and can.Model. |
post |
This week's widget is a real-time chat application that demonstrates sending and receiving messages using Socket.io and can.Model. This article shows how to integrate Socket.io to can.Model's events system.
The same demo page is in each window below. Messages created in one page are pushed to and displayed in the other window.
<iframe style="width: 100%; height: 300px" src="http://jsfiddle.net/HnYVb/13/embedded/" allowfullscreen="allowfullscreen" frameborder="0"></iframe> <iframe style="width: 100%; height: 300px" src="http://jsfiddle.net/HnYVb/13/embedded/" allowfullscreen="allowfullscreen" frameborder="0"></iframe>This widget works by integrating socket.io into can.Model's event system and using can.Model's event system to listen for messages being created and display them on the page. To understand how it works, I'll cover:
- can.Model's event system
- Connecting can.Model's event system to socket.io
- Using can.Model to create the chat client
By default, can.Model connects to a RESTful interface. When a can.Model is created like:
Message = can.Model({
findAll, updated,
},{})
You can create a new message on the server like:
message = new Message({body: "Hi, anyone there?"}).save()
When connecting to a standard RESTful interface, .save()
posts the message to the server, the server creates the message and returns the id to can.Model. Upon receiving the response, can.Model triggers a "created" event on the instance
and the Model constructor function. This allows you to listen to when a message is created like:
message.bind("created", function( ev, message ){
// do something when the message is created
})
Or when any message is created like:
Message.bind("created", function( ev, createdMessage ) {
// do something with the created message
})
can.Model also produces "updated" and "destroyed" events on instances and the constructor function:
Message.bind("updated", function( ev, updatedMessage ) {
}).bind("destroyed", function(ev, destroyedMessage) {
})
A real-time web application uses server-side events (SSEs) to send updates to the client. For example, when a new Message is created by anyone, the client is notified immediately. But, it's beneficial to maintain can.Model's event interface for flexibility and loose coupling. Using can.Model's interface, a chat widget could insert all new messages into the page like:
Message.bind("created", function( ev, createdMessage ) {
$("#messages").append("<p>"+createdMessage.body+"</p>")
})
In the next section, I'll show how to connect can.Model to socket.io so anytime ANY message is created on the server, a Message
model "created" event is published.
First, we will look at the server's code, then the client.
Server
Using a node [Express] server and socket.io, I listen for POST /messages
request with the following code:
var messages = [],
id=0;
app.post('/messages', function (req, res) {
var message = {
id: ++id,
body: req.body.body
}
messages.push(message);
if(messages.length === 100) {
messages.shift();
}
io.sockets.emit('message-created', message);
res.send(message);
});
The preceding code:
- Creates a
message
object with an id. - Adds the
message
to an array of messages (which will be used later for retrieving old messages) - Uses socket.io to publish a "message-created" event with the
message
object.
The following provides a service to retrieve existing messages:
app.get('/messages', function (req, res) {
res.send(messages);
});
Client
Ignoring the server-side events for the moment, I'd create a Message
model like:
Message = can.Model({
findAll: "GET /messages",
create: "POST /messages"
},{})
The following uses Socket.io to listen to 'message-created' server side events and publish a Message
model "created" event:
var socket = io.connect(myServerUrl)
socket.on('message-created', function(message){
new Message(message).created()
})
.created()
is a method on can.Model
that internally publishes a "created" event on a model instance and model constructor function.
But wait! There's a problem. When the user creates a message like:
new Message({body: "Hi world"}).save()
Two Message
model "created" events will fire:
- from the response
- from the server side event
To prevent this we .....
Message.model({id: attrs.id}).updated(attrs)
I have created a messages array to store all chat messages with a threshold of 100. I have used Socket.io to set up services that accept GET AND POST requests. As shown below, the GET request is set up to return what is stored in the messages array.
app.get('/messages', function (req, res) {
res.send(messages);
});
The POST service is used to create new messages. After a message is created on the server, Socket.io will fire a 'message-created' event that the client will listen for. The client will respond to this event by creating a message
model instance and firing a created
event. This will append a message to the DOM (more on this later).
app.post('/messages', function (req, res) {
var message = {
id: ++id,
body: req.body.body
}
messages.push(message);
if(messages.length === 100)
messages.shift();
}
io.sockets.emit('message-created', message);
res.send(message);
});
A user can create a message by entering a message in the form and submitting that form.
$("#create-message").bind("submit",function(ev){
ev.preventDefault()
new Message({body: $("#body").val()}).save()
})
In the submit event, a new Message model instance is created and saved. When this message is saved, the Message model will then post that message to the server. Usually when save
is called, the model will make an ajax request. When it is done it will fire a 'created' event and return a deferred object. In our Model's create function, we return a deferred object which will never resolve, thereby preventing the 'created' event on the model. We want to do this so that we are not showing duplicate messages to the user that created the message. This approach only displays messages that are created and posted to the server first.
Here is what this looks like:
create : function(attrs) {
$.post(myServerUrl + '/messages', attrs)
return $.Deferred()
}
After the message is saved to the 'messages' array, Socket.io will then publish a 'message-created' event that the client will respond to. I will go more into depth about the way the client will respond to this Socket.io event in the next section.
The client will listen to the 'message-created' event and create yet another Message model instance. It is important to note that this time it will trigger the 'created' event so that the message can be attached to the DOM. The client will listen to this event and create a message using this syntax:
new Message(message).created()
Here I am artificially triggering the 'created' event, because I want to prevent the save
function from being called (which would make an unnecessary ajax request). So, when this instance is created and the 'created' event is triggered, I will append this new message to the DOM as seen here.
Message.bind("created", function(ev, message){
var messagesCont = $("#messages");
messagesCont.append( can.view('messageEJS', [message]) )
})
When the app loads, I make a request to get all messages that are stored on the server. can.Model is set up to easily make a request to find or create messages.
var Message = can.Model({
findAll : 'GET ' + myServerUrl + '/messages',
// create a message, but prevent the 'created' event from firing
create : function(attrs) {
$.post(myServerUrl + '/messages', attrs)
return $.Deferred()
}
},{});
When a message is created, as I mentioned in the first section, can.Model will make a request to create a message on the server. When the message is created, Socket.io will emit a 'message-created' event that the client will respond to. The important thing to note is that I am using the model to handle my message data. By creating a message instance twice, I am able to use the 'save' function to create the message on the server, while I can use the other message instance's 'created' event after to actually append it to the DOM.
- Single entry point
- No matter how we create messages, (either after when you create a message or someone else creates a message), we can always respond to the same way. This supports the idea that the client side will be more maintainable using this technique. If you have a single entry point, you can create a message in various other ways and still have the same result.
- Using can.Model to connect to Socket.io
- As described above, we can easily use and setup can.Model to post messages to the server using Socket.io.
So, now you know how to create a real-time chat application using Socket.io and can.Model. For next week's weekly widget, I am going to demonstrate a different technique for sending and receiving messages in another real-time chat app. Stay tuned.