DEV Community

Sébastien NOBILI
Sébastien NOBILI

Posted on • Originally published at techreads.pipoprods.org

Making a Simple PWA

This article is the first of a series in which I'll cover Progressive Web Apps
from scratch.

Nowadays, many tutorials show you how to make a PWA the simple way. IMO it's a bad thing that leads to most PWAs being nothing more than Web bookmarks and making users think this technology is useless.

In this article, I'll show how to make a PWA that:

  • can be installed on your phone/computer as if it was a native application
  • can be used both online and offline

Create a basic Web app

The app will be made of two elements: an HTML page and a script.

index.html file:

<!DOCTYPE html>
<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
        <meta charset="utf-8" />
        <meta name="viewport" content="width=device-width" />
        <meta name="mobile-web-app-capable" content="yes" />
        <meta http-equiv="X-UA-Compatible" content="IE=edge" />
        <meta name="theme-color" content="#8d2382" />

        <title>Simple PWA</title>

        <style type="text/css">
            html,
            body {
                height: 100%;
                width: 80%;
                margin: 0;
                display: flex;
                flex-direction: column;
                margin-left: auto;
                margin-right: auto;
                font-family: sans-serif;
            }

            h1,
            div.controls {
                text-align: center;
            }

            #api_id {
                font-family: monospace;
                text-align: center;
                display: inline-block;
                width: 5em;
            }

            pre {
                display: flex;
                margin-left: auto;
                margin-right: auto;
            }
        </style>

        <script type="text/javascript" src="app.js"></script>
    </head>
    <body>
        <h1>Simple PWA</h1>

        <div class="controls">
            <button onClick="previous()">&lt;</button>
            <span id="api_id"></span>
            <button onClick="next()">&gt;</button>
        </div>

        <pre id="result"></pre>
    </body>
</html>
Enter fullscreen mode Exit fullscreen mode

app.js file:

document.addEventListener('DOMContentLoaded', () => {
    fetch_data(1);
});

async function previous() {
    let id = parseInt(document.getElementById('api_id').innerHTML, 10);
    id--;
    if (id <= 0) {
        id = 0;
    }
    fetch_data(id);
}

async function next() {
    let id = parseInt(document.getElementById('api_id').innerHTML, 10);
    id++;
    fetch_data(id);
}

async function fetch_data(id) {
    document.getElementById('api_id').innerHTML = id;
    fetch(`https://swapi.dev/api/people/${id}`).then((response) => {
        if (response.ok) {
            response.json().then((data) => {
                document.getElementById('result').innerHTML = JSON.stringify(
                    data,
                    null,
                    2,
                );
                document.getElementById('api_id').focus();
            });
        } else {
            document.getElementById('result').innerHTML = "You're currently offline";
            document.getElementById('api_id').focus();
        }
    });
}
Enter fullscreen mode Exit fullscreen mode

You can now serve your application through a Web server, for example using Python:

python3 -m http.server --directory . 8080
Enter fullscreen mode Exit fullscreen mode

Loading http://localhost:8080 in your Web browser should show you this:

The application UI

This app will fetch characters from https://swapi.dev/ and display raw data in a pre block.

Make it installable

Important note: to be installable on a phone, a PWA must be served through HTTPS. Unless you have a properly configured Web server to deploy the app, you'll have to test your PWA on a computer Web browser (Chromium is advised as it provides a convenient "offline mode" toggle).

The first element to make your app installable is a manifest.

manifest.webmanifest file:

{
    "name": "Simple PWA",
    "short_name": "Simple PWA",
    "description": "A simple PWA, for testing",
    "icons": [
        {
            "src": "icons/icon-32.png",
            "sizes": "32x32",
            "type": "image/png"
        },
        {
            "src": "icons/icon-512.png",
            "sizes": "512x512",
            "type": "image/png",
            "purpose": "any maskable"
        }
    ],
    "start_url": "/index.html",
    "display": "fullscreen",
    "theme_color": "#B12A34",
    "background_color": "#B12A34"
}
Enter fullscreen mode Exit fullscreen mode

The manifest must be registered in HTML file head:

    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
        <meta charset="utf-8" />
        <meta name="viewport" content="width=device-width" />
        <meta name="mobile-web-app-capable" content="yes" />
        <meta http-equiv="X-UA-Compatible" content="IE=edge" />
        <meta name="theme-color" content="#8d2382" />
+       <link rel="manifest" href="manifest.webmanifest" />

        <title>Simple PWA</title>
Enter fullscreen mode Exit fullscreen mode

You'll also need to place two icons in:

  • icons/icon-32.png
  • icons/icon-512.png

