Publié le dans JavaScript

Progressive Web App

About

Google came with this idea by the fact that nobody (from regular users, the big majarity of customers of Google) install apps on smartphones. They use what is already installed and for the rest they all mostly use the website/webapp version (Twitter Mobile, Facebook Mobile), especially, but not only, on emergent markets where disk space is very limited (because it's expensive).

The Store/Application flow is bad by design (on mobile or desktop):

  1. Go to the website and discover the service 🎉
  2. There is an app for that™ 🤨
  3. Click the link to open the Store 😐 (1st fail because you lost the app focus)
  4. Oh! You need an account! 😣
  5. Oh! You need a credit card! 🤬 (2nd fail, especially if 🔞)
  6. Download the app and waaaaaaaait 😴
  7. Oh, you probably need Wi-fi 🤬 (remember few years ago)
  8. Finally go to the Application 😒
  9. Log in again... 😒
  10. Use the service 😅

vs. Web Application:

  1. Go to the website and discover the service 🎉
  2. Create an account / Log in 😒
  3. Use the service 😎
  4. Install later as Progressive Web App if wished (+ already downloaded)

Moreover as a developer you must rely on the vendor to publish/update your application on his store. While with PWA, you are the only stakeholder for that. - Which explains why Apple is not a big fan of that -

Support

Unfortunately it is still only available on Chrome Desktop / Mobile and Firefox Android. Partial support is available with Safari on iOS and iPadOS (aka. HTML5 Apps). Well, Apple's App Store does make 🤑. Available also in Microsoft Store and Play Store!

Code

Web manifest

index.html

<head>
  <link rel="manifest" href="./selector.webmanifest">
</head>

selector.webmanifest

{
  "name": "Genedata Selector",
  "short_name": "Selector",
  "start_url": "/home",
  "icons": [
    {
      "src": "./assets/logo-192.png",
      "sizes": "192x192",
      "type": "image/png"
    },
    {
      "src": "./assets/logo-512.png",
      "sizes": "512x512",
      "type": "image/png"
    }
  ],
  "theme_color": "#000000",
  "background_color": "#000000",
  "display": "standalone",
  "scope": "/",
  "orientation": "landscape"
}

Service Worker

index.js

if ("serviceWorker" in navigator) {
  navigator.serviceWorker.register("./service-worker.js");
}

service-worker.js

async function networkOnly(fetchEvent) {
  return fetch(fetchEvent.request);
}

self.addEventListener("fetch", networkOnly);

Strategies

/* --- Strategies --- */

//=> network ? save : cache
async function networkFirst(cacheName, fetchEvent) {
  const request = fetchEvent.request;
  let fetchResponse;

  return fetch(request)
    .then(response => toCache(cacheName, request, fetchResponse = response))
    .then(response => response
      ? Promise.resolve(response)
      : fromCache(cacheName, request))
    .then(response => response || fetchResponse);
}

//=> cache ? cache : (network ? save)
async function cacheFirst(cacheName, fetchEvent) {
  return fromCache(cacheName, fetchEvent.request)
    .then(response => response || networkFirst(cacheName, fetchEvent));
}

//=> cache ? cache : network => network ? save
async function staleWhileRevaliate(cacheName, fetchEvent) {
  return fromCache(cacheName, fetchEvent.request)
    .then(response => response
      ? Promise.race([
          Promise.resolve(response),
          networkFirst(cacheName, fetchEvent)
        ])
      : networkFirst(cacheName, fetchEvent));
}

//=> network
async function networkOnly(fetchEvent) {
  return fetch(fetchEvent.request);
}

//=> cache
async function cacheOnly(cacheName, fetchEvent) {
  return fromCache(cacheName, fetchEvent.request);
}

/* --- Helpers --- */

async function toCache(cacheName, request, response) {
  return response.ok
    ? caches.open(cacheName)
      .then(cache => cache.put(request, response.clone()))
      .then(() => response)
    : Promise.resolve(undefined);
}

async function fromCache(cacheName, request) {
  return caches.match(request, { cacheName });
}

Exemples

Documentation and Libraries