Guide - ShukujiNeel13/my-first-pwapp GitHub Wiki

Prerequisites

  1. A recent chrome browser
  2. Knowledge of HTML, CSS, JavaScript, and Chrome DevTools

Workshop aims:

  1. Create and add a web app manifest.
  2. Provide a simple offline experience.
  3. Provide a full offline experience.
  4. Make your app installable.

Workshop Part 1: Setup Playground and Play !

1. Get a key for Dark Sky API

Visit: Dark Sky API

  • This is the source of the weather data (Dark Sky)
  • You need to sign up first to get your free API Key
    • Just click "Try for Free" and then sign up.
    • Click on the link you receive on email to complete verification.
    • Log in and generate your API key.
      • Copy and paste this somewhere or just keep this page open.

Dark Sky Dashboard

2. Get the code from Github repository

NOTE: Ignore this step if you already cloned the repository from Github

  • (Recommended:) Set up local environment (you can play with different settings and platforms)
  • Download source code as zip file for Codelab Note: Link is available in the codelab also:
  • (Can try later) Use Glitch
    • Collaborative platform for app development on the web
    • Offers quick and easy setup and preview while you code.

3. Steps for local environment setup.

NOTE: You cant proceed if you dont have Nodejs installed.

  • Navigate to the folder of this project (my-first-pwapp)
  • Run npm install to install the dependencies.
  • Open the file server.js and enter your DarkSky API key in the required line of code
    • const API_KEY = <your API Key>
    • Hint: Search for the word: API_KEY
  • Run node server.js to start the server on Port 8000 (port is set in code)
  • Open Google chrome and visit the URL: http://localhost:8000/
  • Play around with the web app

Alternative - If using glitch (can try but not recommended)

NOTE: Please ignore this if you have decided to do local setup above.

  • Goto https://glitch.com, create account Click on New Project and select Clone from Git Repo. Enter the Github URL (link to codebase) here
  • Once the project has loaded, go to the .env file and update it with the DarkSky API key
  • Click Show Live to see the PWA in action

4. Things to try out now:

  • Add a new city
  • Delete a city
  • See how it works on a small screen (to mimic a mobile app screen)
    • Resize the browser window to minimum width and a suitable height
  • What happens when you go offline
    • Switch off internet and reload the page.
  • Open Chrome DevTools (right click on web page and click inspect)
    • Go to the network tab and observe this panel as you:
      • Reload the page
      • Add a new city
      • Delete an existing city
    • Change the throttle to Slow 3G and do the above steps
      • Observe how the web app behaves
  • OPTIONAL: Add a delay to the forecast server
    • By changing the value of <FORECAST_DELAY> in server.js

5. Audit with Lighthouse

  • Lighthouse is an easy to use tool to help improve the quality of your sites and pages.
  • It allows you to run Audits for aspects such as:
    • Performance
    • Accessibility
    • Progressive Web Apps
    • SEO
  • Each Audit has a reference doc explaining why the audit is important as well as how to fix it.
  • View the audit reports and focus on the audit for PWA
  • We will fix the issues highlighted by the PWA audit report

Workshop Part 2: Start adding code !

Hit CTRL / CMD + C to stop serving the app.

  • Need to re-start as a new file is being added.

1. Add a web app manifest

  • This will take care of the following points highlighted by Audit report

    • Web app manifest does not meet the installability requirements
    • Is not configured for a custom splash screen
    • Does not set an address-bar theme colour
  • The web app manifest is a JSON file

  • Gives developer control on how the app appears to the user.

  • This gives the web app additional features like:

    • Browser should open the app in a standalone window (display)
    • Define the landing page (start_url)
      • Choose which page shows up when app is first opened.
    • Define how app looks like in the dock or app launcher (short_name, icons)
    • Create a splash screen (name, icons, colours)
    • Tell the browser to open window in landscape or potrait mode (orientation)
  • TODO: Create a file manifest.json inside the public directory of the project.

Copy paste the below code to manifest.json.

