English |  Español |  Français |  Italiano |  Português |  Русский |  Shqip

Developing a Backbone.js Edge

Chapter 7: Sync

One of the most important parts of a web application is its ability to persist data. Without this persistence, every page reload would start the application over from scratch and, well, that's not very interesting. Backbone handles persistence agnostically through its sync method. This gives developers the flexibility to use AJAX, localStorage, web sockets, and/or any other data transfer protocol by simply overriding sync. This idea of encapsulating the data transfer method while not altering any of Backbone's core Model or Collection methods makes setting up client-to-server communication a breeze. Here is a figure representing this:

It's a good idea get a solid understanding of the sync algorithm before trying to override it. Failure to do so will leave your models and collections hanging high and dry in the data department. The best way to get a grasp of how sync works is to browse through the annotated source code, but we'll go over the major points you'll need to remember when writing your own sync method. After the basics, we can dive into Hubbub's sync method that uses both AJAX and localStorage for persistence.

Overview

The first thing to note about sync is its signature: (method, model, options). These three arguments will give you all you need to create a request for the server. The first argument, method, will always be a CRUD string ('create', 'read', 'update', or 'delete'). 'patch' was added in Backbone 0.9.9, but browser and server support for 'patch' is still a bit lacking, so we'll focus on the main four. The second argument, model, can be a bit misleading, but it will either be an instance of a Model (as the variable name implies) or an instance of a Collection. model is the instance we're going to be creating, reading, updating, or removing. The final argument is options. As with most Backbone methods, an options object is provided to pass along helpful information for tailoring your application. Common options for sync are success and error, which are callbacks that need to be invoked accordingly, if they exist.

The next important piece of sync is data serialization. This is the process of taking a model (or collection) with all of its attributes and methods and turning it into something the server can understand. The default method of serialization in Backbone is with JSON (JavaScript Object Notation). Calling a model's toJSON method will extract this server-ready JSON. For simple models, the default toJSON will work just fine. For more complex attribute structures like those with nested models and collections, toJSON will most likely need to be overridden to format the data to the server's liking.

Once the serialized data is attained, it's time to act on the CRUD method. This part of sync will be where most of the customization is necessary as it is where the actual server request will be sent. This example will use AJAX because it is currently the most common.

AJAX requests are composed of a few key properties, namely the method, url, data, and callbacks. Fortunately, the CRUD methods map very well to HTTP's request methods:

  • CREATE → POST
  • READ → GET
  • UPDATE → PUT
  • DELETE → DELETE

The url for the request is also simple with Backbone. It can be attained by using the Underscore result method and applying it to the passed in model. result takes an object and a property string and returns the value of the property or, if the property is a function, the value of the function. For our case, this would look like:

var url = _.result(model, 'url');

With the method and url set, the AJAX request is already half-way complete.

The next thing we need is the request data, which just so happens to be the same data we serialized earlier with toJSON. Piece of cake.

The final step is to bind the options.success and options.error callbacks to the request and send it off. Backbone sets the user's AJAX library of choice to Backbone.ajax, so all you have to do is invoke Backbone.ajax(options).

The default sync implementation also triggers a few events on the passed in model:

  • 'request' - when the request is sent
  • 'sync' - when the request succeeds
  • 'error' - when the request fails

These events can be helpful for telling views when to show or hide loading spinners, for example.

Hubbub's sync

Now that the basics of sync are covered, we can look at how Hubbub overrides sync to work for GitHub's API through AJAX as well as for the browser's localStorage. You'll want to take a look at the sync.js file in the root of the Hubbub repository as you read this section.

The first decision we had to make was how to distinguish AJAX requests from localStorage. Initially, it was tempting to store a flag (something like 'localStorage: true') at the model level, but that quickly became a convenience that wouldn't work. The reason is that we wanted to be able to use one model to fetch from both AJAX and localStorage endpoints, not just one persistence method per model.

The goal was to be able to fetch a repository or issue from GitHub, then save it locally, and then fetch it locally. It was especially important that we be able to cache results locally due to GitHub's relatively low API request limit of sixty per hour. The solution was to use sync's handy options argument. We decided to pass remote flag and use that to branch our sync logic into two paths: a standard AJAX request with the default sync method when remote is true, and a customized localStorage request when it's not.

The following sections give a line-by-line explanation of our custom sync method.

AJAX

Obviously rewriting the default sync method for use in our AJAX case would be a pain, so we store the default sync in a local variable in our closure.

var sync = Backbone.sync;

By doing this, we can reassign Backbone.sync and still reuse the default sync Backbone provides for AJAX. Inside our sync function, this is the conditional we use to make the AJAX or localStorage decision.

if (method === 'read' && options.remote) {
options.data = _.extend({per_page: 100}, options.data);
return sync.apply(this, arguments);
}

You can see that in the case of a 'read' method when the options.remote flag is truthy, we will set our default per_page option for GitHub and proxy the arguments to the original sync we stored earlier. You already know that the AJAX portion of our sync method works because it's the same as the original Backbone.sync, so now we'll focus on at the localStorage side of it.

localStorage

localStorage is essentially one big key-value store, saved in the client's browser, that persists across browsing sessions. It can't directly store complex JavaScript objects, but it can store strings. Fortunately our awesome serialization tactics (model.toJSON() and JSON.stringify(data)) have already given us the ability to turn complex JavaScript objects into simple strings. We'll look at this in more detail later in the function.

It's important to note that some older browsers do not support localStorage, but there are polyfills out there that can solve that problem. We didn't bother polluting Hubbub with extra code for the sake of a terser sample project (sorry old browsers). Overall localStorage is a great medium for storing data that doesn't need to persist on a server somewhere. It can hold more data than cookies and also doesn't come with the added overhead on every HTTP request.

Setup

window.localStorage is kind of a long variable name to type, so we store this behemoth as ls. res will hold the result we send to the options.success callback.

var ls = window.localStorage
var res;

In an effort to reuse as many resources as we could for both the GitHub and localStorage API, we chose to use each model's urlRoot and a key. This provides a unique location for data to be stored for each type of model (user, repo, issue, etc.).

var url = _.result(model, 'urlRoot') || _.result(model, 'url'); 

The first thing to do before any CRUD operations can take place is to retrieve what already exists in localStorage. Here we get the string from localStorage and parse it into a real JavaScript object.

var models = ls.getItem(url);
models = models ? JSON.parse(models) : {};

The next step is to act on the passed in method. The easiest way to branch logic based on a single value is through a switch statement, as you see here.

switch (method) {

CREATE and UPDATE

The first case we handle is 'create' and 'update'. Since these two methods will take almost identical actions, we group them together.

case 'create':
case 'update':

First we set the response to the result of the model's serialization method, toJSON. In general, our response will be what the server sends back, but in this case there is no server and we already know what the response should be.

res = model.toJSON(options); 

This next bit only applies to the create case. In Backbone, the 'create' method is used when a model returns true for its isNew method. Unless you override it, isNew will simply check the existance of the model's id. The logic being if it has an id, it must exist and therefore we're 'update'-ing it, otherwise we must 'create' it.

Normally an id is assigned to a model through that model's corresponding record in the database. Since we don't have a remote database, it's our job to assign the unique id manually. To do this, we simply start by assuming an available id (stored in our available variable) of 1. We then iterate through the existing models and increase our available ID if it has already been taken.

Note the unary + operator placed before the id in the assignment. This operator is handy for converting non-numbers to numbers. Since the keys of our localStorage data are stored as strings, we want to convert them to numbers before assigning them as an id. Comparing numbers as strings would yield undesirable results, for example '2' > '11' is true, but 2 > 11 is false. Finally, assign the res ID to the available ID.

 
  if (method === 'create') {
var available = 1;
for (var id in models) if (available <= id) available = +id + 1;
res.id = available;
}

The last thing we need to do before saving is update the models object with our model's data.

  models[res.id] = res; 

Now we override the old models with the modifed models using localStorage.setItem. Remember to JSON.stringify the data that's being saved, otherwise it is will be stored by the data's toString method. Try saving an empty object into localStorage and you'll see something like '[object Object]' is really what is stored.

  ls.setItem(url, JSON.stringify(models));
break;

READ

'read' is the next case we handle, and it's probably the simplest. All we have to do in this case is make sure we return the right response. When the model passed to sync is actually a Model, we return the model with the requested model's id, or if it doesn't exists, an empty object. If the model isn't a Model, it must be a Collection, so return an array of all the models object values using Underscore's values method.

case 'read':
res =
model instanceof Backbone.Model ?
models[model.id] || {} :
_.values(models);
break;

DELETE

The final CRUD method to handle is 'delete'. The response data for 'delete' isn't used because the model's destroy method is just looking for a success or error callback, so we set it to an empty object. Then delete the model from the models object using its id as the key, and finally save it to localStorage with localStorage.setItem.

case 'delete':
res = {};
delete models[model.id];
ls.setItem(url, JSON.stringify(models));
}

Callbacks

The very last step in our sync function is to invoke the options.success or options.error callback. In our case, there is no possibility for an error like connection interruption or unauthorized use, so we'll always invoke options.success.

options.success(res);

And that's it! This is just one of the many ways sync can be leveraged to store data in multiple locations with multiple transports.

Conclusion

To recap, sync is the data bridge between the browser and persistent storage. The beauty lies in the fact that you can choose whatever persistent storage method you want and then easily tailor Backbone to work seemlessly with it. You should now have a good idea on how you could write your own AJAX, localStorage, or hybrid sync method. If you're looking for a challenge, try writing a sync method that works with web sockets. In the next chapter we will look at URL state and how Backbone.router manages that for us.

There has been error in communication with Booktype server. Not sure right now where is the problem.

You should refresh this page.