parm530
1/14/2020 - 2:28 PM

PWA (Progressive Web App)

What is PWA?

  • informative blog
  • A web app that looks and feels like a native mobile app (run in the browser)
  • Performs app like functionalities (notifications online and offline)
  • Eliminates issues like slow networks, data limitations and works offline

Attributes

  • Responsive (mobile design)
  • Connectivity independent (uses service workers to work even when offline)
  • App-like interactions
  • Updates content through the use of service workers
  • Safe (secured by SSL)
  • Installable (installs to the home screen of device)
  • Linkable (no hassle to install and can be shared easily)

How does it work?

  • Service worker is a javascript code that runs in the background of the pwa
    • Primarily used to cache resources (html pages, images)
    • Used to submit push notifications and background data syncs
  • Web manifest is a json file that defines the look and feel of the pwa when it's installed
    • Customizes the home screen icons, how the app is launched
    • Includes meta data (app name, version, description, theme colors, screen orientation)
  • TSL is required! PWA's are required to communicate over HTTPS by having an SSL certificate installed on the web server

Benefits

  • Faster (with pre-caching, allows to load faster on poorly connected devices)
  • Reduces data needs
  • Better for SEO

Adding PWA to rails app

  • Doc
  • Web page needs to be served over HTTPS (or localhost) IMPORTANT
    • If using pow, then you'll need to configure chrome to allow your local site to be secure:

      • Enter the following the addressbar and hit enter
      chrome://flags/#unsafely-treat-insecure-origin-as-secure
      
      • Add the domain of your site: https://example.test
      • Relaunch
    • Reload your browser and it should be secure-ish
    • Or just turn on ssl for the app in the appropriate environment: config.force_ssl = true
  • You'll need a web manifest, generated already if using the gem serviceworker-rails
  • The gem does most configuring and settings already, documentation

Understanding Events

  • 3 key events in the service worker lifecycle: install, activate, fetch

Install

  • Invoked the 1st time the service worker is requested or upated and redeployed prior to being activated.
  • Offline assets are pre-cached!
  • event.waitUntil() accepts a promies that MUST be successful in order for the service worker to be installed
  • caches.open() returns a promise that adds the static offline assets to a named cache associated with the site and the user's browser

Fetch

  • Service workers can intercept any external network request from the visitor's browser
  • The basic code filters out GET requests to the host
  • In order to provide the offline fallback, we ask the netowrk to fetch the offline page

Activate

  • Used to clean up old caches (when the offline page or any linked static resources change)
  • If the version number changes, the install event will be invoked again and during the activavte event, any cache names that do no match the new version will be removed!

  • To register the script file above, in application.js, be sure to include navigator.serviceWorker.register('/serviceworker.js');
{
  "name": "Alloy Application Demo",
  "short_name": "Alloy",
  "description": "An Episerver site with PWA added",
  "start_url": "/",
  "display": "standalone",
  "orientation": "any",
  "background_color": "#fff",
  "theme_color": "#fff",
  "icons": [
    {
      "src": "/static/img/icons8-ios-app-icon-shape-72.png",
      "sizes": "72x72",
      "type": "image/png"
    },
    {
      "src": "/static/img/icons8-ios-app-icon-shape-152.png",
      "sizes": "152x152",
      "type": "image/png"
    },
    {
      "src": "/static/img/icons8-ios-app-icon-shape-192.png",
      "sizes": "192x192",
      "type": "image/png"
    },
    {
      "src": "/static/img/icons8-ios-app-icon-shape-256.png",
      "sizes": "256x256",
      "type": "image/png"
    },
    {
      "src": "/static/img/icons8-ios-app-icon-shape-512.png",
      "sizes": "512x512",
      "type": "image/png"
    }
  ]
}
 // File is executed in the background without needing connection
 // ASYNCHRONOUS
 
 if ('serviceWorker' in navigator) {
  window.addEventListener('load', function () {
      navigator.serviceWorker.register('/sw.js').then(function (reg) {
          console.log('Registered Service Worker for Alloy');
      });
  });
  }