{
  "name": "Weather",
  "short_name": "Weather",
  "icons": [{
    "src": "/images/icons/icon-128x128.png",
      "sizes": "128x128",
      "type": "image/png"
    }, {
      "src": "/images/icons/icon-144x144.png",
      "sizes": "144x144",
      "type": "image/png"
    }, {
      "src": "/images/icons/icon-152x152.png",
      "sizes": "152x152",
      "type": "image/png"
    }, {
      "src": "/images/icons/icon-192x192.png",
      "sizes": "192x192",
      "type": "image/png"
    }, {
      "src": "/images/icons/icon-256x256.png",
      "sizes": "256x256",
      "type": "image/png"
    }, {
      "src": "/images/icons/icon-512x512.png",
      "sizes": "512x512",
      "type": "image/png"
    }],
  "start_url": "/index.html",
  "display": "standalone",
  "background_color": "#3E4EB8",
  "theme_color": "#2F3BA2"
}

Notes:

  • The manifest supports an array of icons for different screen sizes

  • To be installable, Chrome requires that you provide at least a 192x192px icon and a 512x512px icon. But you can also provide other sizes. Chrome uses the icon closest to 48dp, for example, 96px on a 2x device or 144px for a 3x device.

  • TODO: Link the web app manifest in your app

    • Tell the browser about the manifest file by adding a link tag in each page of the app
    • Add the following line to the <head> element in your index.html file
<!-- Add link: rel manifest-->
<link rel="manifest" href="/manifest.json">
  • Tip: Try this

  • Chrome DevTools provides a quick, easy way to check your manifest.json file.

  • Open up the Manifest pane on the Application panel.

    If you've added the manifest information correctly, you'll be able to see it parsed and displayed in a human-friendly format on this pane.

2. Add meta tags and touch icon for iOS

  • Safari does not yet support manifest file. This passes the required info.
  • TODO: Copy paste the below code into index.html inside the <head> element
<!-- Add iOS meta tags and icons -->
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="apple-mobile-web-app-title" content="Weather PWA">
<link rel="apple-touch-icon" href="/images/icons/icon-152x152.png">

3. Set the meta description

  • The Audit report (SEO section) pointed out that:

    Document does not have a meta description

  • Descriptions can be displayed in Google's search results.

    High quality unique descriptions can make your results more relevant to search users and can increase your search traffic.

  • TODO: Add the following meta tag inside the <head> element of index.html

<!-- Add meta description here -->
<meta name="description" content="A sample weather app">

4. Set the address bar theme colour

  • Audit report pointed out that:

    Does not set an address bar theme colour.

  • For an immersive user experience it is recommended to set theme of your address bar to match the colours of your brand.

  • TODO: Add the following meta tag to the head of your document

<!-- Add meta theme colour-->
<meta name="theme-colour" content="#2F3BA2" />

This also sets the theme colour for mobile platforms.

5. Re-Run the Audit again

  • TODO: Launch the web app, Refresh the browser page and rerun the Audit.
  • Confirm that the issues pointed before are resolved (Audits pass)
  • The following will be the status (most likely)
SEO Audit
βœ… PASSED: Document has a meta description.

Progressive Web App Audit
❗FAILED: Current page does not respond with a 200 when offline.
❗FAILED: start_url does not respond with a 200 when offline.
❗FAILED: Does not register a service worker that controls page and start_url.
βœ… PASSED: Web app manifest meets the installability requirements.
βœ… PASSED: Configured for a custom splash screen.
βœ… PASSED: Sets an address-bar theme color.

6. Provide a basic offline experience with a Service Worker

