"Offline first" approach to creating web applications

Original author: Joe Lambert
  • Transfer
  • Tutorial
This year at the Full Frontal conference , offline applications were a popular topic. Paul Kinlan made an excellent talk “Building Web Applications of the Future. Tomorrow, today and yesterday ”( here are his slides), in which he compared the user experience of working with 50 popular mobile applications for iOS and Android with the experience of websites and applications.

Needless to say, native applications have proven themselves from a much better side when the Internet connection was unavailable. Offline mode is a very important thing, and you should think about it from the very beginning of working on the application, and not expect to add it later when there is time. Working on the Rareloop Website, from day one we remembered offline mode. FormAgent mobile clients were also originally designed to work offline, so that the user can continue to work in the absence of the Internet and transparently synchronize when the connection appears. In this article, I describe the principles and practices that, in my opinion, are very helpful in developing such applications.

Note! I do not consider issues of caching application resources - you can use App Cache or a hybrid solution (like PhoneGap ), it doesn’t matter [ From the translator: there is a detailed article on the Habré about the features of working with the Application Cache API]. This guide focuses more on how to design a web application architecture to work offline, rather than on what mechanisms to use to implement it.

Basic principles


Untie the application from the server as much as possible

Historically, most of the work on a web page has been taken over by the server. Data was stored in the database, accessed through a thick layer of code in the server language like PHP or Ruby, the data was processed and rendered in HTML using templates. Most modern frameworks use the MVC architecture to separate these tasks, but all the hard work is still done on the server. Storage, processing and display of information require constant communication with the server.

The offline first approach involves moving the entire MVC stack to the client side. On the server side, there is only a lightweight JSON API for accessing the database. This makes server code much smaller, simpler, and easier to test.

James Pierce also talked about this on Full Frontal ( slides), in a somewhat humorous way:
No angle brackets on the line - just curly!

Summary:

  1. Make sure that the client application can do without a server by providing minimal functionality. In extreme cases, at least a message stating that data is not available.
  2. Use JSON.


Create a wrapper object for the server API on the client side

Do not pollute your application code with AJAX calls with nested callbacks. Create an object that will represent the functionality of the server inside the application. This contributes to the separation of code and facilitates testing and debugging; it allows the use of convenient stubs in place of server functions not yet implemented. Inside this object can use AJAX, but from the point of view of the rest of the application, it should not be seen how it communicates with the server.

Summary:

  1. Abstract the JSON API in a separate object.
  2. Do not litter application code with AJAX calls.

Untie the data update from the data warehouse

You should not be tempted to simply request data directly from an object that abstracts the server API and immediately use it to render templates. Better create a data object that will serve as a proxy between the API object and the rest of the application. This data object will be responsible for requests for data updates and handle situations when the connection breaks - to synchronize data that has been changed while working offline.

A data object can interrogate the server for updates when the user clicks the update button, either by timer or by browser event " online" - as you like, and the lack of direct access to the server makes it easier to manage data caching.

The data object must also be responsible for serializing and storing its state in persistent storage, in Local Storage or WebSQL / IndexedDB, and be able to recover this data.

Summary:

  1. Use a separate data object to store and synchronize state.
  2. All work with data must go through this proxy object.

Example


As a simple example, take the contact management application. First, we’ll create a server API that will allow us to get raw contact data. Suppose we created a RESTful API where the URI /contactsreturns a list of all contact entries. Each entry has fields id, firstName, lastNameand email.

Next, write a wrapper over this API:

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
    });
};

Now we need a data object that will become the interface between our application and the data warehouse. It might look like this:

var Data = function() {
    this.api = new API();
    this.contacts = this.readFromStorage();
    this.indexData();
};
Data.prototype.indexData = function() {
    // Выполняем индексирование (например, по email)
};
/* -- API апдейтов-- */
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.prototype.readFromStorage = function() {
    var c = JSON.parse(window.localStorage.getItem('appData'));
    // позаботимся о результате по умолчанию
    return c || [];
};
Data.prototype.writeToStorage = function() {
    window.localStorage.setItem('appData', JSON.stringify(this.contacts));
};
/* -- Стандартные геттеры/сеттеры -- */
Data.prototype.getContacts = function() {
    return this.contacts;
};
// Запрос данных, специфичный для приложения
Data.prototype.getContactWithEmail = function(email) {
    // Поиск контактов с помощью механизмов индексирования...
    return contact;
};

Now we have a data object through which we can request an update from the server API, but by default it will return data stored locally. In the rest of the application, you can use this code:

var App = function() {
    this.data = new Data();
    this.template = '...';
    this.render();
    this.setupListeners();
};
App.prototype.render = function() {
    // Используем this.template и this.data.getContacts() для рендеринга HTML
    return html;
}
App.prototype.setupListeners = function() {
    var _this = this;
    // Обновляем данные с сервера
    $('button.refresh').on('click', function(event) {
        _this.refresh();
    });
};
App.prototype.refresh = function () {
    _this.showLoadingSpinner();
    _this.data.updateFromServer(function() {
        // Данные пришли с сервера
        _this.render();
        _this.hideLoadingSpinner();
    });
};
App.prototype.showLoadingSpinner = function() {
    // показываем крутилку
};
App.prototype.hideLoadingSpinner = function() {
    // прячем крутилку
};

This is a very simple example, in a real application, you probably want to implement a data object as a singleton, but to illustrate how you need to structure the code for working offline this is enough.


Also popular now: