DEV Community

Cover image for Understanding Progressive Web Apps: Delivering a Mobile Experience - HTML5 and JavaScript Service Workers in 2021

Understanding Progressive Web Apps: Delivering a Mobile Experience - HTML5 and JavaScript Service Workers in 2021

What is a PWA?

A PWA (Progressive Web Application) is a type of app software delivered through the web. PWAs are built using common technologies such as HTML, CSS and JavaScript. They can be installed and function on any platform which uses a W3C compliant web browser on either desktop or mobile devices.
It is important to note that the browser will allow your web application to become a PWA only over a secure connection (using SSL encryption technology) or on localhost.

Who Can Install PWAs?

Support for PWAs are mostly focused around mobile browsers but there is limited support for some desktop browsers as well.

Support as of August 2021:

Mobile:

Nearly every mobile browser supports PWAs, with the exception of KaiOS

Desktop:

Chrome 39 & UP
Edge 79 & UP
IOS Safari/Chrome 11.3 & UP - Partial Support
Firefox - Deprecated as of January 2021

Progressive Web App Browser Support Table

Screenshot from caniuse.com

Why Should I Develop PWAs?

Purpose:

PWAs allow us to offer web applications that can be installed onto any device and act indistinguishably from a native app.
By meeting certain criteria, your website or web application can easily be turned into a PWA as well.

Required Technologies:

  • HTML
  • CSS
  • JavaScript
  • NodeJS (optional package that we will be using here)

Benefits:

  • Application Store Registration
  • Offline Fallbacks
  • Network or Cache First Resource Fetching
  • Push Notifications
  • Background Sync
  • And More

It doesn't take much to get started!

Our File Structure

    -root/
      -index.html
      -manifest.json
      -service-worker.js
      -logo.[png, jpg, etc...]
Enter fullscreen mode Exit fullscreen mode

index.html

Development can start with a basic HTML5 boilerplate.

HTML5 Boilerplate

    <html lang="en">
      <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>TechSnack Simple PWA</title>
      </head>
      <body>
        <h1>Hello World!</h1>
      </body>
    <html>
Enter fullscreen mode Exit fullscreen mode

manifest.json

In order for the user's device to know what to do with our web application, we are going to need to provide it some details. These details are fairly self explanatory.

We will leave the icons array empty for now. More on this shortly.

    {
      "name": "TechSnack Simple PWA",
      "short_name": "TechSnack",
      "start_url": "/?home=true",
      "icons": [],
      "theme_color": "#000000",
      "background_color": "#FFFFFF",
      "display": "fullscreen",
      "orientation": "portrait"
    }
Enter fullscreen mode Exit fullscreen mode

With this information the user's device can:

  • Install our application
  • Apply a custom icon for launching the app
  • Display a custom splash screen on launch
  • Allow customization of the application window and behaviour
  • Mimic native application behaviour
  • Allow access to native features such as GPS and push notifications
  • Register our application with popular app stores

Linking manifest.json

Use the link tag to connect manifest.json to our app

    <head>

      ...

      <link rel="manifest" href="manifest.json">
    </head>
Enter fullscreen mode Exit fullscreen mode

Node Packages (1 - optional)

As previously mentioned, the user's device will apply the custom icon for us. For it to do that we will need to supply at least one image for the device to reference.

What about multiple screen sizes or resolutions?

There are countless different mobile devices being used around the world today. To optimize the display of visual assets, each device prefers logos of a certain dimension.

pwa-asset-generator

You will need at least one image file of the following MIME types:

  • PNG
  • JPEG/JPG
  • SVG
  • WebP

Install

   $ npm install --global pwa-asset-generator

We will now want to run the package in our webroot directory.
The following snippet will do for our purposes.

npx pwa-asset-generator [path/to/logo] [path/to/output/dir] -i [path/to/index.html] -m [path/to/manifest.json] -f
The -f flag generates favicon image/meta tag

Execution - From webroot directory

   $ npx pwa-asset-generator logo.jpg logos -i index.html -m manifest.json -f

Command line output | pwa-asset-generator | npm

It is worth noting that if you execute pwa-asset-generator without using the -i, -m and -f flags the results will be output to your console instead.

Copy and paste the results into the icons array within manifest.json

Command line output | pwa-asset-generator | npm

Copy and paste the output into the head tag of index.html

Command line output | pwa-asset-generator | npm

New icons/ directory

Contains all of the generated images.

Directory Structure for PWA Icon Images

Updated index.html

