Building Decoupled JavaScript Applications with Postal.js
Working with asynchronous code is one of the best features of JavaScript. With Ajax, it's very simple to request some JSON API, add a callback and update the UI with the result of that request very easily. Especially when using jQuery.
However, writing an application that is testable, decoupled, and manageable, is a whole other beast. The typical approach to making Ajax requests in an application is to use jQuery's Ajax, pass a success callback and update the DOM with the response. That's great if you have some small application that doesn't need a lot of updates and manageability.
What happens in an enterprise environment when someone writes that kind of shortcut Ajax code, makes a mess of the DOM in the success callback? You've just coupled your DOM directly to your request. What if the new boss comes in and says, "I HATE TEH JQUERY!"? Well, at that point you have to find the code in the success callback that messed with the DOM, rip it out, rewrite your Ajax logic, etc... It's a mess.
A great way to solve this problem is through messaging. A great open-source JavaScript messaging library is postal. Take a look at how 3 objects having separate responsibilities can use postal and not know that each other exist, thereby decoupling the application, and making it testable as well!
The weather is from http://openweathermap.org
var Weather = function() {
this.channel = postal.channel( "weather" );
this.channel.subscribe( "fetch", this.fetch ).withContext( this );
};
_.extend(Weather.prototype, {
fetch: function( city ) {
$.ajax({
url: "http://openweathermap.org/data/2.1/find/name?q=" + city + "&units=imperial",
dataType: "jsonp",
success: _.bind(function( data ) {
this.channel.publish( "fetched", data.list[ 0 ] );
}, this )
});
}
});
var weather = new Weather();
The Weather class listens on a channel called "weather". When a "fetch" topic comes across, it preforms an ajax call to retrieve the weather. When the request comes back, the data is published back onto the weather channel with a topic of "fetched".
Here's the app object.
var App = function() {
this.channel = postal.channel( "weather" );
this.channel.subscribe( "fetched", this.gotWeather ).withContext( this );
};
_.extend(App.prototype, {
getWeather: function( city ) {
this.channel.publish( "fetch", city );
},
gotWeather: function( data ) {
console.log( "weather retrieved" );
}
});
var app = new App();
UI.init();
app.getWeather( "Thompsons station, tn" );
The app channel is listening on the "weather" channel for the "fetched" topic. When the "fetched" topic comes across, the gotWeather method is fired.
The UI code looks like...
var UI = {
init: function() {
postal.channel( "weather" ).subscribe( "fetched", UI.showWeather );
},
showWeather: function( data ) {
var weather = data.weather[0],
displayWeather = "";
displayWeather += "The weather in " + data.name + " is " + weather.description + ".";
displayWeather += "The wind is " + data.wind.speed + "mpg, with gusts of " + data.wind.gust + ".";
$( "#weather" ).html( displayWeather );
}
};
The UI sets up a subscription in the init method to also listen on the "weather" channel for the "fetched" topic.
By having postal in the app, the UI and the app are able to both receive information from the Weather object, yet none of the three objects know that each other exist! This means if ever there was a new portion of the application that needed the same weather information, another subscription could be created and have no effect on any other parts of the application.
The three different pieces could easily be unit tested completely separate from one another.
Here's the fiddle of the working sample.