DEV Community

Cover image for Make websites work offline - What are Service Workers and How to get a custom App Install button on the website.
Saurabh Daware 🌻
Saurabh Daware 🌻

Posted on • Updated on

Make websites work offline - What are Service Workers and How to get a custom App Install button on the website.

Hey everyone,

This is a little different from my usual posts (finally a post without #showdev tag πŸ˜‚). In this post, I will be explaining what are Service Workers, How to implement them with Vanilla JavaScript, and finally How to get the custom "Add to Homescreen" banner on the website.

Table of Content

Why do we need Service Worker?

Do you use Instagram (The Native App)? on the Home Screen, it shows pictures right? what if you turn off the internet? Does it crashes or show dinosaur game like chrome? well nope...

Instead, Instagram shows older posts that were already loaded. Though you cannot refresh the feed or like a picture it is still pretty cool to be able to see the older posts right?

Service Workers let you implement something similar on the web. You can avoid the chrome's dinosaur and show a custom offline page instead! or you can show a part of your website (or a whole website considering it is small) while the user is browsing offline.

Here's what twitter shows when you go offline:
Image of Twitter's offline page

In fact, DEV.to has one of the coolest offline pages ever! They have a coloring canvas as an offline page! super cool right? Here's how it looks:
Image of DEV.to's offline page

And here's a humblebrag:
Last year I made a game called Edge of The Matrix that works offline! since the game is not that large I was able to cache most of the parts of the website and make the entire game work offline.
Offline and Online comparison of eotm.ml

As you can see the Left image is when you are online and right when you are offline. When you are offline the game looks the same (minus the font).

How do they work? hah.. saurabh.. obviously magic.

Well, They use Service WorkersπŸŽ‰ With the three examples above I wanted to give an idea of how websites can use Service Workers in various ways.

So are you all excited to learn how they work and how to implement them!!? Lezgooo!!!

How Service Worker works?

Note: This is a very surface-level explanation of how service worker works, After reading the article if you are interested in knowing more about them, I have linked some references at the end of the article

Service Worker has access to cache, requests coming out from your applications, and the Internet.

Since you have access to these three things, you can write code and handle the requests the way you want.

Diagram that explains how service worker takes the request from the application and can redirect the request to cache or internet depending on the requirement

Service Worker can listen to the requests from the user,

Here's how we usually prefer our requests to load with the service worker registered:

  1. User hits your website's URL (and thus requests for your /index.html)
  2. Service Workers have a fetch event listener that listens to this event.
  3. Now since the service worker has access to caches object which controls the cache. It can check if the /index.html exists in the cache.
  4. If index.html exists in cache: respond with the index.html file from the cache. Else: pass on the request to the internet and return the response from the internet.

Service Worker Lifecycle.

When you first register the service worker, It goes to install state and after installing the service worker it goes to active.

Now, let's say you updated the service worker, In this case, the new service worker goes to install and then waiting state while the old service worker still is in control and is active.

After closing the tab and opening the fresh instance of the website, the service worker from the waiting state takes control and goes active.

Google Developers site has a nice detailed explanation of Service Worker Lifecycle, I will suggest checking it out: https://developers.google.com/web/fundamentals/primers/service-workers/lifecycle

How to implement Service Worker

Before we go into code here are a few things that you should know:

  1. First, you need to tell your website where your service worker file is. (i.e. Register Service Worker).
  2. Service Worker file does not get access to DOM. If you've worked with Web Workers before, the Service Worker is also a kind of a JavaScript Worker.
  3. You can postMessage back and forth from the Service Worker file which allows you to talk to the Service Worker.

1. Service Worker Registration.

In your index.html (or any of the .js file which is sourced to .html)

<html>
<body>
<!-- Your HTML -->

<script>
// ServiceWorker Registration
if('serviceWorker' in navigator) {
    window.addEventListener('load', function() {
        navigator.serviceWorker.register('serviceworker.js')
            .then(registration => {
                // Registration was successful
                console.log('ServiceWorker registration successful with scope: ', registration.scope);
            })
            .catch(err => {
                // registration failed :(
                console.log('ServiceWorker registration failed: ', err);
            });
    });
}
</script>
</body>
</html>

This will register the file /serviceworker.js as a service worker. Now all your service worker handling code will go into the /serviceworker.js file.

2. Handling Requests with Service Worker

2a. Storing URLs to Cache.

YayπŸŽ‰, we have Service Worker registered! Now we want to add our necessary files into the cache so that we can later load them without the internet connection.

In serviceworker.js,

const CACHE_NAME = "version-1";
const urlsToCache = [
    'index.html',
    'assets/logo-192.png',
    'assets/coverblur.jpg',
    'index.js'
];

// Install the service worker and open the cache and add files mentioned in array to cache
self.addEventListener('install', function(event) {
    event.waitUntil(
        caches.open(CACHE_NAME)
            .then(function(cache) {
                console.log('Opened cache');
                return cache.addAll(urlsToCache);
            })
    );
});

caches.open(CACHE_NAME) open the caches that match the name passed to it ("version-1" in our case).
cache.addAll(urlToCache) adds all the URLs to the cache.

Now we have all the files that we need to load offline in our cache.

2b. Loading file from the Cache.

In serviceworker.js,

// Listens to request from application.
self.addEventListener('fetch', function(event) {
    event.respondWith(
        caches.match(event.request)
            .then(function(response) {
                if (response) {
                    // The requested file exists in the cache so we return it from the cache.
                    return response;
                }

                // The requested file is not present in cache so we send it forward to the internet
                return fetch(event.request);
            }
        )
    );
});

caches.match(event.request) checks if the match for event.request is found in the CacheStorage and if it does it responds it in promise.

If the file is not present in the cache, it returns a falsy value and thus you can send the request further to fetch data through the internet.

(If you want to load an offline page, you can check if the fetch is throwing error. If the fetch call fails, in the .catch() block you can respond with offline.html. I have mentioned an example to load offline page below.)

2c. Handling new Cache versions (OPTIONAL - You can avoid this if you hate your life).

So Woah! You've come a long way. Maybe drink water here.

"But wait, Saurabh, I already know how to add to cache and respond from the cache and my code is perfectly working so isn't this article suppose to end here?"

Well yes but actually no.

Here's a problem,
Now you make changes in your code or let's say you added a new JavaScript file. You want those changes to reflect in your app but.. your app still showing older file.. why? because that is what the service worker has in the cache. Now you want to delete the old cache and add your new cache instead.

Now, what we need to do is, we need to tell the service worker to delete all the caches except the new one that has just been added.

We've given caches a key/name right? "version-1". Now if we want to load new cache we will change this name to "version-2" and we would want to delete the cache of name "version-1"

This is how you would do that.

In serviceworker.js,


self.addEventListener('activate', function(event) {
    var cacheWhitelist = []; // add cache names which you do not want to delete
    cacheWhitelist.push(CACHE_NAME);
    event.waitUntil(
        caches.keys().then(function(cacheNames) {
            return Promise.all(
                cacheNames.map(function(cacheName) {
                    if (!cacheWhitelist.includes(cacheName)) {
                        return caches.delete(cacheName);
                    }
                })
            );
        })
    );
});

So since we updated the CACHE_NAME to "version-2". We will delete every cache that is not under "version-2".

When we have a new Service Worker activated we delete the unnecessary old cache.

3. Complete Example for Implementing Service Worker

3a. Code for loading index.html and index.js offline

index.html

<html>
<body>
<!-- Your HTML -->
<script>
// ServiceWorker Registration
if('serviceWorker' in navigator) {
    window.addEventListener('load', function() {
        navigator.serviceWorker.register('serviceworker.js')
            .then((registration) => {
                // Registration was successful
                console.log('ServiceWorker registration successful with scope: ', registration.scope);
            })
            .catch(err => {
                // registration failed :(
                console.log('ServiceWorker registration failed: ', err);
            });
    });
}
</script>
</body>
</html>

serviceworker.js

const CACHE_NAME = "version-1";
const urlsToCache = [
    'index.html',
    'index.js'
];

// Install the service worker and open the cache and add files mentioned in array to cache
self.addEventListener('install', function(event) {
    event.waitUntil(
        caches.open(CACHE_NAME)
            .then(function(cache) {
                console.log('Opened cache');
                return cache.addAll(urlsToCache);
            })
    );
});

// Listens to request from application.
self.addEventListener('fetch', function(event) {
    event.respondWith(
        caches.match(event.request)
            .then(function(response) {

                if (response) {
                    console.log(response);
                    // The requested file exists in cache so we return it from cache.
                    return response;
                }

                // The requested file is not present in cache so we send it forward to the internet
                return fetch(event.request);
            }
        )
    );
});


self.addEventListener('activate', function(event) {
    var cacheWhitelist = []; // add cache names which you do not want to delete
    cacheWhitelist.push(CACHE_NAME);
    event.waitUntil(
        caches.keys().then(function(cacheNames) {
            return Promise.all(
                cacheNames.map(function(cacheName) {
                    if (!cacheWhitelist.includes(cacheName)) {
                        return caches.delete(cacheName);
                    }
                })
            );
        })
    );
});

3b. Code for loading offline.html when you are offline

To load offline.html instead of the actual site, we would add offline.html to urlsToCache[] array. This would cache the offline.html to your CacheStorage.

const urlsToCache = ['offline.html'];

If you are offline, the fetch operation would fail. So from the catch block, we can return our cached offline.html.

Now replace this block with the fetch listener block in the example above.

// Listens to request from application.
self.addEventListener('fetch', function(event) {
    event.respondWith(
        caches.match(event.request)
            .then(function(response) {

                // You can remove this line if you don't want to load other files from cache anymore.
                if (response) return response;

                // If fetch fails, we return offline.html from cache.
                return fetch(event.request)
                    .catch(err => {
                        return caches.match('offline.html');
                    })
            }
        )
    );
});

Custom "Add to Homescreen" banner.

This is how a default Add to Homescreen banner looks like in Chrome:
Screenshot to show how Add to Homescreen Banner looks in Chrome

To get this banner (According to Chrome - https://developers.google.com/web/fundamentals/app-install-banners/)

  • The web app is not already installed and prefer_related_applications is not true.
  • Includes a web app manifest that includes:
    • short_name or name
    • icons must include a 192px and a 512px sized icons
    • start_url
  • display must be one of: fullscreen, standalone, or minimal-ui
  • Served over HTTPS (required for service workers)
  • Has registered a service worker with a fetch event handler

If your app passes these criteria, Users on your website will get the Add to Homescreen banner like the one shown in the image above.

But instead of using the default banner we can even make a "Download App" button with our own UI.

This is how I show "Download App" button in my web app PocketBook.cc:
Screenshot that shows Custom Download button instead of default Add to Homescreen

When a WebApp is downloadable (that is it passes the criteria set by the browser), it fires an event called beforeinstallprompt.

we can listen to this event with window.addEventListener('beforeinstallprompt', callback)

We store this event in variable so we can call the .prompt() method later.
.prompt() method opens the Add to Homescreen dialog bar. So we can call this method when our Download App button is clicked.

In index.html, you can add

<button class="download-button">Download App</button> <!-- Keep its display:none in css by default. -->

and in JavaScript,

let deferredPrompt;
const downloadButton = document.querySelector('.download-button');

window.addEventListener('beforeinstallprompt', (e) => {
    // Stash the event so it can be triggered later.
    deferredPrompt = e;

    // Make the Download App button visible.
    downloadButton.style.display = 'inline-block'; 
});

downloadButton.addEventListener('click', (e) => {
    deferredPrompt.prompt(); // This will display the Add to Homescreen dialog.
    deferredPrompt.userChoice
        .then(choiceResult => {
            if (choiceResult.outcome === 'accepted') {
                console.log('User accepted the A2HS prompt');
            } else {
                console.log('User dismissed the A2HS prompt');
            }
            deferredPrompt = null;
        });
})


Helpful Links:


I hope this article was helpful. This article was part 1 of my series Make Websites work offline and the next part would be about IndexedDB.

Thank You for reading thisπŸ¦„ Do comment on what you think and if you are using a service worker for something different and interesting, do let me know in the comment section!

byeeeee 🌻.

Top comments (4)

Collapse
 
zak45409831 profile image
Alexander Markov

your instructions seems clean and tidy and useful. i will test your code today. my initial guess, is that, it will work without error. thanks. if it works, i will come here and send you another thanks.

Collapse
 
prafulla-codes profile image
Prafulla Raichurkar

Finding it a bit hard to understand, but still a good article!

Collapse
 
saurabhdaware profile image
Saurabh Daware 🌻

Thanks for reading! I guess trying out to run the code while reading would help :D

Collapse
 
devicemxl profile image
David

awesome master!! really nice, clean and concise