You should see an installation icon on your Web browser window:

The application is now installable

Make it work offline

Offline mode in PWAs rely on browser cache. You must store into cache all the data that's needed by your application. In our case, the index.html and app.js files are what we need to be cached.

This is done through a JavaScript code that will run in the background: the service worker.

sw.js file:

const appCacheName = 'simple-pwa';
const appContentToCache = [
    '/',
    'index.html',
    'app.js',
    'favicon.ico',
    'manifest.webmanifest',
    'icons/icon-512.png',
];

/**
 * First of all, the service worker will react to an 'install' event (triggered automatically)
 * We'll put the application content into cache.
 */
self.addEventListener('install', (e) => {
    e.waitUntil(async () => {
        await caches.delete(appCacheName);
        const cache = await caches.open(appCacheName);
        await cache.addAll(appContentToCache);
    });
});

/**
 * Then, the service worker will be involved every time an HTTP request is made
 */
self.addEventListener('fetch', (e) => {
    e.respondWith(fetch_resource(e.request));
});

/**
 * fetch a resource:
 *   - if resource is in app cache, return in
 *   - if resource can be obtained from remote server, fetch it
 *   - otherwise return HTTP-408 response
 */
async function fetch_resource(resource) {
    response = await get_from_cache(resource, appCacheName);
    if (response) {
        return response;
    } else {
        try {
            response = await fetch(resource);
            return response;
        } catch (error) {
            return new Response('offline', {
                status: 408,
                headers: { 'Content-Type': 'text/plain' },
            });
        }
    }
}

/**
 * query cache for resource
 */
async function get_from_cache(resource, cacheName = appCacheName) {
    try {
        const cache = await caches.open(cacheName);
        const response = await cache.match(resource);
        return response;
    } catch (error) {
        return;
    }
}
Enter fullscreen mode Exit fullscreen mode

Don't forget to update app.js file to register the service worker:

document.addEventListener('DOMContentLoaded', () => {
  fetch_data(1);
+  if ('serviceWorker' in navigator) {
+    navigator.serviceWorker.register('sw.js');
+  }
});
Enter fullscreen mode Exit fullscreen mode

Your application should be able to reload even when you're offline now:

The application loads while offline

Of course, the remote resources (from https://swapi.dev/) are not accessible.

Make remote resources available while offline

We'll update our service worker to keep remote resources in a cache so that we can access them while offline.

We add a new cache:

const appCacheName = 'simple-pwa';
const appContentToCache = ['/', 'index.html', 'app.js', 'favicon.ico', 'manifest.webmanifest', 'icons/icon-512.png'];
+const dataCacheName = `${appCacheName}-data`;
Enter fullscreen mode Exit fullscreen mode

The we update the fetch_resource logic that way:

  • if resource is not in app cache, try to get it from remote
  • if remote resource is not reachable, try to get it from data cache
  • if not reachable, nor in cache, return an HTTP-408 code

This way we'll give the user the up-to-date data from remote server whenever it's possible and give him cached version when offline.

/**
 * fetch a resource:
 *   - if resource is in app cache, return in
 *   - if resource can be obtained from remote server, fetch it
+ *   - if resource is in data cache, return it
 *   - otherwise return HTTP-408 response
 */
async function fetch_resource(resource) {
  response = await get_from_cache(resource, appCacheName);
  if (response) {
    return response;
  } else {
    try {
      response = await fetch(resource);
+      await put_into_cache(resource, response);
      return response;
    } catch (error) {
+      response = await get_from_cache(resource);
+      if (response) {
+        // resource was found in data cache
+        return response;
+      } else {
        return new Response('offline', {
          status: 408,
          headers: { 'Content-Type': 'text/plain' },
        });
+      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

A new function is needed to put data in cache:

/**
 * put resource into cache
 */
async function put_into_cache(request, response, cacheName = dataCacheName) {
    const cache = await caches.open(cacheName);
    await cache.put(request, response.clone());
}
Enter fullscreen mode Exit fullscreen mode

With this updated application code, you'll be able to fetche data from remote API, go offline and still be able to display this data.

Your PWA now has a decent offline mode 🙂

Offline mode demo

Final thoughts

This post shows that a PWA can be a good alternative to native or hybrid mobile apps in some cases. In a future post, we'll get deeper into it and see how:

  • the frontend and the service worker can communicate and provide feedback to the user,
  • the service worker can trigger frontend refresh to get application updates.

This post was intended to show how to make a PWA from scratch. Proper code architecture was not a consideration. The code presented here should be refactored to make it stronger.

The source code is here:
https://code.pipoprods.org/web/simple-pwa

Top comments (0)