// Install event
self.addEventListener("install", function(e) {
    console.log("Alloy service worker installation");
    e.waitUntil(
        caches.open(cacheName).then(function(cache) {
            console.log("Alloy service worker caching dependencies");
            initialCache.map(function(url) {
                return cache.add(url).catch(function(reason) {
                    return console.log(
                        "Alloy: " + String(reason) + " " + url
                    );
                });
            });
        })
    );
});

// Activate Event
// Runs after the install event and page refresh
self.addEventListener("activate", function(e) {
    console.log("Alloy service worker activation");
    e.waitUntil(
        caches.keys().then(function(keyList) {
            return Promise.all(
                keyList.map(function(key) {
                    if (key !== cacheName) {
                        console.log("Alloy old cache removed", key);
                        return caches.delete(key);
                    }
                })
            );
        })
    );
    return self.clients.claim();
});


// Fetch Event
// Allows for offline functionality
// Returns results fom the cache when offline
self.addEventListener("fetch", function(e) {
    if (new URL(e.request.url).origin !== location.origin) return;

    if (e.request.mode === "navigate" && navigator.onLine) {
        e.respondWith(
            fetch(e.request).then(function(response) {
                return caches.open(cacheName).then(function(cache) {
                    cache.put(e.request, response.clone());
                    return response;
                });
            })
        );
        return;
    }

    e.respondWith(
        caches
            .match(e.request)
            .then(function(response) {
                return (
                    response ||
                    fetch(e.request).then(function(response) {
                        return caches.open(cacheName).then(function(cache) {
                            cache.put(e.request, response.clone());
                            return response;
                        });
                    })
                );
            })
            .catch(function() {
                return caches.match(offlinePage);
            })
    );
});
// https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API/Using_Service_Workers

var CACHE_VERSION = '1.0.4';
var CACHE_NAME = CACHE_VERSION + '-sw-cache';

/*
  Install is the first event sent to the service worker after registration
  The worker prepares to make resources available offline (caching files)
  When completed, an existing service worker may be active, in this case, the new worker will wait until
  all tabs have been closed to be active

  The new service worker can call skipWaiting() any time in the install phase
  to be asked to be activated immediatley upon install

*/

function onInstall(event) {
    // The promise that skipWaiting() returns can be safely ignored.
    self.skipWaiting();

  // console.log('[Serviceworker]', "Installing!", event);
  event.waitUntil(
    caches.open(CACHE_NAME).then(function(cache) {
      return cache.addAll([
        '/logo-kg.jpg',
        '/offline.html',
      ]);
    })
  );
}


/*
  This phase allows for cleanup of caches affected by the service worker installed
*/
function onActivate(event) {
  // console.log('[Serviceworker]', "Activating!", event);
  event.waitUntil(
    caches.keys().then(function(cacheNames) {
      return Promise.all(
        cacheNames.filter(function(cacheName) {
          // Return true if you want to remove this cache,
          // but remember that caches are shared across
          // the whole origin
          return cacheName.indexOf(CACHE_VERSION) !== 0;
        }).map(function(cacheName) {
          return caches.delete(cacheName);
        })
      );
    })
  );
}

/*
  Service workers can now access resources from the cache
  Fetch event is fired every time any resource controlled by a service workeris requested
  (includes documents inside the scope, resources referenced in the scope ex. cross origin requests)
  intercept the request by using `event.respondWith()`
*/
// Borrowed from https://github.com/TalAter/UpUp
function onFetch(event) {
  event.respondWith(
    // try to return untouched request from network first, catch will receive the failed request
    fetch(event.request).catch(function() {
      // if it fails, try to return request from the cache
      return caches.match(event.request).then(function(response) {
        if (response) {
          return response;
        }
        // if not found in cache, return default offline content for navigate requests
        if (event.request.mode === 'navigate' ||
          (event.request.method === 'GET' && event.request.headers.get('accept').includes('text/html'))) {
          // console.log('[Serviceworker]', "Fetching offline content", event);
          return caches.match('/offline.html');
        }
      })
    })
  );
}

self.addEventListener('install', onInstall);
self.addEventListener('activate', onActivate);
self.addEventListener('fetch', onFetch);