PWA

5.1 PWA Introduction

Module Still Under Development

# What is a PWA

A Progressive Web App is a website that meets certain criteria.

  • It has a Service Worker that supervises all HTTP Requests and Responses for the website.
  • It can be installed by the user. They need a Manifest file.
  • It has the ability to work even when offline.
  • It has a responsive interface.
  • They have fast reliable performance.
  • They can use the latest Web APIs.
  • They are secure, using HTTPS, and other features like Content-Security-Policies

Here is the full playlist about Progressive Web Apps (opens new window)

Video from Google I/O conference introducing PWAs and a Intro by Steve about PWAs.

Google maintains a web.dev website with a huge amount of developer resources. Here is the first section about PWAs (opens new window) and the second section about PWAs (opens new window).

# Manifest Files

A web manifest file is a JSON file that is used with Progressive Web Apps by the browser to understand settings like the name and version of a web app, the icon to use if it gets "installed", fullscreen mode, orientation, and theme values. It can have a .manifest or a .json file extension.

The file needs to include a name for the app and a launcher icon set. It needs to have instructions on how to display (or not display) the browser chrome. It needs to know what url is the start page for the app when launched from the home screen.

Here is a sample manifest file. While a few of these properties are optional, why skip things that improve the app experience?


 




































 


 



