loading...
Cover image for How to Make a Basic Progressive Web App

How to Make a Basic Progressive Web App

ayushmanbthakur profile image Ayushman Bilas Thakur Originally published at ayushmanbthakur.com on ・7 min read

So, in a previous post I made a bookmarking application, which used the localStorage of the browser to store bookmark app. But what happens if there is no internet connection? So, I hosted my app on GitHub and in the developer tools(ctrl + shift + i) in the network tab set the network to offline like this:

Which, makes the app go offline.

But does this app really need to be online to show all the list of your bookmarks? Absolutely not. So let's make this app to work offline with the progressive web app feature.

Technically, only having a service worker can make our website cache data and be available offline. But making our website a PWA has its perks like providing install features on android devices.

Things required to make a PWA

So, to turn an app into a PWA we need to tick a few checkmarks. There is a utility called lighthouse in chrome dev tools. There is a test for PWA there. The setup looks like this:

Lighhouse Test Setup

After running the test in the Installable section, you will find the required things to turn your app into a PWA. So, according to that result, the things we require are.

  1. A Service Worker: Service Worker is a javascript file which works on a different thread rather than the main javascript of the webpage. This service worker has the power to intercept the requests going off the webpage to the server and responses coming from the server.

As service workers are such powerful, so having https:// is a must for sites with service worker to make that work. The only http:// URL allowed to use service worker is localhost. This is for testing purpose.

  1. manifest.json: If you have ever worked on making a native app then you know that those apps require some unique info like app_name, app_icon, theme_color etc. for the app. Manifest will be hosting all these information needed for our app.

Creating and initializing the service worker

So, to start with I will create an sw.js denoting a service worker. Now we need to register the service worker. It takes only a few lines of code. In the main script or in the HTML script tag we can have the service worker registered.

To start with we need to check if the serviceWorker service is available in the browser, and if available, then we need to register the service worker to the navigator. This registration function takes in the path to the service worker and returns a promise. With that promise we can, on success console log the success and also can throw an error.

This code looks like this:

if ('serviceWorker' in navigator) {
    navigator.serviceWorker.register("sw.js")
        .then(sw => console.log("Service work registration successful"))
        .catch(err => console.log("Error"))
} else {
    console.log("Service Worker not supported!")
}

So, if everything goes right and we now have a look at our console, then we will see the message of "Service work registration successful.

Now, if we head to the Application and have a look at the Service Workers, we will be able to see the service worker:

So, we have successfully set up our service worker. Now the next step will be to cache our required files and serve them when required and provide less load time.

Caching the files

To create cache storage specific to this app, we need some code in sw.js. In this file, there will be the logic, which will make our app to be able to render when offline.

So to start with, we need a name for the cache. In this case, I have declared the name to be: bookmark-app-cache. This naming is important is to locate your files and also with some coding, you can make the app to give an update button to tell the cache to automatically update.

Next, we need to declare the files we want to cache. This is the simplest way to cache, just by providing the names of the files & routes to cache and caching them. There is also another approach to caching where you can dynamically cache your data. In this case, we need to cache the "/" route, i.e. the main route. The actual files we need to cache in this case are available at "/index.html", "/style.css","/script.js","/icon.png" and "iconx512.png" respectively.

Combining all the things discussed above the code will look like:

const CACHE_NAME = "bookmark-app-cache-v2";

const assets = [
  "/",
  "/index.html",
  "/style.css",
  "/script.js",
  "/icon.png",
  "iconx512.png",
];

In browser-based javascript, you can add event listeners to DOM elements. Like that, in the service worker file(here sw.js) we can add some event listeners to the "self" available in sw.js. Some events this event listener can listen to are -

  1. Install: This event is fired as soon as the service worker thread is installed i.e. as soon as the code navigator.serviceworker.register() is executed in our browser facing JavaScript file. We will be using the Install event to cache all the required files with the help of the array we already declared.

  2. Activate: Just after installation if there is already a service worker working for that site, the new service worker doesn't do a hot-swap with the old service worker. It waits for a page reload to do this task. When the service worker is active after the reload, then this event listener triggers. And if there is no previous service worker then the activate event is triggered as soon as install is done. In the case of my recent game OddNEven I used this event to update my cache with game updates and remove the old unnecessary cache. We are not using this process here in this app.

  3. Fetch: This is another important event of the service worker. This event is triggered if some request fires from frontend to the server. This event has the power to intercept the request and returning custom value without the request even reaching the server.

