Tuesday, 18 July, 2017 UTC


Summary

I love mobile apps too, and it's one of those who was strong-headed about accepting the new technology that simplified building mobile apps with web technologies. This is ironical because I have a strong web engineering background, but for some sentimental reasons, I just thought things should stay in the boxes made for them.
Pressure from the community and ecosystem had led me into giving in and thinking out of the box. After much back and forth, there just might be something out there to not only make mobile experience design approachable but also create a happy story for our users.
In fact, there is something out there that you need to meet -- Progressive Web App. After trying out this concept and the technology behind it, I'm not sure I will ever have a need to build a native mobile for most of my web projects. If you think you need such solution, then tag along while I get you started in this article.
What we'll build:
Progressive Web Apps
Think of Progressive Web Apps (PWA) as your typical web app but engineered to deliver a better offline experience to users. It answers the question: How can we get rid of the following offline error?
It's not just about getting rid of the offline error, but engaging the user with consumable contents while their connection is restored.
Progressive web apps are major concerns on mobile devices. Let's tell a user a story and see why.

On Desktop Browsers

  • The user reaches for her laptop (at home, school or office).
  • Checks if connected to internet / Connects to internet
  • Opens up your web app

On Mobile

  • User pulls phone off her bag
  • (Doesn't connect to / check for the internet. Hopefully 3G or even worse 2G is available on her way to work or her trip to rural areas)
  • Opens up your app
As you can see, their stories are different. The user on mobile might not have enough internet power. She might not even have an internet connection at all. The goal is to keep her happy, engage her while her internet is restored. If it's a poor connection like 2G, you can keep her engaged while you make the long trip of getting some fresh information for her from your server.
This illustration will make more sense when we start building a demo.
Service Workers
The driving force behind PWAs is service workers. If you need to keep a user engaged at offline scenarios, then you need "someone" to run a background routine when your web app is opened for the first time. That "someone" needs to gather contents/assets from your web app when the user is online for later usage (when offline).
It's like the farming during farming season and storing food to last till the next cycle. You can even relate it with the popular saying "make hay when the sun shines."
That "someone" in the background, that farmer in the farming season, or the person making hay when the sun shines is the Service Worker in PWA's case. This is what MDN thinks about service workers:
A service worker is an event-driven worker registered against an origin and a path. It takes the form of a JavaScript file that can control the web page/site it is associated with, intercepting and modifying navigation and resource requests, and caching resources in a very granular fashion to give you complete control over how your app behaves in certain situations (the most obvious one being when the network is not available.)
Long story short, a service worker is some logic you create to be run in the background. It has no access to the DOM but can interact with other resources like IndexDB and the Fetch API.
Things to keep in mind before we dive in:
  • Service workers must be served over HTTPS (or Localhost).
  • They are designed to be Async so you can't use XHR (but you can use Fetch) or localStorage
  • Their scope is relative to the containing path. Therefore a worker at demo/sw.js is scoped to demo. Another at demo/first/sw.js is scoped to first.
Mobile or PWA
If you can use Service Workers to gather contents for the use of offline, then you might never have a need for a mobile app. As long as your app is responsive and optimized for mobile users, have little or no need to interact with mobile hardware, then you should be fine with a PWA.
I took some time to go activate "Airplane Mode" on my phone and see how some apps behave. In the end, you can categorize them into three types:
  • Offline: Does nothing at all when offline
Example: Coinbase
Coinbase is just stuck at the loading spinner. It even got me wondering why such apps exist because there is no difference compared to what is on the web. Coinbase is not a financial app shows sensitive information in realtime, therefore, PWA might just be good for only serving its App Shell.
An App Shell is that part of an app that does not contain dynamic content. Navigation menu, sidebars, background, app logo, etc. are app shell contents.
  • Offline: Shows warns you, shows App Shell but remains useless
Example: Uber
Uber shows some content hinting the user with what she could achieve (by showing an App Shell and a map) if she were connected and also letting her know she can't achieve those things because she is offline. Uber is a handy app, so this strategy makes a lot of sense for their scenario.
  • Offline: Shows cached data (can also warn when disconnected):
Example: Medium
Medium shows you cached content when offline. Some apps in this category (e.g., Instagram) even warn you when you're disconnected, so you don't expect so much from the app.

Deduction

My thoughts are, if PWA (and service workers) become matured enough and well accepted, why not save our users the stress of 1, going to the app store and 2, downloading apps they might not always use. When we discuss Web Manifest in the 3rd part of this tutorial, you will learn that you can add a home screen icon for you web app with which the web app can be launched.
Some businesses are already doing great with PWA, and you can learn about them at pwa.rocks:
Preparing Demo
We have done a lot of talking already. This is a hands-on tutorial, so it's time to get our hands dirty now that some of the concepts have been explained. First, create a fresh project with the following structure:
|--pwa-demo
|----css
|----fonts
|----images
|----js
|----index.html
|----service-worker.js
Download Materialize material design UI library and copy the CSS, Fonts, and JS assets into the pwa-demo replacing the existing folders.
Open index.html and setup a basic template to include the added assets:
<!-- ./index.html -->
<!DOCTYPE html>
  <html>
    <head>
      <!--Import Google Icon Font-->
      <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
      <!--Import materialize.css-->
      <link type="text/css" rel="stylesheet" href="css/materialize.min.css"  media="screen,projection"/>
      <link type="text/css" rel="stylesheet" href="css/app.css">

      <!--Let browser know website is optimized for mobile-->
      <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
    </head>

    <body>

      Body coming soon

      <!-- Scripts -->
      <script type="text/javascript" src="js/jquery-2.1.1.min.js"></script>
      <script type="text/javascript" src="js/materialize.min.js"></script>
      <script type="text/javascript" src="js/app.js"></script>
    </body>
  </html>
We have imported all the downloaded assets as well as an app.js custom JavaScript file which we are yet to create.
Registering Service Worker
For service workers to work, you need to tell the browser of course. This is best done as early as possible. If possible at your Project's entry point. We can register a new worker in app.js:
(function(){
  if ('serviceWorker' in navigator) {
    navigator.serviceWorker
     .register('/service-worker.js')
     .then(function() { 
        console.log('Service Worker Registered'); 
      });
  }    
})()
First, we check if the browser supports service worker before attempting anything else related to this feature. If it does, then we can go ahead to register it with the register method which takes the path to the service worker file. Registration returns a promise where you can find out if it was successful or not.
Service Worker Lifecycle
Service workers have a lifecycle which you need to understand at the very beginning of your journey:

Install

This stage is where a worker is installed in the browser for a given scope. Take advantage of this stage to cache all your assets as early as possible because this is the first stage of the lifecycle:
// ./service-worker.js

var cacheName = 'PWADemo-v1';
var filesToCache = [
  '/index.html',
  '/css/app.css',
  '/js/app.js',
  /* ...and other assets (jQuery, Materialize, fonts, etc) */
];

self.addEventListener('install', function(e) {
  console.log('[ServiceWorker] Install');
  e.waitUntil(
    caches.open(cacheName).then(function(cache) {
      console.log('[ServiceWorker] Caching app shell');
      return cache.addAll(filesToCache);
    })
  );
});
  • caches.open and cache.addAll are async operations and the service worker could terminate before these operations are completed. e.waitUntil asks the service worker to wait while the promise is resolved/rejected.
  • When the cache is opened, we try to add the assets to the cache with addAll
  • Keep in mind that if any of the files fail to be cached (may because it's probably missing), the service worker will fail to install.

Activate

When changes are made, and service worker is updated, they are not immediately reflected until all sessions with the previous service worker are closed and the app re-visited. Assuming we register another service worker with a different cache name by updating the cache version string:
// ./service-worker.js

var cacheName = 'PWADemo-v2';
var filesToCache = [
  //...
];

self.addEventListener('install', function(e) {
  console.log('[ServiceWorker] Install');
  //...
});
When the new service worker is created, a new cache PWADemo-v2 will be created, but PWADemo-v1 will still be there. When activate is triggered, PWADemo-v2 takes over making this stage a good place to delete PWADemo-v1:
// ./service-worker.js

self.addEventListener('activate', function(e) {
  console.log('[ServiceWorker] Activate');
  e.waitUntil(
    caches.keys().then(function(keyList) {
      return Promise.all(keyList.map(function(key) {
        if (key !== cacheName) {
          console.log('[ServiceWorker] Removing old cache', key);
          return caches.delete(key);
        }
      }));
    })
  );
});
We check for all the cache names that don't match the current cache in use and delete them.

Fetch

Fetch is not necessary a lifecycle hook but an event to intercept requests for assets. When a request is encountered, it first goes through this event:
// ./service-worker.js

self.addEventListener('fetch', function(e) {
  console.log('[ServiceWorker] Fetch', e.request.url);
  e.respondWith(
    caches.match(e.request).then(function(response) {
      return response || fetch(e.request);
    })
  );
});
If the asset is already cached, then we respond the browser with the cached version. If that's not the case, we use the fetch API to make an actual HTTP request for the asset.
Debugging Service Workers
Service workers are not so easy to debug because of how they work, especially when caching. Fortunately, Chrome Dev Tool provides a helpful service worker debugging feature. Take the following steps to inspect our just registered service worker:
  • Open Chrome Dev Tool
  • Navigate to the Applications tab and open service worker section:
  • You can see the status flag in green showing that your service worker is fine:
  • You can check "Update on reload" to forcefully update the service worker without having to close all your existing sessions:
  • Right click on "Cache Storage" and click refresh to see your list of caches. Open the one with your cache name, and you will see the list of items in the cache:
Next Up
The PWA concept is no longer new to you. You have already learned what you need to know to get started. In the next post, we are going to discuss caching and caching strategies you could employ when building PWAs. We will also see how to use IndexDB to persist data and not localStorage. The reason for this choice will also be highlighted.