Friday, June 28, 2013

Writing a real time single page application - client side

Introduction

In this two parts guide I will explain how to use backbone.js, node.js and socket.io (and many others libraries) to write a single page/real time application.
The guide's goal is to explain a system and I will not dive into details of every component, for this reason I suggest you to follow the links to get more informations.
The topic is quite hot, so let's get started !

First step: defining resources

This part is the most boring but it is very important.
In my application I should model every client server exchange using REST principles  (even though I will eventually use websockets instead of HTTP protocol).

Following these principles I need to define resources.
A resource is a well defined piece of information

Every resource should:
  • have an id (the URL)
  • be atomic (every exchange will be stateless)
A very important detail here is that resources don't need to match exactly with backend models (database schemas). For example a resource can be the result of a join between tables.

I will leave to node.js backend to manage the differences between the actual db models and resources.
The backend can update more than one model in a reliable way (and, in case, rolling back changes) while it's not so reliable do this directly from the client.

From this point when I talk about models in the client I am talking about an HTTP resources.

Client side

For the front end I will use backbone.js. This library's task is basically to organize homogeneous models (they are resources!) into collections and keep these data synchronized with the UI (view) and the backend.
This is a model:

var Item = Backbone.Model.extend({
    defaults: {
       "name": "",
       "description": "",
    },
    validate: function(attrs, options) {
        // return a string is attributes are wrong
    }
});

This is a simple collection:

var Items = Backbone.Collection.extend({
    model: Item,
    initialize: function (){
        //...
    }
});

This collection contains a group of Items.
A backbone collection usually has an url attribute that is used to download data from the server. So I'll rewrite the last collection:

var Items = Backbone.Collection.extend({
    url: "/items"
    model: Item,
    initialize: function (){
        //...
    }
});

Backbone.js uses AJAX by default to download and update collections. It uses the standard HTTP methods: GET POST PUT DELETE (optionally PATCH if supported).
If the collection's url is "/items" the single model urls will be "/items/model_id".

Backbone.IO

One of the nicest feature of Backbone.js is that you can use different ways to save your data (overwriting the Backbone.sync function).
In this example I will use backbone.io. This component sends data to the backend using a websocket or another available socket like transport (it uses socket.io under the hood).

Backbone.io.connect();

var Items = Backbone.Collection.extend({
    backend: 'items',
    model: Item,
    initialize: function (){
        this.bindBackend();        
        //...
    }
});

Backbone.io.connect is used to initialize websockets. I replaced the url attribute with a backend identifier.
I have also launched "bindBackend". This method connect backbone.io events to the collection events. So when the server broadcasts that something is changed the collection triggers the event and views are refreshed.

Views

Backbone.js main feature is to be unopinionated. Views, for example, are mostly boilerplate code. For this reason, you often need to build a more high level framework on top of it.
It is usually a good idea to use something like Marionette.
Anyway I tried to build something simpler. This is a model's view:

var ModelView = Backbone.View.extend({
    initialize: function (){
        this.initialEvents();
    },
    initialEvents: function (){
        if (this.model){
          this.listenTo(this.model, "change", this.render);
        }
    },
    serialize: function (){
        var attrs = _.clone(this.model.attributes);
        attrs.id = attrs[this.model.idAttribute];
        return attrs;
    },
    render: function (){
        this.preRender();
        this.$el.html(this.template(this.serialize()));
        this.postRender();
        return this;
    },
    preRender: function (){
        // this runs before the rendering
    },
    postRender: function (){
        // this runs after the rendering
    }
});

I also build a collection's view:

var CollectionView = Backbone.View.extend({
    contentSelector: '.append-item-here',
    // this is the class where I append the model views
    viewOptions: {},
    initialize: function (){
        this.children = {}; // this will contain the models views
        this.initialEvents();
    },
    initialEvents: function (){
        if (this.collection){
          this.listenTo(this.collection, "add", this.addChildView);
          this.listenTo(this.collection, "remove", this.removeChildView);
          this.listenTo(this.collection, "reset", this.render);
          this.listenTo(this.collection, "sort", this.render);
        }
    },   
    render: function (){
        var that = this;
        this.preRender();

        // I remove the old models views
        _.each(this.children, function (view){
            view.remove();
        });

        this.children = {};

        // I rebuild the models views
        this.collection.chain()
        .each(function(item, index){
            // every model view have a reference to its collection view
            var options = _.extend({index: index, parentView: that}, that.viewOptions);
            that.addChildView(item, that.collection, options);
        });

        this.postRender();
        return this;
    },
    // I can use this to filter some view if it is necessary
    filterView: function (model){
        return true;
    },
    addChildView: function (item, collection, options){
        // add a view to this.children trying to respect the sorting order
        var index;
        if (!this.filterView(item)){
            return;
        }
        options.model = item;
        if (options && options.index){
            index = options.index;
        }
        else {
            index = collection.chain().filter(this.filterView, this).indexOf(item).value();
            if (index === -1){
                index = 0;
            }
        }

        var view = new this.itemView(options);
        view.render();

        this.children[item.cid] = view;
        this.addToView(view, index);
        
    },
    removeChildView: function (item, collection, options){
        this.children[item.cid].remove();
        delete this.children[item.cid];
    },

    addToView: function (view, index){
        // append a model view to the collection view in the correct sorting order
        var $el = this.$el.find(this.contentSelector),
            $children = $el.children();
        if (index >= $children.length ){
            $el.append(view.el);
        }
        else {
            $children.eq(index).before(view.el);
        }
    },
    preRender: function (){
        // this runs before the rendering
    },
    postRender: function (){
        // this runs after the rendering
    }

});

Extending these base views I can write a very simple item view:

var ItemView = ModelView.extend({
    template: _.template(html_item),
    tagName: 'div',
    events: {
        //...
    }
});

and, of course, a very simple collection view:

var ItemsView =  CollectionView.extend({
    itemView: ItemView,
    el: $("#items"),
    events: {
        //...
    }
});

You should notice that I am using the underscore template engine (_.template).

Routing and bootstrapping

Backbone uses a router object to keep the state of your application. The state is represented by the current URL.

var Router = Backbone.Router.extend({

    routes: {
        "item/:id":        "changeitem",  // #item/id
    },
    changeitem: function(id) {
        // this is called when the URL is changed
    }
});

Like others backbone primitives you can react to an URL change handling an event (inside a view for example):

var ItemsView =  CollectionView.extend({
    itemView: ItemView,
    el: $("#items"),
    initialize: function (){
        this.listenTo(router, 'route:changeitem', this.update);
    },
    update: function (){
        // ...
    },
    events: {
        //...
    }
});

When every component is in place you can instance it and bootstrap the application.

var router = new Router(); // router instance
 
var items = new Items(); // collection instance
var itemsView = new ItemsView({collection: items}); // collection view instance
    
Backbone.history.start(); // this initialize the router

items.fetch(); // this loads initially the collection from the server

The fetch method is not your best option if you want to load models for the first time. It's better to load models inline.


I hope everything is clear. Next part: the server side!