What we used

Web Components: Following the simple idea, that everything except the app shell is a component. We used Polymer 1.0 (https://www.polymer-project.org/1.0/) as a wrapper around this web standard. We created custom components for the slider, which contains all available board-cards, the board-card itself (displaying the destination image and flight information), the boarding pass (QR-code), journey details, destination information, as well as the check-in and confirmation forms.

Custom Events: To interact between the different components as well as the the basic application script which handles / centralizes async requests, history, app data and date handling.

WebSQL: In addition to the offline service worker we tried to improve offline experience by using localForage (http://mozilla.github.io/localForage/) as a wrapper around IndexedDB, WebSQL, and / or localStorage. The complete communication between server and client is JSON based to simplify this kind of data storage.

HistoryAPI: The PWA is a one-page application. Since service worker / cache does not work with a hash in an url, we decided to use GET parameter to identify the different states and views of the app. A decision, with which we ran into minor offline availability problems. As a learning – do not use keys only, always send a valid pair, if you want the request to be handled properly. Or you recreate your own request depending on the relevant data of the url (see the code example of the service worker further down).

Service worker: As mentioned, it’s a single page app with only one use case, that means – compared with other projects – adding on service worker offline support had been rather simple. The app-shell itself is cached inside the install routine, the outline and the data on its first request. Removing the correct data at the correct time was a bit more challenging. We also integrated push notifications to enable the two-tap check-in process.

VanillaJS: For the rest. Some basic DOM-selectors, a few async requests, there was no need for additional libraries. Except moment.js (http://momentjs.com/timezone/), since we wanted some kind of ‘date awareness’ for the app. It has to handle different timezones, e.g. to display the correct journey step or to disable the boarding pass.

Manifest and Meta data: Allows the user to add the application to the home screen. Even if there is currently no offline support on iOS, we decided to provide a proper app icon, title and color theme on android and iOS.

What we did

Lazy loading – Don’t block the DOM: Besides our major goals: A quick check-in and the possibility to complete boarding without any additional application, we wanted the app to be fast. Therefore we load everything except the critical CSS and some basic placeholders asynchron without blocking the DOM. So basically all what is sent with the first request is a skeleton of the app, with a menubar and colored placeholders for the dashboard, menu options, footer (the web components) and a short intro text in the visible area. The main CSS, the core script and the vendor scripts (polymer, moment, local forage) are loaded deferred, finally the core loads the different polymer elements. For further information, Paul Lewis wrote a great article on that topic.

Comparing the actual check-in page of m.airberlin with the new approach the initial loading time is almost the same (about ~1.5s and ~2.5s on 3G), but the starting point of rendering is significantly lower. (~0.5s vs. ~1.2s), this is still (and will always be) work in progress. However, using lazy loading and placeholder styling to prevent FOUC (Flash of unstyled content) reduces the time of starring at a blank screen.

Minimize – Blurry to hurry: Serving images in different sizes, depending on screen size and resolution should be common sense, but a lot of modern mobile devices require rather large images for a high quality result. Based on the concept used by Facebook in its native apps and an HTML/SVG/CSS adaption described in this article we use very small images (60x40px) and load the optimized variant async – until it comes from cache.

Get the Service Worker working: Our very first version of the service worker had been a few lines of code. We loaded all of our static assets when initializing the worker, and once installed it fetched and cached simply everything. For our demo this is sufficient, since we only provide one flight, one destination, there won’t be any flight history and nothing which can be out of date. In real, we needed more. We have flight data, e.g. an e-ticket which has to be disabled or removed after the flight. In addition, we have related data for each flight, e.g. some information and images for each destination.

Spamming the cache by storing all related data for all flights over all time isn’t a good idea. Therefore we decided to remove everything with a delay of 48 hours after the flight. Since we also use WebSQL / local storage, we needed to do this twice. The following code fragments are showing our current implementation draft:


function _isEmpty = function(obj) {
    if ('undefined' !== Object.keys) {
        return (0 === Object.keys(obj).length);
    }
  for(var prop in obj) {
      if(obj.hasOwnProperty(prop)) {
                return false;
            }
  }
  return true;
};

// called whenever a checkin is requested to be displayed
function checkCheckinStatus( checkinID ) {
  var tS = Math.floor(now.getTime() / 1000),
      removeCheckinFromApp = function(cid) {
    // remove from data 
    delete app.data[cid];

    // update local cache
    localforage.setItem('flightData',app.data);

    // trigger event for updating UI elements
    var event = new CustomEvent(
      'updatedData',
      {detail: {modified: cid}}
    );
    document.dispatchEvent(event);
 };

  // no data or no checkin data? - return
  if(_isEmpty(app.data) || !app.data[checkinID]) {
    return false;
  }

  // remove only in case arrival time is min. 48 hours in past
  if((tS - app.data[checkinID].ticket.arrivalTimestamp) < (60 * 60 * 48) ) { 
    return false;
  }

  if ('serviceWorker' in navigator) {
    // delete cache of flight in service worker...
    app.sendMessage( {
      command: 'deleteCheckin', 
      keyID: checkinID } 
    ).then(function(data) {
      // remove the checkin from app data...
      removeCheckinFromApp(checkinID);
    }).catch(e) {
      // could not remove checkin from service worker
    };
  } else {
    removeCheckinFromApp(checkinID);
  }
}

// send data to the service worker
app.sendMessage = function(message) {
  return new Promise(function(resolve, reject) {
    var messageChannel = new MessageChannel();
    // the onmessage handler
    messageChannel.port1.onmessage = function(event) {
      if (event.data.error) {
        reject(event.data.error);
      } else {
        resolve(event.data);
      }
    };
    
    if(!navigator.serviceWorker.controller){
      return;
    }

    // This sends the message data and port to the service worker.
    // The service worker can use the port to reply via postMessage(), which
    // will he onmessage handler on messageChannel.port1.
    navigator.serviceWorker
      .controller.postMessage(message,[messageChannel.port2]);
  });
}

Inside the app.js (triggered from different polymer elements) a method tests, if the stored data is still valid or not. In case it is outdated, the script uses service worker post messages (see: https://github.com/GoogleChrome/samples/tree/gh-pages/service-worker/post-message) to trigger the cache delete inside the worker and removes the data from local storage. Finally it triggers a custom event to tell all listeners (polymer elements) that the data has been changed.

The sample code also contains part of the fetch handler. We reduced the cache ‚url‘ identifier to the check-in number by creating an own Request. Which allows us to easily identify the data when deleting and brought up a second benefit: The requested URL’s might contain various parameters in future which change the url, but not the data itself (e.g. data for google campaigns), reducing the url to its relevant part provides a solution for this.

One more thing I would like to mention:

In our first draft we put all web-components inside the install routine of the service-worker, and we returned all this, so we blocked the service-worker until every asset had been loaded. At the very same time we do some lazy loading for these elements, the CSS and scripts. While fetching we stored ‘everything‘ in cache. Scanning the timeline of the page, and the amount of requests, it was more efficient to reduce the number of required files and let fetch do the rest.

Add to home screen – if useful: Currently you can’t trigger ‚add-to-home-screen‘ message and there is some unknown magic in it under which conditions the message is shown. In our case we also have an additional condition when this message should be displayed. Going offline is possible after you checked-in, not before. Since you can’t trigger the message, you could go for your own dialog, explaining the user how to do ‚add-to-home-screen‘ via the menu. To implement your own conditions, you’ll need to defer the event:


var deferredPromptEvent; 

window.addEventListener('beforeinstallprompt', function(e) {
    e.preventDefault();
    deferredPromptEvent = e;
    return false;
});

// and in the moment your condition is fulfilled
// check if the prompt had been triggered
if(deferredPromptEvent !== undefined && deferredPromptEvent) {
    // show message
    deferredPromptEvent.prompt();
    // do something on the user choice
    deferredPromptEvent.userChoice.then(function(choiceResult) {
        if(choiceResult.outcome != 'dismissed') {
        }
        // finally remove it
        deferredPromptEvent = null;
    });
}

In our case the message pops up when the client closes the massage window, that the ticket has been saved.

Process integration: Due to it’s atomic structure, a polymer element normally comes with inline CSS and JavaScript. Surely you are able to integrate external stylesheets and scripts in polymer elements, but the inline approach is fast. Only downside, like this it is complicated to maintain the code, especially the CSS part. Our solution – a Grunt task named ‘grunt-inline‘ and SCSS as CSS-precompiler. Each element gets its own SCSS file which includes basic settings (like variables and functions), as well as normalizing styles. The grunt task takes the generated CSS and writes the inline style and script tags to the element. For sure, the very same is possible with gulp and LESS.

What we learned (so far)

Web components: Using arrays of objects as a database for polymer elements can be quite tricky when updating the data. Especially when you work with nested components, in our case the slider which contains all board-cards. Since the rendering of all data went from server to client, we only need to transfer a bit of JSON data and some binary files, great for response time, great for the capacity utilization of the server. Measuring the loading time over different devices it gets a bit more ambivalent. Pre-rendering complex data on the server and serve the complete html is faster on older / less powerful devices (although the start of first rendering takes longer). If the client cache is working, this is only true for the very first request – if not you should do some serious testing what you do on server side and what not.

Service worker: On the first view easy to integrate and the benefits are simply great: Not only the increase of speed and rendering, the offline availability, it also removes a lot of requests from the server (which helps to speed up thinks even further). The complicated part is not the script itself, it is the classic question how to cache properly. Also, for the moment the implementation of service workers differs a lot, only a subset of android users get the full benefit of this technology. However this is no argument for ‘not implementing‘, but you still need an eye on the word progressive inside Progressive Web Apps. On iOS for example, you could add our PWA to the home screen too, due to browser caching and local storage you could open it (with some luck) even if your network is slow or down but it won’t take long and you would see the dinosaur.

Continue Reading:

https://jakearchibald.com/2014/offline-cookbook/

http://www.html5rocks.com/en/tutorials/service-worker/introduction/

https://www.theguardian.com/info/developer-blog/2015/nov/04/building-an-offline-page-for-theguardiancom