This is responsible to handle connections between a RESTful server and the Backbone application is the Backbone.sync module. It transforms the fetch()
and save()
operations into HTTP requests:
fetch()
is mapped as a read
operation. This will make GET
to the the urlRoot
attribute with the model ID for a model or the url
attribute for a collection.save()
is mapped as a create
or update
operation; it depends on the isNew()
method:create
if the model does not have an ID (isNew()
method return true
). A POST request is executed.update
if the model already has an ID (isNew()
method returns false
). A PUT request is executed.destroy()
is mapped as a delete
operation. This will make DELETE to the the urlRoot
attribute with the model ID for a model or the url
attribute for a collection.To better understand how Backbone.sync does its job, consider the following examples:
// read operation will issue a GET /contacts/1 varjohn= new Contact({id: 1}); john.fetch(); // update operation will issue a PUT /contacts/1 john.set('name', 'Johnson'); john.save(); // delete operation will issue a DELETE /contacts/1 john.destroy(); varjane = new Contact({name: 'Jane'}); // create operation will issue a POST /contacts jane.save();
As you can read in the Backbone documentation, Backbone.sync
has the following signature:
sync(method, model, [options])
Here, the method is the operation that is to be issued (read
, create
, update
, or delete
). You can easily overwrite this function in order to redirect the requests to localStorage instead of a RESTful server:
Backbone.sync = function(method, model, options) {
var response;
var store = model.dataStore ||
(model.collection&&model.collection.dataStore);
var defer = Backbone.$.Deferred();
if (store) {
// Use localstorage in the model to execute the query
switch(method) {
case 'read':
response = model.id ?store.find(model) : store.findAll();
break;
case 'create':
response = store.create(model);
break;
case 'update':
response = store.update(model);
break;
case 'delete':
response = store.destroy(model);
break;
}
}
// Respond as promise and as options callbacks
if (response) {
defer.resolve(response);
if (options &&options.success) {
options.success(response);
}
} else {
defer.reject('Not found');
if (options &&options.error) {
options.error(response);
}
}
return defer.promise();
};
While the localStorage API is synchronous, it does not need to use callbacks or promises; however, in order to be compatible with the default implementation, we need to create a Deferred
object and return a promise
.
If you don't know what a promise or Deferred
objects are, please refer to the jQuery documentation for more information about it. The explanation of how promises work is out of the scope of this book.
The previous Backbone.sync
implementation is looking for a dataStore
attribute in the models/collections. The attribute should be included in these objects in order to be stored correctly. As you may guess, it should be an instance of our DataStore driver:
// apps/contacts/models/contact.js class Contact extends Backbone.Model { constructor(options) { super(options); this.validation = { name: { required: true, minLength: 3 } }; this.dataStore = new DataStore('contacts'); } // ... } // apps/contacts/collections/contactCollection.js class ContactCollection extends Backbone.Collection { constructor(options) { super(options); this.dataStore = new DataStore('contacts'); } // ... }
The implementation that we made earlier for localStorage is inspired from the Backbone.localStorage plugin. If you want to store all your models in the browser, please use the plugin that has the support of the community.
Due the limitations of localStorage, it is not suitable to store avatar images on it as we will reach the limits with only a few records.
The Datastore driver is useful to develop small applications that do not need to fetch and store the data in a remote server. It can be enough to prototype small web applications or store configuration data in the browser.
However, another use for the driver can be cache server response in order to speed up the application performance:
// cachedSync.js var _ = require('underscore'); var Backbone = require('backbone'); function getStore(model) { return model.dataStore; } module.exports = _.wrap(Backbone.sync, (sync, method, model, options) => { var store = getStore(model); // Try to read from cache store if (method === 'read') { let cachedModel = getCachedModel(model); if (cachedModel) { let defer = Backbone.$.Deferred(); defer.resolve(cachedModel); if (options &&options.success) { options.success(cachedModel); } return defer.promise(); } } return sync(method, model, options).then((data) => { // When getting a collection data is an array, if is a // model is a single object. Ensure that data is always // an array if (!_.isArray(data)) { data = [data]; } data.forEach(item => { let model = new Backbone.Model(item); cacheResponse(method, store, model); }); }); });
When the application needs to read the data, it tries to read the data from localStorage first. If no model is found, it will use the original Backbone.sync function to fetch the data from the server.
When the server responds, it will store the response in localStorage for future use. To cache a server response, it should store the server response or drop the model from the cache when the model is deleted:
// cachedSync function cacheResponse(method, store, model) { if (method !== 'delete') { updateCache(store, model); } else { dropCache(store, model); } }
Dropping the model from the cache is quite simple:
function dropCache(store, model) { // Ignore if cache is not supported for the model if (store) { store.destroy(model); } }
To store and retrieve the data in the cache is more complex; you should have a cache expiration policy. For this project, we will expire the cached responses after 15 minutes, which means that we will remove the cached data and then make a fetch
:
// cachedSync.js // ... const SECONDS = 1000; const MINUTES = 60 * SECONDS; const TTL = 15 * MINUTES; function cacheExpire(data) { if (data &&data.fetchedAt) { let now = new Date(); let fetchedAt = new Date(data.fetchedAt); let difference = now.getTime() - fetchedAt.getTime(); return difference > TTL; } return false; } function getCachedModel(model) { var store = getStore(model); // If model does not support localStorage cache or is a // collection if (!store&& !model.id) { return null; } var data = store.find(model); if (cacheExpire(data)) { dropCache(store, model); data = null; } return data; }
The fetchedAt
attribute is used to show the time we fetched the data from the server. When the cache expires, it removes the model from the cache and returns null
to force a server fetch
.
When a model is cached, it should set the fetchedAt
attribute for the first time when it is fetched:
// cachedSync.js function updateCache(store, model) { // Ignore if cache is not supported for the model if (store) { varcachedModel = store.find(model); // Use fetchedAt attribute mdoel is already cached if (cachedModel&&cachedModel.fetchedAt) { model.set('fetchedAt', cachedModel.fetchedAt); } else { model.set('fetchedAt', new Date()); } store.update(model); } }
Finally, we need to replace the original Backbone.sync function:
// app.js varcachedSync = require('./cachedSync'); // ... Backbone.sync = cachedSync;
3.129.194.106