DEV Community

Antoine Muller
Antoine Muller

Posted on

Let's make a stopwatch pwa !

In this tutorial, we are going to transform a basic webapp into a Progressive Web App (PWA).
The webapp we are going to use is a stopwatch. I won't explain the stopwatch implementation in this tutorial.
The stopwatch doesn't use external libraries or framework in order to focus on the PWA aspect.
At the end of the tutorial, the stopwatch app will be installable on an android smartphone !

The source code of the project is available here: https://github.com/towaanu/stopwatch-pwa.
The final app is available here: stopwatch.towaanu.com.

For the rest of the article, I will refer to Progressive Web App as PWA.


Why PWA ?

Progressive Web Apps are webapps using a set of features in order to look like a native app. A PWA tries to reduce as much as it can the barrier between webapp and native app.

Here are some features used by PWA:

  • Reduce as much as possible loading/starting time of the app using caches.
  • A PWA can work even without network. It can be started in offline mode.
  • A PWA can be installed natively. For example, you can install a PWA on your mobile and open the app from your mobile homescreen, like you would for any other native app.

You can find feedbacks from projects using PWA on this site: https://www.pwastats.com/.
Of course, there is a little work to transform a webapp into a PWA !

Let's see, how we can transform our little stopwatch webapp into a PWA and install it on a mobile !


Stopwatch webapp

The Stopwatch webapp is a simple app. We can start, stop or reset a timer.
Here is an example of the app:

Stopwatch preview

You can find the final version of the stopwatch here: stopwatch.towaanu.com.
It does not use any external framework or library.

The project

There are 3 files in the starting project :

  • index.html: The html of the stopwatch
  • main.js: The main javascript file handling click events and stopwatch
  • style.css: The css of the app

You can find the starting project on this repo: https://github.com/towaanu/stopwatch-pwa/tree/12addb23ab334b82c81bfd91c6b437cf5f013fdb.

Since I will focus on the PWA part in this tutorial, I won't explain in details the implementation of the stopwatch.

Start the app

When working with a PWA, it's better to serve the app using a server rather than opening directly files from your machine.
I will introduce how to serve files using docker, nodejs, python however you can use other techniques to serve the project locally.
Usually, the app should be served over localhost.

Docker

If you have docker installed, you can use the nginx image to serve any files using a http server.
You need to be at the root of the project, then you can do:

docker run -p 8080:80 -v `pwd`:/usr/share/nginx/html:ro nginx
Enter fullscreen mode Exit fullscreen mode

Then the stopwatch webapp (index.html, main.js, style.css) should be accessible at http://localhost:8080.

Nodejs

If you have nodejs installed locally, you can use http-server to start the http server.
You need to be at the root of the project, then you can do:

npx http-server .
Enter fullscreen mode Exit fullscreen mode

Then the stopwatch webapp (index.html, main.js, style.css) should be accessible at http://localhost:8080.

Python

If you have python installed locally, you can use the following command at the root of the project :

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

Then the stopwatch webapp (index.html, main.js, style.css) should be accessible at http://localhost:8080.

Great, the app is accessible at http://localhost:8080 !
Now let's transform the stopwatch webapp into a PWA !


Webapp => PWA

For the rest of the tutorial we are going to use the chrome dev tool and more specifically the lighthouse tool.
Lighthouse can provide some feedbacks about what we need to turn a webapp into a PWA.

Where to start ?

Let's see what lighthouse tells us about the app:

First lighthouse result

Wow ! There is a lots of things to do. This is normal, we didn't have done anything to add PWA features to the app.
At first, we are going to focus on the Installable part.

Installable

Installable means the PWA can be installed on a device like any other native app.
For example you can install it on a smartphone and launch it like any other app !
Lighthouse tells us : Web app manifest or service worker do not meet the installability requirements.
What is a Web app manifest and a service worker ? Let's see it now !


Web app manifest

The web app manifest is a json file, commonly named manifest.json. This file contains data to help device display additional information when the PWA is installed.
You can define a lots of information such as name, short name, description, colors etc...
All properties are not mandatory for an app to be installable.
Let's make a manifest.json for the stopwatch app:

