Progressive Web Apps

By Andy Hill

Why Progressive Web Apps?

  • Offline Functionality
  • Client-side Caching
  • Push Notifications
  • Native App Behavior

Experience

Invitation to install
In app list
App experience
App in settings

Support

Short answer: Fullest support in Chrome and Firefox, but coming to all platforms and browsers.

  • Can I Use?
  • Is Service Worker Ready?
  • iOS/Safari
  • Microsoft/Edge
  • PWAs to replace Chrome Apps
  • What Makes a Progressive Web App?

    1. SSL (trivial)
    2. Manifest File (trivial)
    3. Service Worker (not trivial)

    SSL (https)

    SSL is required for progressive web apps.
    An exception is made for localhost.

    You can create certs for SSL via Let's Encrypt

    Manifest File

    1. Project metadata
    2. Information for application display
    3. Icons of various sizes
    4. Place in manifest.json in site root
    https://developer.mozilla.org/en-US/docs/Web/Manifest
    {
      "name": "Informed Electorate",
      "short_name": "Informed Electorate",
      "start_url": ".",
      "display": "standalone",
      "background_color": "#000044",
      "theme_color": "#8B0000",
      "description": "Tools for voters to stay informed.",
      "icons": [{
        "src": "images/touch/informed_48x48.png",
        "sizes": "48x48",
        "type": "image/png"
      }, {
      ...
        "src": "images/touch/informed_512x512.png",
        "sizes": "512x512",
        "type": "image/png"
      }],
      "related_applications": [{
        "platform": "web",
        "url": "https://informedelectorate.net"
      }]
    }	
    
    name
    Provides a human-readable name for the application as it is intended to be displayed to the user
    start_url
    Specifies the URL that loads when a user launches the application from a device.
    theme_color
    Defines the default theme color for an application. This sometimes affects how the application is displayed by the OS
    background_color
    Can be used by browsers to draw the background color of a web application when the manifest is available before the style sheet has loaded.
    display
    fullscreen | standalone | minimal-ui | browser
    icons
    Specifies an array of image objects that can serve as application icons in various contexts. Standard sizes include 48x48, 72x72, 96x96, 144x144, 168x168, 192x192, 512x512. You can use Inkscape to create and SVG and export PNG icons

    Service Workers

    A service worker is a script that your browser runs in the background, separate from a web page, opening the door to features that don't need a web page or user interaction.
    https://developers.google.com/web/fundamentals/primers/service-workers/

    Service workers sit between the browser and the web, allowing new possibilities.

    (image via https://codeburst.io/work-it-featuring-service-workers-de769bd4917b)

    Register Service Worker

    	if ('serviceWorker' in navigator) {
    	    navigator.serviceWorker.register('/sw.js')
    		    .then(function (registration) {
    		    	console.log('service worker registered');
    		    }).catch(error) {
    		    	console.err('something went wrong', error);
    		    });
    	}
    						

    This code is added to your standard JavaScript entry point. ServiceWorkerContainer.register() has a required first argument: a string representing the path to the service worker code, and an optional second options argument. Currently, the only available option is "scope", which indicates what range of URLs a service worker can control. The default value for scope is './'.

    						

    Install Event

    const STATIC_CACHE = 'informed-cache-v1'; const urlsToCache = [ '/css/app.css', '/js/app.js', ... 'https://fonts.googleapis.com/css?family=UnifrakturMaguntia', ]; self.addEventListener('install', function(event) { // Perform install steps event.waitUntil( caches.open(STATIC_CACHE) .then(function(cache) { return cache.addAll(urlsToCache); }) ); });

    The install event runs once when the service worker is first loaded or modified.
    This is when you cache static assets.

    Activate Event

    this.addEventListener('activate', function(event) {
      const cacheWhitelist = [STATIC_CACHE];
      event.waitUntil(
        caches.keys().then(function(keyList) {
          return Promise.all(keyList.map(function(key) {
            if (cacheWhitelist.indexOf(key) === -1) {
              return caches.delete(key);
            }
          }));
        })
      );
    });							
    						

    The activate event runs after the install event.
    This is a common place to clear old caches.

    Fetch Event

    self.addEventListener('fetch', function(event) {
      const requestUrl = new URL(event.request.url);
      if (requestUrl.pathname.startsWith('/api/')) {
        event.respondWith(onlineFirstStrategy(event));
        return;
      }
    
      event.respondWith(cacheFirstStrategy(event));
    });							
    						

    The fetch event intercepts fetches made in your JS code.
    Here, you can decide what to do before going to the network.

    Cache First

    function cacheFirstStrategy(event) {
      return caches.match(event.request)
        .then(function(response) {
          // Cache hit - return response
          if (response) {
            return response;
          }
          var fetchRequest = event.request.clone();
          return fetch(fetchRequest)
            .then(function(response) {
              var responseToCache = response.clone();
              caches.open(STATIC_CACHE)
                .then(function(cache) {
                  cache.put(event.request, responseToCache);
                });
              return response;
          	})
            .catch(function(err) {
              toast(event, 'You appear to be offline. Please check your internet connection.');
              return;
            });
        }); 
    }							
    						

    Online First

    function onlineFirstStrategy(event) {
      return fetch(event.request)
        .then(function(response) {
          var responseToCache = response.clone();
          caches.open(DYNAMIC_CACHE).then(function(cache) {
             cache.put(event.request, responseToCache);
          })
          return response;
        })
        .catch(function() {
          return caches.match(event.request, { cacheName: DYNAMIC_CACHE })
            .then(function(match) {
              if (match) {
                toast(event, 'You appear to be offline. Returning result from local cache.');
                return match;            
              } else {
                toast(event, 'You appear to be offline. Please check your internet connection.');
                return;              
              }
            })
        })
    }							
    						

    Communicate between code and service worker

    //// in service worker
    self.addEventListener('message', function(event) {
      if (event.data.action === 'skipWaiting') {
        self.skipWaiting();
      }
    });					
    
    //// in js code
    worker.postMessage({action: 'skipWaiting'});		
    						

    Communicate between service worker and code

    //// in code
    navigator.serviceWorker.addEventListener('message', event => {
      indexController._sendMessage(event.data.msg);
    }); 		
    
    //// in service worker
    if (!event.clientId) return;
    
    // Get the client.
    clients.get(event.clientId)
      .then(function(client) {
        client.postMessage({
          msg: message,
        });       
      })
      .catch(function(err)  {
        return;
      });
    						

    Push Notifications

    A notification is a message that pops up on the user's device. Notifications can be triggered locally by an open application, or they can be "pushed" from the server to the user even when the app is not running. They allow your users to opt-in to timely updates and allow you to effectively re-engage users with customized content.

    Push Notifications are assembled using two APIs: the Notifications API and the Push API. The Notifications API lets the app display system notifications to the user. The Push API allows a service worker to handle Push Messages from a server, even while the app is not active.

    https://developers.google.com/web/ilt/pwa/introduction-to-push-notifications

    IndexedDB

    Google Lighthouse

    Resources