0% found this document useful (0 votes)
5 views

How To Use Service Workers in Javascript - DEV Community

How to use service workers in javascript - DEV Community

Uploaded by

M052221
Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
5 views

How To Use Service Workers in Javascript - DEV Community

How to use service workers in javascript - DEV Community

Uploaded by

M052221
Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 19

tq-bit

Posted on Apr 15, 2021 • Updated on Jul 8, 2021

5 2

How to use service workers in javascript


#webdev #javascript #tutorial

Progressive Webapps use service workers to make websites and webapps feel more like
the native apps users know and love on their phones. This article will give you an
introduction into the topic with some simple - to - follow code examples.

Technological purposes and limitations


Being a proxy between content on the internet and the user's client, service workers are
addressing the issue of making browser-specific content available even when one's
device is offline. Once registered, they are used for a variety of features, some of which
are:

Client-side caching of static files and remote data


Serverside push - messages, e.g. with Node.js and web-push
(Periodic) background data synchronisation
Take devdocs.io. The site offers its full content within a Progressive Webapp (PWA) that
will even be available if your computer or mobile phone is off the net, given you have
installed it when visiting the website

When clicking on the + - sign, the PWA will be installed and grants you offline-access to
devdocs.io

You should not mistake PWAs with desktop or native applications built with Electron.js
or Nativescript though - they do only run on the browser's context and have no access
to device specific APIs.

But even without using the full toolkit, service workers give you a high level of control
over what gets cached, improving app speed and lowering server-side pressure. As of
2021, they are also supported in all major browsers, making them well considerable for
production - ready apps.

Service worker constraints


When developing webapps using them, you have to consider that service workers

can only be used in a https - context for security reasons (note that for development
localhost is also considered a secure domain)
run on a separate browser thread, therefor have no direct DOM - access.
run completely asynchronous and rely a lot on promises. In case you need a refresh,
I've got an article on promise basics here.

Project setup
You can of course follow freestyle, but I recommend you use the boilerplate from my
Github repository - it includes placeholders for the functions that are introduced in this
article, as well as some helper functions to create new elements on the DOM. If you just
want to jump into the source code, there's also a 'done' branch.

https://github.com/tq-bit/service-worker-basic-demo/tree/main

The initial user interface looks like this:


The main.js - and the serviceworker.js file also include a bit of boilerplate, like logging
and to create / query DOM elements.

Registration process, scope and state: Illustration


The following graphic from MDN perfectly sums up a service worker lifecycle. In the
coming section, I'll use some code snippets below to illustrate how to hook up events to
each of them.
Service worker Lifecycle by Mozilla Contributors is licensed under CC-BY-SA 2.5.

Registration process, scope and state:

Working code examples


Before you move ahead, let's take a moment and describe what we'd like to do next.

We will

1. register a service worker script inside our application


2. unregister the worker again and add a function to monitor if a worker is currently
active in the user interface
3. take a look at basic service worker features, such as initial file - as well as http-
request caching

Note that for each snippet below, there is a corresponding TODO - comment inside
each file that serves as a placeholder. If you somehow get lost, try and use the Todo
Tree VSCode plugin for quick navigation.

1: Register the service worker.


Before doing anything else, a service worker has to be downloaded and registered on
the client-side. Imagine it as just another JS - file you would place inside the body of
your index.html , just that it runs separated from the main thread. Like this, the lifecycle
illustrated above will start and we have access to the Service-Worker's API.

Heads up: Many other articles also cover a few lines of code to check if service
workers are supported by the browser. I've left this out on purpose, as most of the
modern browsers have a built-in support as of today.

Add the following to your main.js file

// TODO: Add the registerWorker function here