It's a big step, brace yourselves :)

  • The goal is to show something meaningful or customized when the app is offline.

    • A simple (custom) offline page
    • Read only experience with previously cached data
    • Fully functional offline experience that syncs when the network connection is restored
  • Warning: Service worker functionality is only available on pages that are accessed via HTTPS (http://localhost and equivalents will also work to facilitate testing).

  • TODO: Add an offline page to the app

    • One is created for you. Take a look at offline.html inside public directory.
    • There is a surprise sitting in there, which we will open later :) Hint: Panda
      • To try later: You can add a base 64 encoded string representation of your own image in here
        • Replace the src attribute of the <img> element in this file.
  • Result: If the user tries to load the app while offline, it will show a custom page instead of the browser default offline page (Chrome dinosaur)

  • The following audits will be passed by doing this

    • current page does not respond with a 200 when offline
    • start_url does not respond with a 200 when offline
    • Does not register a service worker that controls page and start_url

    In the next section, we'll replace our custom offline page with a functional offline experience. Not only this improves the user experience, it'll also significantly improve our app performance because most of our assets (HTML, CSS and JavaScript) will be stored and served locally, eliminating the network as a potential bottleneck.

    Features provided via Service Workers (considered a progressive enhancement) should be added only if they are supported by the browser. For example: With service workers you can cache the app shell and data so that it is available even when the network isn't. When service workers are not supported, the offline code is not called, and the user gets a basic experience.

  • TODO: Register the service worker by adding the following script to your index.html file

<!-- Register the Service Worker-->
<script>
if ('serviceWorker' in navigator) {
    window.addEventListener('load', () => {
        navigator.serviceWorker.register('service-worker.js').then((reg) => {
            console.log('Service worker registered.', reg);
        });
    });
}
</script>

This code checks to see if the service worker API is available. If it is, the service worker at /service-worker.js is registered once the page is loaded.

The scope of service worker controls which files the service worker controls, i.e. from which path it will intercept requests. The default scope is the directory path of the service worker file and extends to all directories below.

The service worker is placed in the root directory (so it will be able to control requests from all web pages in this domain)

  • We will now precache the offline page

    • Tell the service worker what to cache (an offline page)
    • Create a simple offline page offline.html in public directory (Already done)
    • This page will be displayed when the device is offline.
  • TODO: In public/service-worker.js file, add the path to offline page (/offline.html) in the FILES_TO_CACHE array.

const FILES_TO_CACHE = [
    '/offline.html',
];

Note both files service-worker.js and offline.html are inside the same public directory

  • Update the install event in service-worker.js to pre-cache the offline page.
    • From the following code snippet, below the //TODO, add it to the file.
self.addEventListener('install', (evt) => {
  console.log('[ServiceWorker] Install');
  // TODO: Precache static resources here.
  evt.waitUntil(
    caches.open(CACHE_NAME).then((cache) => {
      console.log('[ServiceWorker] Pre-caching offline page');
      return cache.addAll(FILES_TO_CACHE);
    })
  );
  self.skipWaiting();
});

Notes: The install event opens the cache with cache.open() and provides a cache name Providing a name helps version files and separate data from cached files so that we can update one without affecting other.

Once cache is open, cache.addAll() is called, which takes a list of URLs, fetches them from the server and adds the response to the cache.

If any individual request fails during cache.addAll() it will reject caching anything. Therefore if the install step succeeds, the cache will be consistent, otherwise if it fails for any reason, it will automatically try again the next time service worker starts up.

  • Open DevTools, Go to the Application Tab and click on section Service Workers (left)

  • Reload the page and the Service Worker will get activated and displayed here.

    The status shows a number, which is unique to every run. Anytime service worker is updated, the number in status will change.

  • Clean up old offline pages.

    • Use the activate event to clean up any old data in our cache.
    • This ensures that the service worker updates the cache whenever any of the app shell files change. (important)
    • In order for this to work, the version number in the value of CACHE_NAME variable (at top of service-worker.js) needs to be incremented. const CACHE_NAME = 'static-cache-v2'; // Updated from v1 to v2
  • From the following code snippet, copy the code below //TODO and add it to the same place service-worker.js file

self.addEventListener('activate', (evt) => {
  console.log('[ServiceWorker] activate');
  // TODO: Remove previous cached data from disk.
  evt.waitUntil(
    caches.keys().then((keyList) => {
        return Promise.all(keyList.map((key) => {
            if (key !== CACHE_NAME) {
                console.log('[ServiceWorker] Removing old cache', key);
                return caches.delete(key);
            }
        }));
    })
  );
  self.clients.claim();
});

