Lecture 11

An introduction to PWA and the offline experience

slides.com/jkohlin/pwa

Questions

menti.com : 68 245

Resources

 

Johan's PWA Boilerplate, free to investigate

Progressive

Webb

Application

  • Discoverable, so the contents can be found through search engines.
  • Installable, so it's available on the device's home screen.
  • Linkable, so you can share it by simply sending a URL. 
  • Network independent, so it works offline or with a poor network connection.
  • Progressive, so it's still usable on older browsers, but fully-functional on the latest ones.
  • Re-engageable, so it's able to send notifications whenever there's new content available.
  • Responsive, so it's usable on any device — mobile phones, tablets, laptops, TVs, fridges
  • Safe, so the connection between you and the app is secured against any third parties trying to get access to your sensitive data.
  • Discoverable
  • Installable
  • Linkable
  • Network independent
  • Progressive
  • Re-engageable
  • Responsive
  • Safe

Components of a PWA

The manifest file

sets the environment

{
  "name": "Progressive Web application",
  "short_name": "PWA",
  "description": "my first PWA",
  "start_url": "/index.html",
  "lang": "en-us",
  "display": "standalone",
  "orientation": "portrait",
  "theme_color": "red",
  "background_color": "#cccccc",
  "icons": [   
    {
      "src": "images/icons/icon-72x72.png",
      "sizes": "72x72",
      "type": "image/png"
    },
    {
      "src": "images/icons/icon-96x96.png",
      "sizes": "192x192",
      "type": "image/png"
    }
}
<head>
...
<link rel="manifest" href="app.webmanifest">
</head>
<body>
...

app.webmanifest

index.html

var filesToCache=[
    "/",
    "/images.html",
    "/index.html",
    "/css/style.css",
    "/app.webmanifest",
    "/images/icons/icon-192x192.png",
];

var latestCacheName='App-Shell-v2';

self.addEventListener('install', function (event) {  // Lifecycle event #2    * * INSTALL * *
    event.waitUntil(
        caches.open(latestCacheName)
        .then(function (cache) {
        return cache.addAll(filesToCache)            // cache my app shell
        .then(() => self.skipWaiting());
        })
    );
});

self.addEventListener('activate', event => {         // Lifecycle event #3    * * ACTIVATE * *
    const activeCache=latestCacheName;
    event.waitUntil(
        caches.keys()
        .then(function(allCaches) {
            allCaches.forEach(cache => {
                if (activeCache!==cache) {
                    caches.delete(cache);                    // remove old caches if there's a new one
                }
            })
        })
    );
});


self.addEventListener('fetch', function (event) {     // Hijacking a request, listens for FETCH
    event.respondWith(
        caches.match(event.request)
        .then(function (response) {
            if (response) {
                return response;     // Cache found - return local cached file instead
            }
            return fetch(event.request);
        })
    );
});

serviceworker.js

PENDING

RESOLVED

REJECTED

with a value

with a reason

promises

.then()
.catch()

Ordering at Max

orderPromise  = orderAtMax("Spicy Brioche Halloumi");
deliverPromise = orderPromise.then( function(burger){ eat(burger) } );
deliverPromise.catch( function(reason) { alert(reason) } );

// or:

orderAtMax("Spicy Brioche Halloumi")
.then(function(burger){
    eat(burger);  // yummi!
})
.catch(function(reason) {
    alert(reason) // sorry, out of Brioche
})

Example: Fetch

fetch("images.html")
.then(function(response) {
    response.text()
    .then(function(htmlText) {
        document.body.innerHTML=htmlText;
    });
})
.catch(function (err){
    console.error(err);
});

Service Worker

Deals with the following:

CACHE

but what is a worker?

PUSH / NOTIFICATION

BACKGROUND SYNC

Web Worker

a script file running parallel to the main thread

//main.js
var ww = new Worker('/webworker.js');

ww.onmessage = function(event) {
  alert( "The answer to everything is " + event.data );
}
// web worker
var answer = parseInt(Math.random()**Math.atan2(Math.E, Math.PI)*100)

self.onmessage = e => {
  self.postMessage(answer)
}

Service Worker

The service worker is a special worker,

with a proxy server like behaviour

All requests goes through the service worker

Being in the middle, it can cache files.

Later, it can return the cached files,

rather than downloading them again

And voila! the app works offline

Service worker life cycle

Register the Service worker

This can be done from index.html

<head>
<script>


    navigator.serviceWorker.register('/sw.js')
    .then(function (regObj) {
      console.log('ServiceWorker registration successful');
    })
    .catch(err=> {
      console.error('blip blop failure: ', err);
    });


</script>
</head>
<body>

Installation event

when installed, cache files for offline use.

var filesToCache=[
  "/",
  "/images.html",
  "/index.html",
  "/css/style.css"
];

latestCacheName = 'App-Shell-v9';

self.addEventListener('install', function (event) {
  event.waitUntil(
    caches.open(latestCacheName)
      .then(function (cache) {
        console.log('Opened cache');
        return cache.addAll(filesToCache)
        .then(() => self.skipWaiting());
      })
  );
});

sw.js

Activation event

Service worker is now in control of window(s)

self.addEventListener("activate", event => {

  console.log("I am activated and ready to start working!");
  
});

// if we changed files and need to re-cache new files,
// lets see how to remove old cache first.

Activation event

here we also remove all old caches, except the latest one

latestCacheName = 'App-Shell-v9';
/************************************************************
    Activating new service worker. Remove old named caches
******************************/
self.addEventListener('activate', event => {
  event.waitUntil(
    caches.keys().then(allCaches => {
        allCaches.forEach(cache => {
          if (latestCacheName !== cache) { //don't remove the current cache
            caches.delete(cache);
          }
        })
    })
  );
});

Updating Service worker

Updating Service worker

self.addEventListener('install', function (event) {
  event.waitUntil(
    caches.open(latestCacheName)
      .then(function (cache) {
        console.log('Opened cache');
        return cache.addAll(filesToCache)
        .then(() => self.skipWaiting());
      })
  );
});

the Fetch event

this is where we hijack the request and serve from the cache

/* * * * * * * ***********************
    How to respond to a fetch event.
    if match in cache, return 
    cached file, else return 
    the requested file from 
    the web server.
******************************/
self.addEventListener('fetch', function (event) {
  event.respondWith( 
    caches.match(event.request)
      .then(function (response) {
        if (response) {
          console.log(response.url + " found in cache");
          return response;
        }
        return fetch(event.request); // not in cache. Go get!
      })
  );
});

example: notification

Notification.requestPermission()
.then(function(permission){
  if (permission === "granted") {
    new Notification("Splendid!");
  }
});

Notifications are not part of the service worker, 

PWA

By Johan Kohlin

PWA

  • 835