const registerWorker = async () => {
try {
// Define the serviceworker and an optional options object.
const worker = navigator.serviceWorker;
const options = { scope: './' };

// Register the worker and save the registeration in a variable.


const swRegisteration = await worker.register('serviceworker.js', options);

// We will make use of this event later on to display if a worker is registered


window.dispatchEvent(new Event('sw-toggle'));

// Return the registeration object to the calling function


return swRegisteration;
} catch (e) {
console.error(e);
}
};
Once you click the button Register Worker in your browser, the service worker is
downloaded from the location you've given in the worker.register - method. It then
proceeds to run through the lifecycle methods and, once that is done, remains idle until
receiving an event-nudge from the main Javascript thread.

To confirm everything worked, check your browser's development tools under


Application > Service Workers - as we can see, the registration process was successful
and your worker is ready for action.

Heads up: While you're here, make sure to toggle the Update on reload checkbox - it
makes sure that no data remains unintentionally cached.

2: Unregistering and monitoring


Now while one might just take the above code as given and use it as-is, I was curious to
understand what exactly was going on with this registration object that is returned by
the worker.register - method.

Turns out that, once downloaded and activated, a service worker registration is created
inside the navigator.serviceWorker container and can be read out like this:

const swRegisteration = await worker.getRegistration();


This means: If there are no active instances, the above variable declaration will resolve
into undefined , which comes in handy and allows us to show possible registrations in
our user interface.

Add the following to your main.js file:

// TODO: Add the unregisterWorker function here


const unregisterWorker = async () => {
try {
// Define the serviceworker
const worker = navigator.serviceWorker;

// Try to get a sw-registration


const swRegisteration = await worker.getRegistration();

// If there is one, call its unregister function


if (swRegisteration) {
swRegisteration.unregister();
window.dispatchEvent(new Event('sw-toggle'));

// If there's none, give a hint in the console


} else {
console.info('No active workers found');
}
} catch (e) {
console.error(e);
}
};

To round things up, add the following to your main.js file for user feedback:

// TODO: Add checkWorkerActive function here


const checkWorkerActive = async () => {
// Get registration object
const swRegisteration = await navigator.serviceWorker.getRegistration();

// Query for the indicator DOM element and remove its classes
const indicator = dqs('#worker-indicator');
indicator.classList.remove('bg-danger', 'bg-success');

// Change its content according to whether there's a registered worker or not


if (swRegisteration && swRegisteration !== undefined) {
indicator.innerText = 'You have an active service worker';
indicator.classList.add('bg-success');
} else {
indicator.innerText = 'Service worker is not active';
indicator.classList.add('bg-danger');
}
};

Finally, hook the method up to the sw-toggle event that is fired when registering and
unregistering happens (therefor the window.dispatchEvent ):

// TODO: Add the sw-toggle - event listener here


window.addEventListener('sw-toggle', () => {
checkWorkerActive();
});

Back to your app, the image below now shows an active service worker instance.

Once you click on Unregister , you can also monitor the change in your devtools
In case of manual registration / un-registration via the button, you might have to
refresh your browser once to activate the service worker. In a productive scenario,
this step should be taken by the website's / app's lifecycle methods

That wraps up how to handle registration and also what we want to do within our
main.js file. Let's now take a look inside the serviceworker.js file.

3. Caching and offline-availability


Two basic functionalities of a service worker are making static files available for offline
usage, as well as caching requests from a remote server. A core benefit to be taken
away here is an improvement in user experience due to faster - or offline - page
loading. To wrap this article up, let's find out how it works.

Note that, since a service worker will only work over https or from the localhost, you
will need a development server to serve your files. If you're using VSCode, try the Live
Server extension.

3.1 Service worker global 'this'


The global this behaves a bit differently inside a service worker - compared to the
main.js - file. In a nutshell:
this describes the object that owns the function calling it (read more about the
topic in general on MDN).
In the context of a service worker, it is represented by the ServiceWorkerGlobalScope -
object

Inside the service worker file, the same provides us functions and properties such as
self or caches . These we can utilize to enforce the service worker magic.