There are also events like push, sync and message which I have no deep knowledge about.

As I told before we need to cache our files just after installation. To do that, we need to make the install event to wait for a function to run. This function will open our cache with the name we provided and will use our array of assets to cache the required assets and route responses. So that part will look like:

//the cache name and asset declaration

self.addEventListener("install", (e) => {
  e.waitUntil(
    caches.open(CACHE_NAME).then((cache) => {
      return cache.addAll(assets);
    })
  );
});

Now the main part of making our app PWA comes in. We need to intercept the fetch event and make it respond with either the cache(if available) or continue with the request. The code will simply look like this:

self.addEventListener("fetch", (e) => {
  e.respondWith(
    caches.match(e.request).then((res) => {
      return res || fetch(e.request);
    })
  );
});

Now If you go to the Network tab and make the app offline, you will not see the dinosaur, instead, you will have the page served. It might be just enough to just make your app work offline, but we can go one step better by creating a manifest.json for our app and making it installable.

Creating manifest.json

The few properties we need in the manifest.json are:

  1. Name: This will be the name of the app
  2. Short Name: This is the name shown when the app is installed.
  3. icons: This is a list of icons to be used when the app is installed. Icons of 192 x 192 and 512 x 512 are required.
  4. start_url: This is the url to start the app on.
  5. background_color
  6. theme_color
  7. display: This sets the display of the app when installed on a mobile device.

Want to know more about manifest.json, read here

So, for this app the manifest.json I wrote looks like this:

{
    "short_name": "Bookmarks",
    "name": "Bookmark App",
    "icons": [{
            "src": "icon.png",
            "sizes": "192x192",
            "type": "image/png"
        },
        {
            "src": "iconx512.png",
            "sizes": "512x512",
            "type": "image/png"
        }
    ],
    "start_url": "/",
    "background_color": "#000000",
    "theme_color": "#ffa500",
    "display": "standalone"
}

So, now it was the time to link this manifest.json to our app and set up a few properties, and run the app for one last time. The HTML tags added were:

<link rel="manifest" href="manifest.json">
<meta name="theme-color" content="#ffa500">
<meta name="viewport" content="width=device-width, initial-scale=1.0" />

These are three tags required to turn your app into a PWA. The first one connects the manifest.json to our page. The second one sets the theme color for our app. The last one sets the viewport size for the app, making it responsive for any device.

Conclusion

The app is finally converted into a PWA. You can see the final code here. If you want to try or install the app visit this link. GitHub was not letting me have the PWA activated,(due to the base route not being "/" ) so I chose glitch to host my app. But there are many topics about PWA which I could not cover in this post. Some of these topics are:

  1. Caching requests to API
  2. Rendering different views based on if the app is offline or online
  3. Manually prompting the users to install the app. In my hosted version of the app I have a custom install button on the navbar, it has some CSS and HTML involved but the javascript is not too difficult, it looks something like this:
let haltedPrompt;
const installButton = document.getElementById("install_button");

window.addEventListener("beforeinstallprompt", (e) => {
  e.preventDefault();
  haltedPrompt = e;
  installButton.style.display = "block";
});


installButton.addEventListener("click", () => {
  if (!haltedPrompt) return

  haltedPrompt.prompt();
  haltedPrompt.userChoice.then((result) => {
    console.log("userChoice", result);
    haltedPrompt = null;
    installButton.style.display = "none";
  });
});

At first, I declared a global variable to store the event of install prompt fired automatically. Then I selected the button to make appear once the app is available for installation(by default the display property of the install button is set to none). So, there are two steps in this process:

  1. Storing the automatic install prompt from the browser. For that, I added an event listener to the window object to listen to the beforeinstallprompt event and once that event is fired I stopped the default event, stored the event in the haltedPrompt and made the button visible.

  2. Triggering the stored event on click. For that, I added an event listener to the button. Then I perform a check if the stored event is valid and if it is valid I prompt the user and if the choice is accepted then I clear the variable and hide the install button.

Once again the code for this project is available at GitHub and the project is hosted at glitch

Posted on May 8 by:

ayushmanbthakur profile

Ayushman Bilas Thakur

@ayushmanbthakur

Web development is my ❤. I love writing blog posts and experimenting with new things!

Discussion

markdown guide