{
  "name": "My PWA Example",
  "short_name": "PWA Ex",
  "description": "This app suggests you know what you are doing with PWA",
  "icons": [
    {
      "src": "./img/favicon-16x16.png",
      "sizes": "16x16",
      "type": "image/png"
    },
    {
      "src": "./img/favicon-32x32.png",
      "sizes": "32x32",
      "type": "image/png"
    },
    {
      "src": "./img/apple-touch-icon.png",
      "sizes": "128x128",
      "type": "image/png",
      "purpose": "maskable"
    },
    {
      "src": "./img/mstile-150x150.png",
      "sizes": "150x150",
      "type": "image/png",
      "purpose": "maskable"
    },
    {
      "src": "./img/android-chrome-192x192.png",
      "sizes": "192x192",
      "type": "image/png"
    },
    {
      "src": "./img/android-chrome-512x512.png",
      "sizes": "512x512",
      "type": "image/png"
    }
  ],
  "orientation": "portrait-primary",
  "theme_color": "#bada55",
  "background_color": "#eeeeee",
  "display": "standalone",
  "start_url": "./index.html"
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44

The orientation property gives us a starting orientation for the app when it opens. For the full list look at the list on MDN (opens new window).

The display property can be standalone, fullscreen, browser or minimal-ui. browser makes it look like a normal browser... so why do that? standalone makes it look like a native app by removing all the menus. fullscreen removes some of the elements at the top of the screen. minimal-ui is like standalone but it is allowed to remove some navigation controls... so again, why?

We need to link all our HTML files, in our app, to the manifest file. We do this by adding a <link> tag inside the <head>.

<link rel="manifest" href="./mainfest.json" />
1

It should be found at the root of the domain. The same place as your service worker.

For more properties in your manifest file see the full reference on MDN (opens new window)

Did you know?

The display property that you use in a manifest file can also be used as part of a media query in your CSS using the display-mode property with one of the allowed values (opens new window)

@media (display-mode: fullscreen) {
  /* css for when your app is running as fullscreen */
}
1
2
3

The web manifest file does provide all the requirements outlined by the standard. Unfortunately, iOS Safari has lagged behind in support for PWAs and there are a number of additions that we need to add to our web pages inside the <head> to keep the experience decent for iOS.

Older Android can also use a couple of these.

<!-- pwa manifest -->
<link rel="manifest" href="manifest.json" />
<!-- older android support (manifest uses theme_color)-->
<meta name="theme-color" content="#bada55" />
<!-- ios support  (manifest theme_color and icons)-->
<link rel="apple-touch-icon" href="/img/apple-touch-icon.png" />
<meta name="apple-mobile-web-app-status-bar" content="#bada55" />
1
2
3
4
5
6
7

# Launcher Icons

One of the most tedious parts of building your web manifest can be the creation of all those icons.

Here is a site that you can use for building them.

Real Favicon Generator (opens new window)

# Making a PWA Installable

To be installable a Progressive Web App needs to meet some basic requirements.

  1. It must have a web manifest JSON file, which includes some minimal properties: a name or short_name to provide the name of the app; an icons array for launcher icons; a start_url for the home page location; and a display property that controls the appearance of the browser's chrome when installed.
  2. A user engagement heuristic, which means that the user has spent some time on the website and interacted with it.
  3. It has a registered Service Worker.
  4. It is not already installed.

For Chrome, the icons must include a 192px and a 512px icon.

# Offline Requirement since Chrome 89

Starting with Chrome 89, in March 2021, the fetch event listener needs to return a valid response if the app is offline. If it doesn't then the LightHouse test will fail. With Chrome 93, in August 2021, this will be an absolute requirement for installation.

# Handling Install Events in Chrome

If your user is visiting your website on any non-iOS mobile device using Chrome, Edge, or Opera then they will have access to an event that is the trigger for installing your PWA.

We can use this event to interrupt the process and create our own install experience.

On iOS we can create a similar install experience but we will have less control.

The event that we get on the Chrome/Chromium/Blink-based browsers is called beforeinstallprompt. When this event happens we know that the browser thinks that all requirements, including the heuristic engagement, have been met. This will be a trigger for us to save the event to be used later, in our own enhanced install experience.

//listen for Chrome install prompt
window.addEventListener('beforeinstallprompt', (ev) => {
  // Prevent the mini-infobar from appearing on mobile
  ev.preventDefault();
  // Save the event in a global property
  // so that it can be triggered later.
  APP.deferredPrompt = ev;
  console.log('deferredPrompt saved');
  // Build your own enhanced install experience
  // use the APP.deferredPrompt saved event
});
1
2
3
4
5
6
7
8
9
10
11

Later on, when we DO want to install our app at a more appropriate time, we can get that saved APP.deferredPrompt event and trigger displaying the install mini-infobar.

//check if we have the event saved
if (APP.deferredPrompt) {
  //trigger the display of the install prompt
  APP.deferredPrompt.prompt();
  //wait for the resulting Promise from the install prompt
  APP.deferredPrompt.userChoice.then((choiceResult) => {
    if (choiceResult.outcome === 'accepted') {
      //user says yes
      console.log('User accepted the install prompt');
      APP.deferredPrompt = null; //we will not need it again.
    } else {
      //user says not now
      console.log('User dismissed the install prompt');
    }
  });
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

In the Chrome/Chromium/Blink-based browsers we can also listen for an event that tells us that the App was installed, in case you want to trigger some action, like saving more files to the cache.

//listen for sign that app was installed
window.addEventListener('appinstalled', (evt) => {
  console.log('app was installed');
  //tell the service worker that the app has been installed
  //this can be the trigger to fetch and cache more assets
  let msg = {
    appInstalled: true,
  };
  navigator.serviceWorker.controller.postMessage(msg);
});
1
2
3
4
5
6
7
8
9
10

# Content-Security-Policies

The Content-Security-Policy is a security feature in the browser that lets you add to the headers or the <meta> tags in your document head, a list of the locations that the website is allowed to use to find all the different parts of the page. For example, if you had one place for fonts, two places for images, three allowed servers for scripts, and one allowed server for fetch calls, then all that can be defined in the Content-Security-Policy header.

The Content-Security-Policy can also be set as a header inside of an HTTP Response object. Any time you are setting a <meta> tag with an http-equiv attribute instead of a name attribute, it means that this could be a header in a Response object.

<meta http-equiv="x-ua-compatible" content="ie=edge" />
1

CSP official site (opens new window)

When you are setting the value for the Content-Security-Policy, it is made up of a series of different source values, like image-src or style-src. Each of those are followed by a string of allowed values. At the end of each source and value string there needs to be a semi-colon.

<meta
  http-equiv="Content-Security-Policy"
  content="
    default-src https 'self';
    image-src https://picsum.photos 'self'; 
    font-src fonts.gstatic.com;
    script-src 'self' jsdelivr.com;
    style-src 'self' fonts.googleapis.com;
    connect-src random-data-api.com 'self';
"
/>
1
2
3
4
5
6
7
8
9
10
11

The image-src is accepting images that come from the same domain as the html file plus https://picsum.photos. The font-src accepts only font files that come from the google fonts gstatic domain. The script-src limits script files from the same domain as the html file plus the jsdelivr CDN.

The style-src accepts css files from googleapis or the same domain as the html file. The connect-src provides locations for fetch and websocket calls.

The default-src is a fallback for other sources that are not listed. Never use * in the default-src. Never use only default-src. If you do it is like having no security at all.

# Authentication and JWT

When you need to do things like pass a username and password along with a fetch request, there are a few things you can do.

The following videos will be very useful to you later on when you start using JWT in MAD9124 and need to accept tokens on the client side.

Tokens has become the most common way to authenticate with fetch and API calls. This first video explains how tokens can be passed through fetch.

This next video shows the full life cycle of JWT (JavaScript Web Tokens) on the client and server. The Server-side part of this will be discussed in detail in MAD9124.

# Resources

Here is a great series by @netNinjaUK about creating a PWA. He walks through most things that we need for our PWA project, However he uses Firebase for his data storage instead of indexed DB and has no API fetch calls.

Net Ninja YouTube Series on PWAs (opens new window)

Steve Griffith Progressive Web Apps playlist (opens new window)

# ToDo This Week

To Do this week

Last Updated: 5/2/2024, 10:23:30 PM