Our index.html file should look like this now:

    <!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta name="theme-color" content="#000000">
    <link rel="apple-touch-icon" href="icons/apple-icon-180.png">

    <meta name="apple-mobile-web-app-capable" content="yes">
    <link rel="apple-touch-startup-image" href="icons/apple-splash-2048-2732.jpg" media="(device-width: 1024px) and (device-height: 1366px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)">
    <link rel="apple-touch-startup-image" href="icons/apple-splash-2732-2048.jpg" media="(device-width: 1024px) and (device-height: 1366px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)">
    <link rel="apple-touch-startup-image" href="icons/apple-splash-1668-2388.jpg" media="(device-width: 834px) and (device-height: 1194px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)">
    <link rel="apple-touch-startup-image" href="icons/apple-splash-2388-1668.jpg" media="(device-width: 834px) and (device-height: 1194px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)">
    <link rel="apple-touch-startup-image" href="icons/apple-splash-1536-2048.jpg" media="(device-width: 768px) and (device-height: 1024px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)">
    <link rel="apple-touch-startup-image" href="icons/apple-splash-2048-1536.jpg" media="(device-width: 768px) and (device-height: 1024px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)">
    <link rel="apple-touch-startup-image" href="icons/apple-splash-1668-2224.jpg" media="(device-width: 834px) and (device-height: 1112px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)">
    <link rel="apple-touch-startup-image" href="icons/apple-splash-2224-1668.jpg" media="(device-width: 834px) and (device-height: 1112px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)">
    <link rel="apple-touch-startup-image" href="icons/apple-splash-1620-2160.jpg" media="(device-width: 810px) and (device-height: 1080px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)">
    <link rel="apple-touch-startup-image" href="icons/apple-splash-2160-1620.jpg" media="(device-width: 810px) and (device-height: 1080px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)">
    <link rel="apple-touch-startup-image" href="icons/apple-splash-1284-2778.jpg" media="(device-width: 428px) and (device-height: 926px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)">
    <link rel="apple-touch-startup-image" href="icons/apple-splash-2778-1284.jpg" media="(device-width: 428px) and (device-height: 926px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)">
    <link rel="apple-touch-startup-image" href="icons/apple-splash-1170-2532.jpg" media="(device-width: 390px) and (device-height: 844px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)">
    <link rel="apple-touch-startup-image" href="icons/apple-splash-2532-1170.jpg" media="(device-width: 390px) and (device-height: 844px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)">
    <link rel="apple-touch-startup-image" href="icons/apple-splash-1125-2436.jpg" media="(device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)">
    <link rel="apple-touch-startup-image" href="icons/apple-splash-2436-1125.jpg" media="(device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)">
    <link rel="apple-touch-startup-image" href="icons/apple-splash-1242-2688.jpg" media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)">
    <link rel="apple-touch-startup-image" href="icons/apple-splash-2688-1242.jpg" media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)">
    <link rel="apple-touch-startup-image" href="icons/apple-splash-828-1792.jpg" media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)">
    <link rel="apple-touch-startup-image" href="icons/apple-splash-1792-828.jpg" media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)">
    <link rel="apple-touch-startup-image" href="icons/apple-splash-1242-2208.jpg" media="(device-width: 414px) and (device-height: 736px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)">
    <link rel="apple-touch-startup-image" href="icons/apple-splash-2208-1242.jpg" media="(device-width: 414px) and (device-height: 736px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)">
    <link rel="apple-touch-startup-image" href="icons/apple-splash-750-1334.jpg" media="(device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)">
    <link rel="apple-touch-startup-image" href="icons/apple-splash-1334-750.jpg" media="(device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)">
    <link rel="apple-touch-startup-image" href="icons/apple-splash-640-1136.jpg" media="(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)">
    <link rel="apple-touch-startup-image" href="icons/apple-splash-1136-640.jpg" media="(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)">

    <title>TechSnack | Simple PWA</title>
    <link id="favicon" rel="sortcut icon" href="favicon.ico" type="image/x-icon">
    <link rel="manifest" href="manifest.json">
  </head>
  <body>
    <h1>Hello World!</h1>
  </body>
</html>
Enter fullscreen mode Exit fullscreen mode

Final manifest.json

Our manifest.json file should look like this now:

    {
      "name": "TechSnack Simple PWA",
      "short_name": "TechSnack",
      "start_url": "/?home=true",
      "icons": [
        {
          "src": "icons/manifest-icon-192.png",
          "sizes": "192x192",
          "type": "image/png",
          "purpose": "maskable any"
        },
        {
          "src": "icons/manifest-icon-512.png",
          "sizes": "512x512",
          "type": "image/png",
          "purpose": "maskable any"
        }
      ],
      "theme_color": "#000000",
      "background_color": "#FFFFFF",
      "display": "fullscreen",
      "orientation": "portrait"
    }
Enter fullscreen mode Exit fullscreen mode

Service Worker

Before our PWA can do all of the fancy things previously mentioned we must first create a service worker.