Note: The updated service worker takes control immediately because our install event finishes with self.skipWaiting(), and the activate event finishes with self.clients.claim(). Without those, the old service worker would continue to control the page as long as there is a tab open to the page.

  • Handle failed network events.

    • Use the fetch event in Service Worker
    • Caching Strategy: Network falling back to Cache

    Reference: The Offline Cookbook

network falling back to cache

This strategy entails:

The service worker will first fetch the resource(s) from the network.
If this fails, it will return those resource(s) from cache.
  • TODO: Add the following to service-worker.js

Fetch event VERSION 1

self.addEventListener('fetch', (evt) => {
  console.log('[ServiceWorker] Fetch', evt.request.url);
  // TODO: Add fetch event handler here.
  if (evt.request.mode !== 'navigate') {
    // Not a page navigation, bail out.
    return;
  }
  evt.respondWith(
    fetch(evt.request).catch(() => {
      return caches.open(CACHE_NAME).then((cache) => {
        return cache.match('offline.html');
      });
    }));
  });

Explanation:

  • The fetch handler only needs to handle page navigations

    • So, other requests are ignored by the handler (handled normally by the browser)
    • So, if request.mode is "navigate", use fetch to try to get the item from the network.
    • If it fails, the attached catch handler opens the cache with caches.open(CACHE_NAME) and uses cache.match('offline.html') to get the precached offline page.
    • The result is then passed back to the browser using evt.respondWith()

    NOTE: Wrapping the fetch call in evt.respondWith() prevents the browser from applying the default fetch behaviour.

    • Tells the browser I myself want to handle the response.
    • If you dont call fetch inside evt.respondWith(), you will just get the default network behaviour
    • Read about it online - Search: Why use evt.respondWith()
  • Go to the Cache Storage pane in the Application tab of Chrome DevTools

  • Right click on the pane and click refresh caches.

    • The cache file shows up. Open it and preview the cached data.
  • Test out the offline mode

    • Open Service Workers in DevTools, check the Offline checkbox.

    • A warning symbol appears next to Network tab indicating there is no network (offline)

    • Now refresh the browser page, and the custom offline page we created shows up instead of the default chrome dinosaur page!

      Yes, this was the offline page surprise 🐼

  • Run lightout Audits on the app again:

Expected results:

SEO Audit
βœ… PASSED: Document has a meta description.

Progressive Web App Audit
βœ… PASSED: Current page responds with a 200 when offline.
βœ… PASSED: start_url responds with a 200 when offline.
βœ… PASSED: Registers a service worker that controls page and start_url.
βœ… PASSED: Web app manifest meets the installability requirements.
βœ… PASSED: Configured for a custom splash screen.
βœ… PASSED: Sets an address-bar theme color.

7. Provide a full offline experience:

Boss Level 😎

  • Designing for offline-first can drastically improve the performance of your web app by reducing the number of network requests made by your app.

  • Instead resources can be precached and served directly from the local cache.

  • Even with the fastest network connection, serving from the local cache will be faster!

  • Pre-requisite: Understand the next two sections


Section 1: Service worker lifecycle

Service Worker lifecycle

  1. event: install:
  • The first event a service worker gets is install. It's triggered as soon as the service worker executes, and is called only once per service worker.

  • If you alter your service worker script, the browser considers it a different service worker, and it'll get its own install event.

      Note: Typically the **install** event is used to cache everything that is required for the app to run.
    
  1. event: activate:
  • The service worker will receive an activate event everytime it starts up.
  • This event allows you to
    • Configure the service worker's behaviour,
    • Clean up any resources left behind from previous runs (eg. old caches)
    • Get the service worker ready to handle network requests (for example the fetch event described below)
  • For example, activate is called everytime you refresh the browser while the app is running.
  1. event: fetch
  • This event allows the service worker to intercept any network requests and handle requests.
  • It can go to the network to get the resource, or pull it from it's own cache, generate a custom response or other options.
  • Check out the offline cookbook for various offline strategies, (Reference to cookbook in last section.)

