This is the 4th post in a multi-part tutorial series on Socket.io. See Part 3 here.
In today’s section of the tutorial, we’re going to be discussing the chat client. This is a single page with all the HTML, CSS, and JavaScript rolled into one. It’s bad form for production use but, again, we’re all for simplicity in this tutorial. Since we’re on the client (i.e. browser) side of the code, everything we’re covering today will be in JavaScript with some jQuery (as opposed to the NodeJS from Part 3). Of course, we’ll also be using the SocketIO client side libraries as well.
If you haven’t downloaded the code already, please see
part 2. What follows is exactly what is in index.html with line numbers (as reported by GitHub, so that includes all the HTML/CSS too). As for explanations, we’re skipping all the HTML/CSS and going right to the code.
var version = 3;
var names = ["Lucid", "Lynx", "Maverick", "Meerkat", "Natty", "Narwhal", "Oneiric", "Ocelot", "Precise", "Pangolin", "Quantal", "Quetzal", "Raring", "Ringtail", "Saucy", "Salamander", "Trusty", "Tahr", "Utopic", "Unicorn", "Vivid", "Vervet", "Warty", "Warthog", "Hoary", "Hedgehog", "Breezy", "Badger", "Dapper", "Drake", "Edgy", "Eft"];
var nickName = "";
// Generate a random 3 word nickname
for(var a = 1; a <= 3; a++){
var num = Math.floor(Math.random() * (names.length - 1));
nickName = nickName.concat(names[num]);
}
Once again we're setting up some basic variables to use. The version
variable syncs up with the variable of the same name on the server side of the code. The names
array are random names we'll pick for a starting username (and yes, they're Ubuntu names).
var server = io('http://localhost:3000/server');
var chat = io('http://localhost:3000/chat');
var color = io('http://localhost:3000/color');
// We're connected, so lets send our nick
server.emit('joinnick', nickName);
console.log(nickName);
This is where we connect to our server that we got running in part 3. Obviously localhost and port can be changed to whatever you'd prefer. However, you'll notice here how we connect to those unique namespaces, but can stay consistent on both client and server. Lastly we "emit" our joining of the server with our randomly selected nickname.
$('form').submit(function(){
//check for /nick
var regIt1 = /^\/nick ([a-zA-Z0-9]{0,20})/g;
var ray1 = regIt1.exec($('#m').val());
//check for colors
var regIt2 = /^\#[0-9a-fA-F]{6}/g;
var ray2 = regIt2.exec($('#m').val());
Line 50 is jQuery catching when you hit enter or click the "Send" button. Since we're doing this IRC style, we can not only chat, but send special commands which is what these two RegEx blocks check for. The first looks for "/nick YOURUSERNAME" and the second looks for "#000000", being a change of nickname and color commands, respectively.
This is one of the checkpoints where you could easily make this code much more IRC like. It wouldn't be that hard to make RegEx look for "/COMMAND DATA", where COMMAND could be a list of valid commands and DATA would be passed to each respective command. If you wanted to keep the client side code simple, you could send the entire command to the server (rather than pre-parsing in the client) and then allow the server to parse it and handle the valid/invalid response. Just a thought.
if(ray1){
// We want to change our nick, send request to server.
console.log('Requesting Nick: ', ray1[1]);
server.emit('nickchange', [nickName, ray1[1]]);
}else if(ray2){
// We want to change colors, send that on its own namespace
console.log('Coloring: ', ray2.toString());
color.emit('color', ray2[0]);
}else{
// Send a regular chat message
var chatObj = {'nick' : nickName, 'message' : $('#m').val()};
chat.emit('chat', chatObj);
console.log('Sending: ', JSON.stringify(chatObj));
}
$('#m').val('');
return false;
});
Line 60 follows the action if we got a RegEx hit for "/nick YOURUSERNAME", sending the newly typed username and existing username to the server for verification. You'll notice that the client takes no other action since it waits to hear from the server (which gets covered at line 110). The second if on line 64 checks for a color match, again taking no action and waiting for the server to verify. Lastly we assume that if there are no special commands, then it's a chat message. The client packages up the username and message into an object then sends it to the server. Oh, and don't forget to empty the chat box when you're done (line 75).
chat.on('chat', function(msg){
console.log('Recieving: ', JSON.stringify(msg));
var tex = msg.nick + '(' + msg.time +'): ' + msg.message;
//Add a new message li in the chatArea
$('#messages').append($('').text(tex));
//Scroll the window to the bottom
$('body').scrollTop($('#messages')[0].scrollHeight);
});
Alright, finally we talk about what happens when the server sends data to the client. The above section is the first of several asynchronous callbacks that are waiting for data of their specific namespace and event. In this particular case we're waiting for the namespace "chat" (of the chat.on
) with the event of "chat" (from the ('chat',
, this is admittedly a little confusing/redundant. When a chat message is received, it extracts the sending username, timestamp (as added by the server), and message. Those are appended to the page HTML (using jQuery) and then the chat window is scrolled to the bottom.
server.on('server', function(msg){
console.log('Server says: ', msg);
var tex = 'Server: ' + msg;
$('#messages').append($('').text(tex));
});
If the server sends us a generic server message, we put it into the chat in a specially formatted way. This is used predominately for join/leaves and nickchange announcements.
//Server sent us a version
server.on('version', function(msg){
console.log('Server Version: ', msg);
console.log('Client Version: ', version);
//Compare versions, if server version is newer, reload HTML
//Warning: Update client (HTML) version first or you get stuck
// in an infinte reload loop
if(msg > version){
location.reload();
}
});
As has been mentioned in a few places in this portion of the tutorial, and part 3, we have a simple versioning service in effect. That check is carried out in this code block which is one of the first messages a client should receive (since app.js should send it immediately after a client connects).
server.on('nickchange', function(msg){
console.log('Nick change: ', msg.toString);
if(nickName == msg[0]){
nickName = msg[1];
}
});
As noted from the line 60 area, while the client may send a request for nickchange, it doesn't actually make the change until it receives the server's approval here. While probably not strictly necessary, there is a small sanity check to make sure the server is telling the right client to change their nickname (i.e. checking the before and after).
color.on('color', function(msg){
console.log(msg);
document.body.style.backgroundColor = msg;
})
Since we love to irritate our user base in this demo, we put in the capability to change the background color. When the color code message is received from the server, we change the document background color, simple as that.
There you have it, in 93 lines of JavaScript, the client code is completed. This client might seem a bit "stupid"; in many ways it has more logic than is strictly necessary. Mostly the client should just have a few sanity checks and be left to handle the UI. As a webpage, it's too easy for the user to manipulate the code or use the JavaScript console to tinker with values. In fact, everything of value in this app is logged (for debugging) to the console. The heavy lifting logic should all be left to the server, which is an environment that you (the author) control and can secure.