November 26, 2012 29

Offline First – A better HTML5 User Experience

By

This year at Full Frontal, offline enabled web site/apps were a recurring theme. Paul Kinlan gave an excellent talk entitled ‘Building Web Apps of the future. Tomorrow, today and yesterday’ (his slides are available here), in which he compared the ‘offline’ user experience provided by the top 50 or so iOS and Android apps with that of popular web sites/apps.

Needless to say, the native apps fared better and more gracefully degraded when no internet connection was available. Offline is a feature and it’s crucial we start to consider it from the start of a project as apposed to adding support later in the development lifecycle. At Rareloop we’ve been focussing on adding offline support to our apps from the start. Most notably the FormAgent mobile clients where designed from the outset to support offline access, to allow for use when no connectivity was possible and to transparently sync up data when internet is available. The following are guidelines I’ve found helpful.

Note, I’m not going to discuss how to cache your app assets (e.g. JavaScript & CSS). Whether you’re using App Cache or a hybrid solution (like PhoneGap) it’s not really important, the guidelines below are more focussed on how to engineer your app to enable sensible offline access and assume your core assets are already readily available via some mechanism.

Basic guidelines

1. Decouple your app from the server

Historically when creating content for the web the server does the majority of the heavy lifting. Data is stored in a database and is accessed via server side code (e.g. PHP or Ruby), the data is then manipulated and rendered to HTML with a series of templates. Many modern frameworks use good Software Engineering principles and separate these tasks in an MVC architecture (Model, View, Controller) but they are still server side. This means that the storing, manipulation & rendering of content all requires a connection to the server.

An offline first approach would be to move the entire MVC stack into client side code (aka our app) and to turn our server side component into a data only JSON API. This keeps the server component largely logic less, lightweight and makes it really easy to write unit tests for.

James Pearce also touched on this at Full Frontal (slides), with a slightly tongue in cheek statement:

no angle brackets on the wire – only curly brackets

Summary:

  1. Make sure client side app is not reliant on server side code to produce the minimum viable experience. It should be able to at least render something to say that no data is available.

  2. Communicate using JSON

2. Create an API wrapper object in client side code

Don’t litter your app code with Ajax calls and nested callback functions. Create an API object that mirrors the functionality of the API end point you’ve created. This creates a better separation of code within your app which is easier to debug, enables you to write unit tests and crucially allows for mock data to be used if the server side API has yet to be written. The object you create will internally use AJAX but the idea is that from the point of view of the rest of your app, its irrelevant what the implementation details are in order to provide the data requested.

Summary:

  1. Abstract JSON API into an object

  2. Don’t litter your app code with direct AJAX calls and callbacks

3. Decouple data updates from data storage

It might be tempting to now just request data directly from the API object straight into our view objects for rendering with client side templates. I’d suggest creating a Data object to act as a proxy between the API object and the rest of your app. This new Data object is responsible for making requests to the API for updates and processing the responses. This includes indexing any results received for quick retrieval within the app, synchronising any data stored when no connection was available or just gracefully returning sensible data structures when no data is available.

It’s up to you to decide what triggers the Data object to request an update from the server (maybe when the user presses a refresh button or the browser fires an online event) but by not calling the server directly you’re able to locally cache the data more easily.

The Data object must also be able to serialise itself to persistent storage, be that LocalStorage or WebSQL/IndexDB, it should also know how to re-read this serialised form back into memory.

Summary:

  1. Use a data controller object to store and schedule updates

  2. Make all requests for data via this proxy object

An example

As way of a trivial example, lets consider a contact card viewing application. The first step would be to create a server based API that lets us retrieve the raw contact data. Lets assume we’ve created a RESTful API with a URI /contacts that returns the complete list of contact records. Each record has an id, firstName, lastName & email field.

Next, we write the API object to wrap our actual end point which might look something like:

var API = function() { };

API.prototype.getContacts = function(success, failure) {
    var win = function(data) {
        if(success)
            success(data);
    };

    var fail = function() {
        if(failure)
            failure()
    };

    $.ajax('http://myserver.com/contacts', {
        success: win,
        failure: fail
    });
};

Then we need to write our Data object, which is the interface between our app and the data store. This might look a little like this:

var Data = function() {
    this.api = new API();
    this.contacts = this.readFromStorage();
    this.indexData();
};

Data.prototype.indexData = function() {
    // Do indexing task (e.g. store contact via email)
};

/* -- API Updating -- */

Data.prototype.updateFromServer = function(callback) {
    var _this = this;

    var win = function(data) {
        _this.contacts = data;
        _this.indexData();

        if(callback)
            callback();
    };

    var fail = function() {
        if(callback)
            callback();
    };

    this.api.getContacts(win, fail);
};

/* -- Data serialisation -- */

Data.prototype.readFromStorage = function() {
    var c = JSON.parse(window.localStorage.getItem('appData'));

    // Ensure a sensible default
    return c || [];
};

Data.prototype.writeToStorage = function() {
    window.localStorage.setItem('appData', JSON.stringify(this.contacts));
};

/* -- Standard getters/setters -- */

Data.prototype.getContacts = function() {
    return this.contacts;
};

// App specific data request
Data.prototype.getContactWithEmail = function(email) {
    // Retrieve the contact from our index datastructure
    return contact;
};

We now have a Data object that can be told to update from the API but that by default serves up data that it has stored locally. We then might have the following code elsewhere in our app:

var App = function() {
    this.data = new Data();
    this.template = '...';

    this.render();
    this.setupListeners();
};

App.prototype.render = function() {
    // Use this.template && this.data.getContacts() to render HTML
    return html;
}

App.prototype.setupListeners = function() {
    var _this = this;

    // Reload the data from the server
    $('button.refresh').on('click', function(event) {
        _this.refresh();
    });
};

App.prototype.refresh = function () {
    _this.showLoadingSpinner();

    _this.data.updateFromServer(function() {
        // We've now got new data to show
        _this.render();
        _this.hideLoadingSpinner();
    });
};

App.prototype.showLoadingSpinner = function() {
    // show spinner
};

App.prototype.hideLoadingSpinner = function() {
    // hide spinner
};

The above is a very simple example and in a real app you’d probably want to make sure the Data object was s singleton but it does illustrate how to structure your code to enable offline access out of the box.


Check our the ‘Offline First’ approach in action in FormAgent