2. Updating a Service Worker

  • The browser checks if there is a new version of the Service Worker on each page load.
  • If there is one, it gets downloaded and installed in the background, but does not get activated.
  • It sits in the waiting state, until there are no longer any pages open that use the old service worker.

As all pages using the old service worker are closed, the new service worker gets activated and takes control!


Section 2: Caching strategies

Choosing the right caching strategy

This depends on the type of resources you are trying to cache and how you might need it later.

Note: For this Project we will split the resources to cache into 2 categories

  1. Resources we want to pre-cache
  2. Data that we will cache at runtime

  • Caching Static resources
    • The resources required to run the app are installed or cached on the device so that it can run later, whether there is network connection or not.
    • For our app, we will pre cache all the static resources when the service worker is installed (So that everything required to run the app are stored on the users device)

Now we will look at caching strategies

1. Cache First strategy

  • Helps our app load lightning fast (Please Refer to caching strategies in offline cookbook)

  • This strategy entails:

    Instead of going to the network to get the resources, they are pulled from the local cache. Only if it is not available in cache we will try to get it from the network.
    

Benefit:

Pulling from the local cache ensures there is no network variability. No matter what kind of network the user is on, the key resources we need to run the app are almost immediately available

**Caution:**

  • In this app, static resources are served using a cache-first strategy
  • This results in a copy of any cached content being returned without consulting the network.
  • While this is easy to implement, it can cause challenges in the future.
    • Read about it online or in the Offline Cookbook

2. Stale while revalidate strategy

  • This strategy is ideal for certain types of data and works well for our app.

  • This strategy entails:

    Get data on screen as quickly as possible, then update those once the network has returned latest data.
    
  • This means we need to kick off two asynchronous requests:

  1. one to the cache
  2. one to the network

stale while revalidate strategy

  • Under normal circumstances, the cached data will be returned almost immediately, providing the app with recent (but not latest) data it can use.

  • Then when the network request returns, the app will be updated using latest data from the network

  • This strategy **provides a better experience than** the network falling back to cache strategy.

    The user does not have to wait till network request times out to see something on screen.
    They may initially see older data, but as soon as network request returns, the app will be updated with the latest data.
    

Update App Logic

  • Note: We will now open the file public/scripts/app.js
  • The app uses caches object available in **window** object to access the cache and retrieve the data.
  • Caution: The caches object may not be available in all the browsers, and if it's not, the network request should still work

So this is an excellent example of Progressive content

  • In app.js Update the getForecastFromCache() to check if the caches object is available in the global window object, and if it is, request the data from cache

Add the following code to the public/scripts/app.js file inside the function: getForecastFromCache()

if (!('caches' in window)) {
    // no caches object found. Will not request cache.
    return null;
}
const url = `${window.location.origin}/forecast/${coords}`;
return caches.match(url).then((response) => {
    if (response) {
        return response.json();
    }
    return null;
}).catch((err) => {
    console.error('Error getting data from cache', err);
    return null;
});
  • Modify updateData() so that is makes 2 calls, to the following:
  1. getForecastFromNetwork()
  2. getForecastFromCache()
getForecastFromCache(location.geo).then((forecast) => {
    renderForecast(card, forecast);
});

Note: Both cache request and fetch request end with a call to update the forecast card. How does the app know whether it's displaying the latest data? this is handled in the following code from renderForecast()

// If the data on the element is newer, skip the update.
if (lastUpdated >= data.currently.time) {
    return;
}

Everytime a card is updated, the app stores the timestamp of the data on a hidden attribute on the card. The app just bails out if the timestamp already set on the card is newer than that of the data that was passed to the function.

8. Pre cache our app resources.

  • Add a constant DATA_CACHE_NAME in the service worker so that we can separate our applications data from the app shell.
  • When the app shell is updated and older caches removed, our application data will remain unaffected.

Note: If your data format changes in future you need a way to handle that and ensure the app shell and content stay in sync.

  • Update the CACHE_NAME to next version as we will change our static resources as well
  • We had manually added the list of files to cache
    • So everytime we update a file, we must update the CACHE_NAME

What is an app shell?

(More on the link given in references below)

