How to make your web app work offline with just HTML, CSS, and JavaScript.
In today’s world, more and more people have access to the internet. But internet connection is not always stable. And when it is relatively stable, users are not always able to get internet subscriptions. This causes a problem. For example, a potential customer may want to urgently get some important information from a company’s website but at that point lacks a stable internet connection, and cannot access the website. This could cause the company to lose a valuable customer.
This is just one of many negative effects of not being able to access the internet at all times. But should it be a problem? Must we always need a stable internet connection to access a website, even a website we just viewed some minutes ago? Surely not!
And so in this battle against slow or no internet connection, a savior came to show us the way, … Its name … Service Workers. (Drum rolls 🥁)
In this article, we are going to learn how to use service workers to ensure that users can access our web app even after going offline. We will also learn how to use service workers in combination with a web manifest to make our web app give users the kind of experience they get while using a native mobile or desktop app.
We will be making use of an already existing web application. The starter files can be found here.
WHAT IS A SERVICE WORKER?
A service worker is a javascript program that runs in the background of a browser without being affected by user interaction. They are used to store network requests and core assets (HTML, CSS, JS, images, etc) in the browser’s cache. This makes it possible to access the files and requests any other time after it has been saved, whether or not there is an internet connection.
There is more. Service workers can also be used to create push notifications to give the native app feel. And there is, even more, we can do with service workers, but we are only going to be looking at the most basic thing - getting our web app to work offline.
There is a caveat! Service workers can’t work over HTTP, except localhost. This means that we can only make use of service workers when we are on an HTTPS protocol (that is the URL begins with HTTPS) or when we are on a localhost port (in development).
Let’s get started.
REGISTERING A SERVICE WORKER
The first step in making our app work offline is registering a service worker. To do this, we create a ./sw.js
file in the root directory of our project. This is our service worker script, and it can be given any name. What is worthy of note is that the service worker must be located strategically in such a way that any file that needs to make use of the service worker is in the same folder as the service worker file or in a sub-folder. Now we go to our index.js
file to tell our app where our service worker is.
The index.js
file can be any JavaScript file that is run when our webpage loads. Now we paste the following code in our index.js
file.
The code on line 2 first checks if the browser supports service workers. Then we call the register
method of the navigator.serviceWorker
object. This method receives one argument, which is the path to the service worker file.
The method returns a Promise
object. The argument of the then
method is a callback that is run if the service worker registration is completed successfully. If the service worker registration is not successful, the callback in the catch
method is run.
INSTALLING A SERVICE WORKER
The next step is installing the service worker we just registered. Service workers are based on events. To install the service worker, we use the install event. This is the best place to cache resources and assets so that they can still be accessed when the user goes offline. This is done in the callback of the install event listener. Let’s paste the following in the sw.js
file we created earlier.
We call caches.open()
and pass the name of the cache as a parameter. This opens the cache if it exists, and if it does not exist, a new cache with the name is created Then we call cache.addAll()
passing an array of paths to files as a parameter. This adds the files to the cache that was opened earlier. We notice that we passed the caches.open()
as a parameter to e.waitUntil()
method. This is because cache.open()
and cache.addAll()
return promises. The event.waitUntil()
method helps to determine how long the caching takes before the installation of the service worker is completed.
If all the files specified in the array passed to the cache.addAll()
object are successfully cached, then the installation of the service worker is successful. But if anyone fails, the entire installation process will fail. And the installation process tries again for every refresh made on the app.
Note that we can perform many other tasks at this stage - in the install event callback. But for now, this is enough to get our app to work offline… Well, almost enough …
FETCH CACHED DATA.
We are done registering and installing our service worker, as well as caching some files. At this point, we need a way to be able to get our cached data when they are requested. We make use of the fetch event of the service worker to do this.
After the installation of a service worker, anytime the page is refreshed or a new page under the scope of the service worker is navigated to, the fetch event of the service worker is fired. In the callback of the fetch event, we specify what we want to do. We can get our cached data.
In the callback of the fetch
event, we specified a callback that intercepts any fetch request made in our app under the scope of the service worker. The event.respondWith()
method prevents the browser's default fetch handling and allows us to provide a promise for a Response
. So we pass a promise from caches.match()
. This method examines the request and looks in any of the caches that our service worker set up for any cached results that match the requested resource.
The code on line 7 above specifies that we return res
(the cached value) if we have a matching response; otherwise, we return the result of a fetch function, which will conduct a network request and return the data if anything can be received from the network.
At this point, we have successfully made our app accessible offline. If we deploy our app and try accessing it with an internet connection for the first time, the service worker gets installed. Refreshing the app or going to a new page allows the service worker to cache the specified files. If we go offline and refresh our app, we will still be able to access the site, and those resources that were previously cached will be fetched and displayed.
Since our app has been made accessible offline, we can end here. But we want to see how to make our web app give users the kind of experience they get while using a native mobile or desktop app. We do this with a web manifest in combination with the service worker we just added to our app. This turns our web app into a progressive web app (PWA).
PROGRESSIVE WEB APPS (PWA)
According to the MDN web documentation, PWAs are web apps that use service workers, manifests, and other web-platform features in combination with progressive enhancement to give users an experience on par with native apps. PWAs provide several advantages to users — including being installable like a native app, progressively enhanced, responsively designed, re-engageable, linkable, discoverable, network independent, and secure.
The step left to turn our web app into a PWA is adding our web manifest. The web manifest is a JSON file containing configurations and guidelines that inform the browser about our PWA and how it should behave when it is installed.
Web manifests are usually named manifest.json
. Although we can name it whatever, it has to end with a .json
or .webmanifest
extension. Also, the web manifest should be placed in the root folder of the project
The above file is what should be placed in our manifest.json
. Each of those keys has a different meaning and gives the browser a particular instruction regarding handling the PWA.
- The
name
andshort_name
specifies the name of the PWA. - The
description
gives a short description of our app. - The
start_url
tells the browser where it should start when the browser is launched. - The
theme_color
specifies the background color of the toolbar. Note that the color we specify as the theme color should be the same as the one we specified in our HTML - The
background_color
specifies the color of the background of the splash screen (introductory screen of our application) when the app is installed on mobile. - The display specifies a custom browser UI that is shown when our app is launched after it has been downloaded. It has several options including
fullscreen
standalone
minimal-ui
browser
- The
icons
property is an array of image objects that defines the set of icons the browser should use on different parts of the app - home screen, splash screen, app launcher, etc - after the user has installed the PWA.
To get an in-depth explanation of these properties and more, check out this web.dev article or this MDN article.
At this point, if we load our app over an HTTPS protocol or on localhost, the browser will ask us to install the app. On desktop, we should see this prompt at the right-hand side of our URL bar. And on mobile, we should see it at the bottom of our screen.
Also, installing the app creates an app launcher that you can use to launch the app at any time.
Furthermore, opening a PWA that has already been installed on a desktop brings up an icon that asks us to open this link with an app. Clicking on the icon opens the app.
GALLERY
Below are some shots from our PWA, the process of installation and launching on both mobile and desktop devices.
SUMMARY
In conclusion, to make our web app work offline, we need to implement a service worker. And to give our web app some of the features of a native app, we include a web manifest.
The live version of the app we used in this tutorial is here and the github repository is here.
Signing out, Steph Crown ✌️
Subscribe to my newsletter
Read articles from Stephen Emmanuel directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Stephen Emmanuel
Stephen Emmanuel
I am a software engineer interested in gaining and sharing knowledge. Most interested in web development and blockchain. Writing about React.js, TypeScript, Phoenix, ClojureScript.