{
  "short_name": "Stopwatch",
  "name": "A stopwatch pwa",
  "icons": [
    {
      "src": "/images/icon-192.png",
      "type": "image/png",
      "sizes": "192x192",
      "purpose": "any maskable"
    },
    {
      "src": "/images/icon-512.png",
      "type": "image/png",
      "sizes": "512x512"
    }
  ],
  "start_url": "/",
  "display": "standalone",
  "scope": "/",
  "theme_color": "#ff5500",
  "background_color":"#ff5500"
}
Enter fullscreen mode Exit fullscreen mode

Let's analyze manifest.json fields:

  • name or short_name: The name of the app. This name is used by the device to display the name of the app on home screen for example.
  • icons: List of icons to be used when the app is installed. You can provide any numbers of icons with different sizes. However, you can only provide 2 sizes : 192x192 and 512x512 and devices should be able to scale icons if needed.
  • theme_color: The theme color of the app. It can be used to colorize the topbar (of a smartphone for example) or the browser UI when displaying the webapp.
  • background_color: The background_color can be used as a splashscreen when the app is loading on mobile.
  • start_url: The start url of the app. We need to specify the start_url, in order to know which url to load when you open an installed PWA app. ( most of the time it's / )
  • display: How the app should be display. Possible values are: fullscreen, standalone, minimal-ui, browser. standalone means that the app should be displayed like any other native apps.

There are more properties you can use in manifest.json. You can find more information about manifest's properties on mdn web doc.

Nice! We have our manifest.json file, but we still need to include it in our app.
We can add the manifest.json file by adding this line in index.html (inside head tag):

<head>
    <meta charset="utf-8">

    <title>Stopwatch</title>

    <link rel="apple-touch-icon" href="/images/icons-192.png">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta name="theme-color" content="#ff5500"/>

    <!-- Manifest is added here -->
    <link rel="manifest" href="/manifest.json">

    <script src="main.js"></script>
    <link href="/style.css" rel="stylesheet">

</head>
Enter fullscreen mode Exit fullscreen mode

Note: manifest.json is assumed to be at the root of the project in this example.
You can find the updated project with manifest here: https://github.com/towaanu/stopwatch-pwa/tree/2193c3fa88d451c8842001b362e06a55d9b4041d

Our web app manifest is now configured ! If you try to run a test with lighthouse again, you should see that the app is not yet installable.

In fact, we also need a service worker to make the app installable. Let's see what is a service worker !


Service Worker

As I said, a PWA needs to be usable offline. This way, it can act as a native app. In order to be used offline, a PWA needs to cache a lots of assets ( images, html, css, js ...). This is where the service worker comes into play !

Service workers enable us to control how assets should be cached. Basically a service worker is between the app and the internet. The service worker can intercept every network request from the webapp and decide whether or not it should return cache data or let the request go over the network. The service worker is also responsible to handle how elements are cached.

The service worker can:

  • Intercept every request from the webapp.
  • Decide whether or not a request should go over the network.
  • Return cache values when cache values are available.
  • Precache assets when the app starts.
  • Cache value return from network requests.

Here is a schema showing how service worker works when the webapp wants to fetch an images:

Schema service worker

Note: Service worker can intercept any requests, not only images.

Now that we've seen what the service worker can do, let's implement one !

Register the service worker

Before creating our service worker, we need to register it.
We are going to register it at the beginning of the main.js file:

// main.js

// Check if browsers support service worker
if ('serviceWorker' in navigator) {
  window.addEventListener('load', function() {
    // Register the service worker
    navigator.serviceWorker.register('/sw.js').then(function(registration) {
      // Registration was successful
      console.log('ServiceWorker registration successful ', registration);
    }, function(err) {
      // registration failed
      console.log('ServiceWorker registration failed: ', err);
    });
  });
}

// stopwatch code...
Enter fullscreen mode Exit fullscreen mode

And that's it for registering the service worker. As you can see, we are trying to load a sw.js file.
Let's create the sw.js file.

Create the service worker

The sw.js file is our service worker file.
Let's create the sw.js file at the root of the project:

var CACHE_NAME = 'cache-v1';
var urlsToCache = [
  '/',
  '/main.js',
  '/style.css'
];

// Event triggered the first time service worker is installed
self.addEventListener('install', function(event) {
    /*
     * Here we are caching urls specified above
     * This way when the app needs it files will be cached
     * Even if we close the app, and open later, files will still be in cache
     */
  event.waitUntil(
    caches.open(CACHE_NAME)
      .then(function(cache) {
        // Cache url defined in urlsToCache
        return cache.addAll(urlsToCache);
      })
  );
});

// Event triggered when the service worker is activated
self.addEventListener('activate', function(event) {
    // We don't need to do anything special here for this project
    console.log("Service worker activated");
});

// Event triggered whenever webapp needs to fetch a resource
self.addEventListener('fetch', function(event) {
  event.respondWith(
      // Check if the request is in the cache
    caches.match(event.request)
      .then(function(response) {
        /*
         * Found the request in cache
         * We can return the response in cache
         * We don't need to process the request
         */
        if (response) {
          return response;
        }

        /*
         * Request not found in cache
         * The request is processed and the result is returned
         */
        return fetch(event.request);
      }
    )
  );
});
Enter fullscreen mode Exit fullscreen mode

Note: To keep things simple, I decided not to cache requests after fetch.

As you can see we can listen to several events related to service worker.
Those events are called lifecycle events of a service worker. There are 3 events:

  • install: This event is fired only once, when the service worker is installed for the first time. We are using it to precache some assets from our stopwatch app
  • activate: This event is fired when the service worker is activated. It can be useful to use this event when you update your service worker and you want to clean up cache before activating a new service worker
  • fetch: This event is fired every time the app is trying to make a request. This is where we can decide whether to process or not the request. We can also return the cache if the request has already been cached

Now, when the app is launched for the first time it will cache /, /main.js and /style.css. Then whenever one of those paths is requested, the service worker will return the value in cache without making the request to the network.
Great ! We successfully created the service worker.

You can find the updated version with service worker of the project in this repo: https://github.com/towaanu/stopwatch-pwa.

Workbox

The service worker here is really basic. However, sometime you need a more complex service worker with special rules to cache specific files or request. There is library commonly used to deal with service worker.
This library is Workbox. With workbox you can easily configure strategies for your service worker :

  • Stale-While-Revalidate: Responds with the cache value if it exists, otherwise use the request result. Even if the cache value is returned, Stale While Revalidate strategies will fetch the request and update the cache for the next request.
  • Cache-First: Always return the cache value. If the request is not cached, requests will be processed and the cache will be updated.
  • Network-First: Always return the value returned by the request. If the request failed, fallback to the cache value.
  • Network-Only: Always return the value returned by the request. Cache is not used even if request failed.
  • Cache-Only: Always return the value from the cache. Network request is not used even if value is not in cache.

In a real world application you probably want to use a library like Workbox for service worker.
You can learn more about Workbox strategies on the Workbox documentation.

Install the app

Now that we have our manifest and service worker, let's run lighthouse again !

PWA Installable

Note: The only point left is the redirection HTTP => HTTPS. This is something to be configured at the server level ( by using nginx for example ).

Lighthouse tells us that our app meet the requirements to be installed. However if you try to access the app, you can not install it.
For security reasons, a PWA can only be installed if it is served from an HTTPS endpoint.
Since we are testing the app in local, it's complicated to serve the app over https.

You can try and install the app using this url: https://stopwatch.towaanu.com.
On the above url, the app is served using https, you should be able to install it!

PWA install on mobile

Nice ! We have successfully add PWA features to a webapp. The stopwatch app can now be installed and used like any other native app !


Conclusion

I hope this tutorial helps you understand how PWA works!
You can find a working version of the project here : https://stopwatch.towaanu.com/
The source code is available on this repo : https://github.com/towaanu/stopwatch-pwa

Most of popular frameworks such as react, vue, angular... provide tools to generate app with pwa features included. Usually, tools generate a service worker and a manifest.json that you can customize.
If you want to see a PWA app using React, I have an opensource pwa project here: memodogs.towaanu.com. (You can find the source on this repo : https://github.com/towaanu/memodogs).

I hope you enjoy this article :)

Discussion (0)