An application shell (or app shell) architecture is one way to build a Progressive Web App that reliably and instantly loads on your users' screens, similar to what you see in native applications.

The app "shell" is the minimal HTML, CSS and JavaScript required to power the user interface and when cached offline can ensure instant, reliably good performance to users on repeat visits.

This means the application shell is not loaded from the network every time the user visits. Only the necessary content is fetched from the network.
  • Update the FILES_TO_CACHE array with the list of files to cache (given below in code snippet)

  • What are we adding here. Particularly:

    • '/' (the app root directory, so that the app project structure is also cached, and then you can point out to other files relative to the app root directory)
    • index.html
    • scripts/* (contains app.js and install.js)
    • styles/* (contains all css and other style / theme related files)
    • images/* (contains all the static images for app.)
const FILES_TO_CACHE = [
    '/',
    '/index.html',
    '/scripts/app.js',
    '/scripts/install.js',
    '/scripts/luxon-1.11.4.js',
    '/styles/inline.css',
    '/images/add.svg',
    '/images/clear-day.svg',
    '/images/clear-night.svg',
    '/images/cloudy.svg',
    '/images/fog.svg',
    '/images/hail.svg',
    '/images/install.svg',
    '/images/partly-cloudy-day.svg',
    '/images/partly-cloudy-night.svg',
    '/images/rain.svg',
    '/images/refresh.svg',
    '/images/sleet.svg',
    '/images/snow.svg',
    '/images/thunderstorm.svg',
    '/images/tornado.svg',
    '/images/wind.svg',
];

Did you notice? '/offline.html' added previously in FILES_TO_CACHE has been removed. Why? Guesses?

Our app now has all the necessary resources to work offline, and we will never need to show the offline page again!

Remember:

  • Since, In this sample, we hand-rolled our own service worker.
  • Each time we update any of the static resources, we need to re-roll the service worker and update the cache
    • Otherwise the old content will get served.
  • In addition, when one file changes, the entire cache is invalidated and needs to be re-downloaded.
    • That means fixing a simple single character spelling mistake will invalidate the cache and require everything to be downloaded again...

...Not exactly efficient.

Workbox handles this gracefully, by integrating it into your build process, only changed files will be updated, saving bandwidth for users and easier maintenance for you!

  • Workbox is not covered in this workshop.

  • Update the activate event handler

    • To ensure our activate event doesnt accidentally delete our data
    • In the activate event handler of service-worker.js, replace the the below statement with with the statement given below that!
Note: in this workshop The functions are already provided, 
Just Uncomment appropriate section below a TODO.

Only need to change a single line between the activate event function 1 and function 2: BEFORE:

if (key !== CACHE_NAME) {}

AFTER:

if (key !== CACHE_NAME && key !== DATA_CACHE_NAME) {}

Intercept requests to network

For those resources which we need to cache

  • Update the fetch event handler
    • Need to modify the service worker to intercept requests to the weather API and store their responses in the cache so we can easily access them later.
In the stale while revalidate strategy, we expect the response from network request to be the 'source of truth', If it cant get response from network request, it  is okay to fail as we already have fetched the latest data from cache.
  • Update the fetch event handler to handle requests to the Data API seperately from other requests...
// TODO: Add fetch event handler here.
if (evt.request.url.includes('/forecast/')) {
  console.log('[Service Worker] Fetch (data)', evt.request.url);
  evt.respondWith(
      caches.open(DATA_CACHE_NAME).then((cache) => {
        return fetch(evt.request)
            .then((response) => {
              // If the response was good, clone it and store it in the cache.
              if (response.status === 200) {
                cache.put(evt.request.url, response.clone());
              }
              return response;
            }).catch((err) => {
              // Network request failed, try to get it from the cache.
              return cache.match(evt.request);
            });
      }));
  return;
}
evt.respondWith(
    caches.open(CACHE_NAME).then((cache) => {
      return cache.match(evt.request)
          .then((response) => {
            return response || fetch(evt.request);
          });
    })
);

Explanation of the above code

  • I.e. Changes made to fetch handler in service-worker.js
The code intercepts the request and checks if it is for a weather forecast.

If it is,use fetch to make the request. 

Once the response is returned, clone the response and store it in the cache, then return the response to the original requestor.

We need to remove the evt.request.mode !== 'navigate' check because we **want our service worker to handle all requests** (including images, scripts, CSS files, etc), not just navigations. 

If we left that check in, only the HTML would be served from the service worker cache, everything else would be requested from the network.

Try the app now:

  • Ensure your app is running (being served)
  • Refresh the page to ensure that you've got the latest service worker installed.
  • Add a couple of cities and press the refresh button on the app to get fresh weather data.

  • Then go to the Cache Storage pane on the Application panel of DevTools.
  • Expand the section and you should see the name of your static cache and data cache listed on the left-hand side.
  • Opening the data cache should show the data stored for each city.
  • While in DevTools switch to the Service Workers pane, and check the Offline checkbox,
  • Reload the page ... What do you see

If you want to see how weather forecast data is updated on a slow connection: Set the FORECAST_DELAY property in server.js to 5000.

  • All requests to the forecast API will be delayed by 5000ms.

Cached App Data

Verify changes with Lighthouse Run the Audit again, the results will be as follows:

SEO Audit βœ… PASSED: Document has a meta description.

Progressive Web App Audit βœ… PASSED: Current page responds with a 200 when offline. βœ… PASSED: start_url responds with a 200 when offline. βœ… PASSED: Registers a service worker that controls page and start_url. βœ… PASSED: Web app manifest meets the installability requirements. βœ… PASSED: Configured for a custom splash screen. βœ… PASSED: Sets an address-bar theme color.

Add an Install Experience

In Chrome, a Progressive Web App can either be installed through the three-dot context menu, or you can provide a button or other UI component to the user that will prompt them to install your app.

Useful Tip: Since the install experience in Chrome's three-dot context menu is somewhat buried, we recommend that you provide some indication within your app to notify the user your app can be installed, and an install button to complete the install process.

In order for a user to be able to install your Progressive Web App, it needs to meet certain criteria. The easiest way to check is to use Lighthouse and make sure it meets the installable criteria. Such as the following

  • Uses HTTPS.

  • Registers a service worker that controls page and start_url.

  • Web app manifest meets the installability requirements.

    NOTE: By now your PWA should already meet these criteria (if all steps followed).

Todo: Key Point: For this section, enable the Bypass for network checkbox in the Service Workers pane of the Application panel in DevTools. When checked, requests bypass the service worker and are sent directly to the network. This simplifies our development process since we don't have to update our service worker while working through this section.

Add install.js to index.html

<!-- TODO: Add the install script here -->
<script src="/scripts/install.js"></script>
  • Listen for beforeinstallprompt event

If the add to home screen criteria are met, Chrome will fire a beforeinstallprompt event,that you can use to indicate your app can be 'installed', and then prompt the user to install it. Add the code below to listen for the beforeinstallprompt event:

In public/scripts/install.js

// TODO: Add event listener for beforeinstallprompt event
window.addEventListener('beforeinstallprompt', saveBeforeInstallPromptEvent);
  • Save event and show Install button

In our saveBeforeInstallPromptEvent function, we'll save a reference to the beforeinstallprompt event so that we can call prompt() on it later, and update our UI to show the install button.

  • Show the prompt, hide the button

When the user clicks the install button, we need to call .prompt() on the saved beforeinstallprompt event. We also need to hide the install button, because .prompt() can only be called once on each saved event.

In public/scripts/install.js inside function installPWA(evt) add the following.

// CODELAB: Add code show install prompt & hide the install button.
deferredInstallPrompt.prompt();

// Hide the install Button as it had been called (it cant be called twice)
evt.srcElement.setAttribute('hidden', true);
// installButton.setAttribute('hidden'); // this could be another alternative?

Explanation: Calling .prompt() will show a modal dialog to the user, asking them to add your app to their home screen.

  • Log the results

You can check to see how the user responded to the install dialog by listening for the Promise returned by the userChoice property of the saved beforeinstallprompt Event. The Promise returns an object with an outcome property after the user has responded to the prompt shown.

In public/scripts/install.js inside installPWA() function

  // CODELAB: Log user response to prompt.
  deferredInstallPrompt.userChoice.then((choice) => {
    if (choice.outcome === 'accepted') {
      console.log('User accepted A2HS prompt', choice);
    } else {
      console.log('User dismissed A2HS prompt', choice);
    }
    deferredInstallPrompt = null;
  });
  • Log all install events

In addition to installing your PWA through the install prompt, users can also install it via other ways like browser function (eg 3 dot menu in Chrome) To track these events listen for the appinstalled event

// CODELAB: Add event listener for appinstalled event
window.addEventListener('appinstalled', logAppInstalled);

Update the logAppInstalled function. Here use a console.log() for simplicity. In a production app you may be required to use your analytics software.

  • Update the logAppInstalled(evt)
function logAppInstalled(evt) {
  // CODELAB: Add code to log the event
  console.log('Weather App was installed.', evt);
}
Update the Service Worker
  • Update the CACHE_NAME in your service-worker.js since you made changes to files already cached.

  • Try the app now:

Let's see how our install step went. To be safe, use the Clear site data button in the Application panel (Clear Storage section) of DevTools to clear everything away and make sure we're starting fresh. If you previously installed the app, be sure to uninstall it, otherwise the install icon won't show up again.

Workshop Complete

Thank you πŸ˜ƒ


  • Bonus: Detecting if your app is launched from the home screen Too much already? ... yup, we're not covering this :)

The display-mode media query makes it possible to apply styles depending on how the app was launched, or determine how it was launched with Javascript

@media all and (display-mode: standalone) {
  body {
    background-color: yellow;
  }
}

It's also possible to detect if the display-mode is standalone from Javascript

if (window.matchMedia('(display-mode: standalone)').matches) {
  console.log('display-mode is standalone');
}
  • For Safari: To determine if app was launched in standalone mode from Safari, you can use Javascript to check:
if (window.navigator.standalone === true) {
  console.log('display-mode is standalone');
}
  • Updating App Icon and Name: On Android when WebAPK is launched, Chrome will check the currently installed manifest against the live manifest. If an update is required, it will be queued and updated (once device being charged and connected to Wifi)

  • Test the A2HS experience

Helpful: You can manually trigger the beforeinstallprompt event using Chrome DevTools. This makes if possible to see the user experience, understand how the flow works, or debug the flow. If PWA criteria are not met, Chrome will throw an exception in the console and the event will not be fired.

Caution: Chrome has a slightly different install flow for desktop and mobile. Although the instructions are similar, testing on mobile requires remote debugging; without it, Chrome will use the desktop install flow.

  • Tip: The easiest way to test if the beforeinstallprompt event will be fired, is to use Lighthouse to audit your app, and check the results of the User Can Be Prompted To Install The Web App test.

  • Uninstalling your PWA:

Remember, the beforeinstallevent doesn't fire if the app is already installed, so during development you'll probably want to install and uninstall your app several times to make sure everything is working as expected.

Android On Android, PWAs are uninstalled in the same way other installed apps are uninstalled.

Open the app drawer. Scroll down to find the Weather icon. Drag the app icon to the top of the screen. Choose Uninstall. ChromeOS

On ChromeOS, PWAs are easily uninstalled from the launcher search box. Open the launcher. Type " Weather " into the search box, your Weather PWA should appear in the results. Right click (alt-click) on the Weather PWA. Click Remove from Chrome... macOS and Windows

On Mac and Windows, PWAs may be uninstalled through Chrome. In a new browser tab, open chrome://apps. Right click (alt-click) on the Weather PWA. Click Remove from Chrome... You can also open the installed PWA, click the the dot menu in the upper right corner, and choose Uninstall Weather PWA…

Link to the original codebase (Github Repository): Codelab: Your first PWA

More reading

(Section: Network falling back to cache) https://developers.google.com/web/fundamentals/instant-and-offline/offline-cookbook/#network-falling-back-to-cache

Further reading

⚠️ **GitHub.com Fallback** ⚠️