3.2 Caching strategies


Since the global service worker scope might compete with the version of your webapp,
you have to make sure old caches get cleaned up properly before a new instance of
your project is deployed. One method to do the same is to define an app version as well
as a whitelist, based on which a new instance, before getting to work, can do some
cleanup tasks (remember the visualization above? This happens in the active - phase).
These two variables are already available in the serviceworker.js file, we'll use them in
the upcoming snippets.

// appWhitelist indicates of which versions caches are meant to be kept


// If there is a gamebreaking change in static files or data delivery,
// you should consider depracating old apps by removing their ids from here.
const appWhitelist = ['app_v1', 'app_v2', 'app_v3'];

// appActive indicates the currently active cache, or more specific the name
// of the cache used by the application. This variable should be synchronized
// with appWhitelist and fit the latest app version.
const appActive = 'app_v1';

// appFiles holds the path to files that should be cached for offline usage
const appFiles = ['./index.html', './main.css', './main.js'];

In case you do not want to handle these strategies yourself, there are a few handy
javascript libraries that can help you out, such as workbox-sw.

3.3 Caching static files


Having said and considered the above points, caching static files is as easy as adding
the following snippets to your serviceworker.js file

// TODO: Add cacheAppFiles function here


const cacheAppFiles = async (appActive, appFiles) => {
// Wait for the active cache version to open and add all files to it
const cacheActive = await caches.open(appActive);
cacheActive.addAll(appFiles);
};

While we are at it, let's also add a function to get rid of old caches. Like this, we can
make sure that only the current relevant cache is active and no old files will get in the
way and cause inconsistencies.

const deleteOldCache = async (appWhitelist) => {

// The caches.key property contains an array of cache names. In our case,


// their names would be app_v1, app_v2, etc. Each of them contains the
// associated cached files and data.
const keys = await caches.keys();

// In case the cache is not whitelisted, let's get rid of it


keys.forEach((version) => {
if (!appWhitelist.includes(version)) {
caches.delete(version);
}
});
};

Then, once a new service worker is installing, call this function. the event.waitUntil -
method makes sure the above function resolves before moving ahead in the code. After
installation, the files will then be cached and ready for offline usage.

self.addEventListener('install', (event) => {


// Add the application files to the service worker cache
event.waitUntil([cacheAppFiles(appActive, appFiles)]);
});

self.addEventListener('activate', (event) => {


// Remove all old caches from the service worker
event.waitUntil([deleteOldCache(appWhitelist)]);
});

And that's about it - the defined files are now available within the service worker's
cache.
3.4 Accessing cached content
The above makes sure our caching strategy is being enforced, but does not yet give us
access to the files or data being stored. To gain access, our service worker has to listen
to outgoing http-requests and then - based on our caching strategy - either return a
cached response or fetch the data from the remote location.

Let us first add the necessary event listener. Add the following to your serviceworker.js
- file

self.addEventListener('fetch', (event) => {


// When receiving a fetch - request, intercept and respond accordingly
event.respondWith(cacheRequest(appActive, event.request));
});

As you see, cacheRequest takes in two arguments - the active version of the cache, as
well as the outgoing request from the client to the server. It is meant to return a
response that can be resolved as if there was no middleman involved. Therefor, before
we write the code, let us first define what exactly is meant to happen.

1. Check all active service worker caches (not just the currently active one, but all!) for
an already cached response.
2. If it exists, return it - no network communication happens and the http-request
resolves. If it does not exist, move ahead.
3. Check if the user is online (via navigator.onLine property)
4. If user is online, execute the fetch-request. When it resolves, clone the raw response
and put it into the currently active service worker cache (not all, just the currently
active one!). Also, returns response to the calling function
5. If user is offline and no cached content is available, log an error to the console.

At this point, I'd like to state that a carefully chosen caching-strategy in step 3.1 is key to
properly handle these interceptions.

