Make websites work offline - What are Service Workers and How to get a custom App Install button on the website.
Tags: javascript, webdev, html
Published: 2020-01-07 14:03:58

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?](#why-do-we-need-service-worker)
- [How Service Worker works?](#how-service-worker-works)
  - [Service Worker Lifecycle](#service-worker-lifecycle)
- [How to implement Service Worker](#how-to-implement-service-worker)
  - [1. Service Worker Registration](#1-service-worker-registration)
  - [2. Handling Requests with Service Worker](#2-handling-requests-with-service-worker)
      - [2a. Storing URLs to Cache](#2a-storing-urls-to-cache)
      - [2b. Loading file from the Cache](#2b-loading-file-from-the-cache)
      - [2c. Handling new Cache version](#2c-handling-new-cache-versions-optional-you-can-avoid-this-if-you-hate-your-life)
  - [3. Complete example for implementing Service Worker](#3-complete-example-for-implementing-service-worker) (If your deadline to implement service worker is tomorrow, just jump here.)
      - [3a. Code for loading index.html and index.js offline](#3a-code-for-loading-indexhtml-and-indexjs-offline)
      - [3b. Code for loading offline.html when you are offline](#3b-code-for-loading-offlinehtml-when-you-are-offline)
- [Custom "Add to Homescreen" banner](#custom-add-to-homescreen-banner) (If you are only here for how to get custom App Install button on website, you can jump here)
- [Helpful Links](#helpful-links)


# 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](https://thepracticaldev.s3.amazonaws.com/i/pb2by543uosbnrw1u71n.png)

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](https://thepracticaldev.s3.amazonaws.com/i/srwjkrxtnq51867se7xx.png)


And here's a humblebrag: 
Last year I made a game called [Edge of The Matrix](https://eotm.ml) 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](https://thepracticaldev.s3.amazonaws.com/i/7ltr51gej71rrl8g98m1.png)

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](https://thepracticaldev.s3.amazonaws.com/i/dvaipw6r2v5a5w23dpyd.png)

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
<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`,
```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`,
```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](#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`,
```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
<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`
```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.
```js
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.
```js
// 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](https://thepracticaldev.s3.amazonaws.com/i/13owh2jlq3iixpx41oyc.png)


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](https://developers.google.com/web/fundamentals/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](https://pocketbook.cc):
![Screenshot that shows Custom Download button instead of default Add to Homescreen](https://thepracticaldev.s3.amazonaws.com/i/em9akjn48g1pg9a9yimc.png)

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
```html
<button class="download-button">Download App</button> <!-- Keep its display:none in css by default. -->
```
and in JavaScript,

```js
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:
- [DEV's Service Worker file for example](https://github.com/thepracticaldev/dev.to/blob/39618f66a2167e88e65527b68d0fd996f0869c0a/app/assets/javascripts/serviceworker.js.erb)
- [Google Developer: Service Workers - An Introduction](https://developers.google.com/web/fundamentals/primers/service-workers/)
- [Google Developer: Service Worker Lifecycle](https://developers.google.com/web/fundamentals/primers/service-workers/lifecycle)
- [Google Developer: App Install Banners](https://developers.google.com/web/fundamentals/app-install-banners/)


--- 
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 🌻.