A service worker is a listener script the browser runs in the background. The service worker runs separately from the webpage allowing for the implementation of features that do not require interaction or calls from the webpage or user.
Service Workers may support features such as periodic sync or geofencing in the future.
NOTE: Although service workers are a JavaScript file, there are additional limitations imposed when coding. You may not have access to the DOM through a service worker.

Lifecycle of a Service Worker

There are multiple functions/features that you would normally have to build out around A Service Workers Lifecycle.
We will be using an API called workbox to avoid worrying about configuring what is under the hood.

workbox API

For our service worker, we are going to employ the use of an API called workbox. This API offers baked-in features that would require multiple articles to describe on its own.
If you are interested in digging into the nitty gritty you can read about A Service Workers Lifecycle.

ImportScript

Inside of service-worker.js we will import the workbox API

   importScripts('https://storage.googleapis.com/workbox-cdn/releases/6.0.2/workbox-sw.js');

Registering Routes

A service worker can intercept network requests from a page. It may respond to the page with cached content or generated content

Service Worker Routing| JavaScript

Screenshot from Google Dev

Note: (From Above)

The method is GET by default.
To change this it must be specified.

The order of route registration is important when multiple workers are available to handle a request.
Whichever worker has been created first will take priority in handling a given request.

service-worker.js

We can now add the following code within service-worker.js:


    ...

    workbox.routing.registerRoute(
      ({request}) => request.destination === 'image',
      new workbox.strategies.CacheFirst() //to search cache first
      //new workbox.strategies.NetworkFirst() //to search server first
);
Enter fullscreen mode Exit fullscreen mode

That's it! The above code will:

  • RegisterRoute with workbox
  • Intercept all 'image' files at the page's request

Here we choose our strategies. Would we like to serve to our page from CacheFirst or NetworkFirst? This we will decide by whether the specific resource(s) we are interested in is static or dynamic.

  • If they are generally static to the page our user lands on then we will want to serve them from cache.

  • However, if they are dynamically generated by some sort of back-end then we would want to largely get that file from the network.

Final service-worker.js


    importScripts('https://storage.googleapis.com/workbox-cdn/releases/6.0.2/workbox-sw.js');

    workbox.routing.registerRoute(
      ({request}) => request.destination === 'image',
      new workbox.strategies.CacheFirst() //to search cache first
      //new workbox.strategies.NetworkFirst() //to search server first
);
Enter fullscreen mode Exit fullscreen mode

Linking service-worker.js

Now that we have our service worker in place and intercepting requests for image files, we can link our script within index.html

    <body>

      ...

      <script>
        if('serviceWorker' in navigator){
        navigator.serviceWorker.register('/service-worker.js');
      }
      </script>
    </body>
Enter fullscreen mode Exit fullscreen mode

Its that simple to register our service worker!

Putting it all together

We can finally take a look at the final code base for our PWA project.

Our File Structure:

Progressive Web Application | File Structure