In the described case, a static file's version that has been cached in app_v1 will still be
fetched in app_v2 if the app_v1 still remains in the appWhitelist array. This will cause
problems if the initial file is outdated.

Now, to wrap caching up, add the following to your serviceworker.js - file
const cacheRequest = async (appActive, request) => {
const online = navigator.onLine;

// 1. Check if a cached response matches the outgoing request


const cachedResponse = await caches.match(request);

// 2. If response has been cached before, return it


if (cachedResponse) {
return cachedResponse;

// 3. Check if user is online


} else if (online) {

// 4. If response is not in cache, get it from network and store in cache


const response = await fetch(request);
const resClone = response.clone();
const cache = await caches.open(appActive);
cache.put(request, resClone);

// Return the response to the client


return response;
} else {

// 5. If none of the above worked, log an error


console.error('No cached data and no network connection recognized');
}
};

3.5 Final result and outlook to other features


It was a tough ride, but we've finally arrived at the point we can put everything together.
What we can do now is:

Cache static files and remote server responses


Access not only one, but several caches at once
Integrate a simple caching strategy that keeps our caches lean and clean

Don't take my word for it though - try it out yourself. Below, I'll link you the final Github
branch so even if you didn't follow every single step, you can get your hands dirty and
try an offline-first approach. If you'd just like to take a glimpse into the functionality of
this article's proof of concept, I've also added some screenshots for that under 4.
Working samples.
https://github.com/tq-bit/service-worker-basic-demo/tree/done

So what are you waiting for? Clone down that repos and start coding.

4. Working samples
4.1 Lifecycle and exercising caching strategies
Assume you just deployed your service worker app or release a new app (and therefor a
new cache) - version, your service worker will do the necessary setup during installation:

A new service worker will always clean up old versions that are not whitelisted and make
sure the static files are available before the first fetch request. Note how it conveniently
caches the bootstrap css I'm using for the styling.

4.2 Service worker at work - online


Once registered, try and fetch the test data once. You'll notice they get cached and
retrieved in case a subsequent request matches a cached response. While the static files
were available right away, the dynamic data from jsonplaceholder were not. After they
have been saved once, however, and the appVersion remains part of the appWhitelist ,
the service worker will deliver the data from the cache instead of getting it from the
remote server.
Static content is available straight away, as it's been registered while installing the
service worker. Remote data have to be fetched once on demand.

4.3 Service worker at work - offline


The same thing now also works offline. Try to tick the 'Offline' checkbox in your devtools
and hit 'Fetch test data'

The content is now always delivered from the cache.


This post was originally published at https://blog.q-bit.me/an-introduction-to-the-
javascript-service-worker-api/
Thank you for reading. If you enjoyed this article, let's stay in touch on Twitter 🐤
@qbitme

👋 While you are here

Reinvent your career. Join DEV.

It takes one minute and is worth it for your career.

Get started

Top comments (0)

Code of Conduct • Report abuse

Auth0 PROMOTED
Easy to implement, endless possibilities
With a few lines of code, you can have Auth0 by Okta integrated in any app
written in any language and any framework. 🚀 Start building for free today.

Sign up now

tq-bit
I'm a selftaught (web) developer. On sunny days, you can find me hiking through the Teutoburg
Forest, on rainy days coding or with a good fiction novel in hand.

LOCATION
Bielefeld, Germany
WORK
Developer @ CPro IoT Connect
JOINED
Dec 21, 2020

More from tq-bit

15 open-source tools to elevate your software design workflow in 2024


#webdev #productivity #opensource #programming

How to get the count of your followers on dev.to


#javascript #todayilearned #webdev #tutorial

How to use IndexedDB to store images and other files in your browser
#javascript #webdev #database #tutorial

Arm PROMOTED
Stay Ahead with Cloud-Native Insights
💪 Save resources and build scalable and efficient cloud-native apps.

Register Now

You might also like