Introduction
In this article I'm going to show you how to create a completely serverless chat application that you can host on your blog or website. This can serve as a good starting point if you want to develop a chat platform of your own. Below is the chat application we are going to create.
Background
In order to transfer messages between clients, we need to rely on some external service that would allow us to dispatch events, in real-time. For this purpose we are going to use emitter.io, a scalable, affordable and simple service that also offers a nice free tier that we can use. Emitter is a cloud publish/subscribe service that uses open and standardized MQTT protocol.
Essentially, emitter.io (we're using hosted version here) will act as message bus allowing our javascript client (e.g. browser) to:
- Subscribe to a particular channel, which can be represented by a simple string.
- Publish messages to that channel.
- Store the messages published for a limited amount of time, allowing us to retrieve the data if the page is suddenly refreshed.
- Expire the messages after some time, allowing the system to 'forget' old conversations.
- Encrypt the communication through TLS/SSL so we can avoid man-in-the-middle attacks.
- Secure the channel using a particular emitter.io key, which can only be allowed to publish and/or subscribe to a particular channel. This allows us to simply embed the key for the chat channel into the webpage itself.
The Application DOM
Alright, let's jump right into the code. First, let's have a look in our index.html
page.
<div v-for="message in messages" class="row animated bounceInRight">
<div class="col-sm-12">
<canvas width="80" height="80" data-jdenticon-hash="{{message.hash}}" class="img-thumbnail img-responsive img-circle"></canvas>
<div class="panel panel-default"><div class="panel-body">{{message.text}}</div></div>
</div>
</div>
There are several interesting points in the above code:
- We set a vue.js repeater property (
v-for="message in messages"
) which allows us to repeat the inner dom for every message. Essentially, we are setting up our template for rendering our message list there. - We assign a jdenticon hash (
data-jdenticon-hash="{{message.hash}}"
) which allows the jdenticon plugin to render the identicon. Notice that the actual hash value will be data-bound by vue.js itself. That means that we would need to simply re-run jdenticon every time we receive a new message. - We also set CSS class (
class="row animated bounceInRight"
) which comes from a very nice animate.css library, allowing us to animate new messages when they appear. This is super-easy as there's pretty much nothing else to do to set this up.
Second interesting part of our HTML dom consists of a simple input section where we can submit a message:
<form v-on:submit.prevent class="col-sm-12 col-md-6 col-md-offset-3">
<div class="input-group">
<input type="text" class="form-control" v-model="message" placeholder="Say Something">
<span class="input-group-btn">
<button class="btn btn-default form-control" type="submit" v-on:click="sendMessage()">Say it! <i class="fa fa-commenting-o"></i></button>
</span>
</div>
<ul class="emoji">
<li v-for="em in emoji"><a v-on:click="append(em)" >{{em}}</a></li>
</ul>
</form>
- Once again, we use vue.js to perform data-binding (
v-model="message"
). However, this time a two-way data binding is performed. Two way data binding mechanism implements the 'always up to date' relationship between model and view, which allows our model to retrieve the value of the text box. - We also attach an 'on click' method (
v-on:click="sendMessage()"
) to the button, allowing us to execute some javascript once the user clicks or submits the form. - Finally, we print out a list of emojis, again using vue.js data binding (
v-for="em in emoji"
) and set an 'on click' function on every link (v-on:click="append(em)"
).
The JavaScript Implementation
The javascript code contained in the app.js
page does all the magic.
var emitter = emitter.connect({
secure: true
});
First, we need to connect to emitter service. This is done through a simple emitter.connect()
function call which returns a client we can use. We also specify that we want to communication over an encrypted connection (using TLS/SSL).
emitter.on('connect', function(){
console.log('emitter: connected');
emitter.subscribe({
key: key,
channel: "article1",
last: 5
});
jdenticon.update(".img-circle");
})
- Once we're connected, we are going to subscribe to the channel by calling an
emitter.subscribe()
function. We provide a key that I have previously generated on the emitter.io website. This key allows us to publish and subscribe to the "article1" channel. However, it won't allow publishing or subscribing to any other channel. This restricts its usage and allows us to distribute this key on the web. If we wanted, we could also specify an expiration time for the key (for example 1 day), after which the key would simply expire and not be valid. - Additionally, during the subscription we also specify
last: 5
parameter, which allows us to automatically retrieve 5 last messages from the channel.
emitter.on('message', function(msg){
console.log('emitter: received ' + msg.asString() );
if (vue.$data.messages.length >= 5){
vue.$data.messages.shift();
}
vue.$data.messages.push(msg.asObject());
setTimeout(function(){
jdenticon.update(".img-circle");
},5);
});
- Once a message is received, we are going to parse the message as JSON by calling
msg.asObject()
function and push the message into our vuejs array (vue.$data.messages.push(msg.asObject())
). - We remove the very first message, so we can keep our UI short and nice.
- We need to wait a little bit for data-binding to execute, hence we schedule a jdenticon update at some later stage.
var vue = new Vue({
el: '#app',
data: {
messages: [],
message: '',
emoji: [
"😀", "😬", "😁", "😂", "😃", "😄", "😅", "😆", "😇", "😉", "😊",
"🙂", "😋", "😌", "😍", "😘", "😗", "😙", "😚", "😜", "😝", "😛",
"😎", "😏", "😶", "😐", "😑", "😒", "😳", "😞", "😟", "😠", "😡",
"😔", "😕", "🙁", "☹", "😣", "😖", "😫", "😩", "😤", "😮", "😱",
"😨", "😰", "😯", "😦", "😧", "😢", "😥", "😪", "😓", "😭", "😵",
"😲", "😷"
]
},
methods: {
sendMessage: function () {
var message = this.$data.message;
this.$data.message = '';
console.log('emitter: publishing');
emitter.publish({
key: key,
channel: "article1/" + getPersistentVisitorId(),
ttl: 1200,
message: JSON.stringify({
name: 'test',
hash: getPersistentVisitorId(),
text: message,
date: new Date()
})
});
},
append: function(emoji) {
this.$data.message += ' ' + emoji + ' ';
}
}
});
The last part, shown above, is our actual view model (vue.js). This part is pretty straightforward as it declares some data to bind and a couple of methods to execute.
messages
is our message array to display. message
contains our input text from the text box. emoji
contains a list of nice emoji characters. Interestingly enough, Emojis are supported on all operating systems: iOS, Android, OS X, Windows and Windows Phone and we have used some characters that can be simply copied and pasted (see http://getemoji.com).
Most interestingly, sendMessage
method publishes a message to emitter service, on the article1 channel. We have specified that a message should be automatically expired after 1200 seconds to allow the service to 'forget' old messages. When publishing, we also pass a hash value for our visitor (some kind of unique value) that would allow the remote clients to generate an appropriate avatar.
History
- 21/01/2018 - Minor changes.
- 09/08/2016 - Clarified some things.
- 22/07/2016 - Fixed some typos.
- 20/07/2016 - Initial article.