index.html

    <!DOCTYPE html>
    <html lang="en">
      <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <meta name="theme-color" content="#000000">
        <link rel="apple-touch-icon" href="icons/apple-icon-180.png">

        <meta name="apple-mobile-web-app-capable" content="yes">
        <link rel="apple-touch-startup-image" href="icons/apple-splash-2048-2732.jpg" media="(device-width: 1024px) and (device-height: 1366px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)">
        <link rel="apple-touch-startup-image" href="icons/apple-splash-2732-2048.jpg" media="(device-width: 1024px) and (device-height: 1366px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)">
        <link rel="apple-touch-startup-image" href="icons/apple-splash-1668-2388.jpg" media="(device-width: 834px) and (device-height: 1194px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)">
        <link rel="apple-touch-startup-image" href="icons/apple-splash-2388-1668.jpg" media="(device-width: 834px) and (device-height: 1194px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)">
        <link rel="apple-touch-startup-image" href="icons/apple-splash-1536-2048.jpg" media="(device-width: 768px) and (device-height: 1024px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)">
        <link rel="apple-touch-startup-image" href="icons/apple-splash-2048-1536.jpg" media="(device-width: 768px) and (device-height: 1024px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)">
        <link rel="apple-touch-startup-image" href="icons/apple-splash-1668-2224.jpg" media="(device-width: 834px) and (device-height: 1112px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)">
        <link rel="apple-touch-startup-image" href="icons/apple-splash-2224-1668.jpg" media="(device-width: 834px) and (device-height: 1112px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)">
        <link rel="apple-touch-startup-image" href="icons/apple-splash-1620-2160.jpg" media="(device-width: 810px) and (device-height: 1080px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)">
        <link rel="apple-touch-startup-image" href="icons/apple-splash-2160-1620.jpg" media="(device-width: 810px) and (device-height: 1080px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)">
        <link rel="apple-touch-startup-image" href="icons/apple-splash-1284-2778.jpg" media="(device-width: 428px) and (device-height: 926px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)">
        <link rel="apple-touch-startup-image" href="icons/apple-splash-2778-1284.jpg" media="(device-width: 428px) and (device-height: 926px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)">
        <link rel="apple-touch-startup-image" href="icons/apple-splash-1170-2532.jpg" media="(device-width: 390px) and (device-height: 844px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)">
        <link rel="apple-touch-startup-image" href="icons/apple-splash-2532-1170.jpg" media="(device-width: 390px) and (device-height: 844px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)">
        <link rel="apple-touch-startup-image" href="icons/apple-splash-1125-2436.jpg" media="(device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)">
        <link rel="apple-touch-startup-image" href="icons/apple-splash-2436-1125.jpg" media="(device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)">
        <link rel="apple-touch-startup-image" href="icons/apple-splash-1242-2688.jpg" media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)">
        <link rel="apple-touch-startup-image" href="icons/apple-splash-2688-1242.jpg" media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)">
        <link rel="apple-touch-startup-image" href="icons/apple-splash-828-1792.jpg" media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)">
        <link rel="apple-touch-startup-image" href="icons/apple-splash-1792-828.jpg" media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)">
        <link rel="apple-touch-startup-image" href="icons/apple-splash-1242-2208.jpg" media="(device-width: 414px) and (device-height: 736px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)">
        <link rel="apple-touch-startup-image" href="icons/apple-splash-2208-1242.jpg" media="(device-width: 414px) and (device-height: 736px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)">
        <link rel="apple-touch-startup-image" href="icons/apple-splash-750-1334.jpg" media="(device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)">
        <link rel="apple-touch-startup-image" href="icons/apple-splash-1334-750.jpg" media="(device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)">
        <link rel="apple-touch-startup-image" href="icons/apple-splash-640-1136.jpg" media="(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)">
        <link rel="apple-touch-startup-image" href="icons/apple-splash-1136-640.jpg" media="(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)">

        <title>TechSnack | Simple PWA</title>
        <link id="favicon" rel="sortcut icon" href="favicon.ico" type="image/x-icon">
        <link rel="manifest" href="manifest.json">
      </head>
      <body>
        <h1>Hello World!</h1>

        <script>
          if('serviceWorker' in navigator){
            navigator.serviceWorker.register('/service-worker.js');
          }
        </script>
      </body>
    </html>
Enter fullscreen mode Exit fullscreen mode

manifest.json

    {
      "name": "TechSnack Simple PWA",
      "short_name": "TechSnack",
      "start_url": "/?home=true",
      "icons": [
        {
          "src": "icons/manifest-icon-192.png",
          "sizes": "192x192",
          "type": "image/png",
          "purpose": "maskable any"
        },
        {
          "src": "icons/manifest-icon-512.png",
          "sizes": "512x512",
          "type": "image/png",
          "purpose": "maskable any"
        }
      ],
      "theme_color": "#000000",
      "background_color": "#FFFFFF",
      "display": "fullscreen",
      "orientation": "portrait"
    }
Enter fullscreen mode Exit fullscreen mode

service-worker.js

    importScripts('https://storage.googleapis.com/workbox-cdn/releases/6.0.2/workbox-sw.js');

    workbox.routing.registerRoute(
      ({request}) => request.destination === 'image',
      new workbox.strategies.CacheFirst() //to search cache first
    );
Enter fullscreen mode Exit fullscreen mode

Serving Our Page

We can now view our page in the browser.

For localhost run npx serve in the webroot directory.
Then you can visit your securely served remote ip or domain
Please keep in mind the browser support for your device

Progressive Web App In Browser Window

There is a new icon in our address bar!
Progressive Web App | Install User Action Icon

The browser requires a user action to install the PWA
The User can click this icon to see the prompt
Progressive Web App | Install User Action Prompt

Now you can click on the icon installed on the device's homepage
PWA Application Running in Program Browser Window

Summarization of PWA Development

As you can see it is super easy to create a web application that can mimic the same functions as a native app. In future articles we will delve into each feature that we will now have access to with this powerful piece of technology.

Help TechSnack Write Concise Content:

Leave us a comment with your thoughts on the article below. Whether you liked or disliked the article, all feedback will help me to know how to better create content that meets your needs, goals and aspirations.

Sharing the article on your social platforms would also be a great help!

Follow TechSnack on Twitter

Join the conversation at r/TechSnack

Discussion (2)

Collapse
code913 profile image
code913

Couldn't understand anything. There was much less explanation and more copy paste this to your file.

Collapse
techsnack profile image
TechSnack - Technology Tutorials Author

Thank you. I will be adding more explainatory content.