Author: Mikołaj Smoleński
HTTPS & Service workers
Let's talk about the last two requirements that we need to meet before our app becomes installable - having it
served through https and do so accompanied by a service worker.

What is a service worker?
A service worker is a type of web worker, an exciting concept which has opened modern browsers to the world of offline experiences. How? In short, it is a script that runs on a separate thread, independently from the website. Why is it such a game-changer?
During the first steps into the world of JavaScript, you probably saw how infinite loops in the code affected the UI - namely, the JS-run functionalities became unreposnsive. They blocked "the main thread" and then the interface froze.
It's all because JavaScript is a single threaded language. It means it has one call stack and one memory heap, executing code in order. When things go south (like loops iterating endlessly or synchronous requests taking too long), the whole thing topples, rendering the UI unresponsive.
The idea of a web worker is exciting, because it has its own thread, and so it can be performing heavy-lifting tasks, which otherwise would reduce your website's performance, in the background. What's more, according to the spec, workers are able to be working for a long time after the window responsible for its spawning has been closed (although it can take no more new tasks).
"Ok," someone could say, "so a service worker is like some additional computing power, but we get it anyhow, as the hardware tech progresses." Workers would be a considerable boost with no clear direction, if it wasn't for one architecural implication: they sit between the browser and the network and act as proxy servers. It means that they not only get their own thread for computation, but also they do so while in between the client and the server. They can intercept requests made by the website and redirect them to the cache. Imagine, there is an outgoing request for constent and all the response get saved in the browser memory along the way, dropping the need for requesting it again. Next time we are trying to access some of it, no internet connection is needed.
This is where the service worker shines - as a vehicle for caching and data management. It can provide us with content access and features like push notifications, even in case the internet connection was lost.
This kind of power comes with a price, though. This is a new technology, so it's only supported by modern browsers.
Also, as service workers could be easily misused, they use is restricted to running across HTTPS for security
reasons. Although during development we will be able to use a service worker through localhost, to
deploy it on a site, a HTTPS setup on your server will be needed.
Web workers are a huge topic by themselves and here I'll touch upon their bare, code-based essentials in the PWA context - namely, how to register one, what it usually consists of and some tips on development. There are some extensive materials regarding the subject prepared by the industry leaders - be sure to check them for a deep dive.
The File
Service Worker is JavaScript code living in a different thread than the browser's main one, where the UI and and
rest of the JS resides. In that way, it is very special - for browsers to be able to adequately utilize it, it must
be properly registered (more on that below) and defined in a separate file. From there it does not have direct
access to reglar Web APIs, like the window global object.
What the worker does, in practive, is reacting to certain events - this is why the file itself consists mainly of
callbacks. To refer to itself from within the worker to add event listeners, the API exposes a self
variable, making it a little bit more reliable for beginners than the regular, dynamically bound this.
const cacheVersion = 'v1';
self.addEventListener('install', function (event) {
self.skipWaiting(); // We do it for our worker not to wait until the previous one expires
event.waitUntil(
caches.open(cacheVersion).then(function (cache) {
return cache.addAll([
'./index.html'
]);
})
);
});
self.addEventListener('fetch', function (event) {
event.respondWith(caches.match(event.request).then(function (response) {
// caches.match() always resolves
// but in case of success response will have value
if (response !== undefined) {
return response;
} else {
return fetch(event.request).then(function (response) {
// response may be used only once
// we need to save clone to put one copy in cache
// and serve second one
let responseClone = response.clone();
caches.open(cacheVersion).then(function (cache) {
cache.put(event.request, responseClone);
});
return response;
}).catch(function () {
return console.log('no cache response!');
});
}
}));
});
As you can see, the worker above listens for two events.
The install event provides an opportunity to define the caching policy. First, we call
self.skipWaiting(); so that our Worker gets to work instantly. Then, it opens a particular cache (named
v1) and uses the cache.addAll() method to add a list of specific files there. As of now,
we only cache ./index.html.
What we have stored, we ultimately want to retrieve. To define the fashion in which it happens, fetch
event callback and response's .respondWith() method are used. In case there is an already stored
resource for a specific request (caches.match(event.request)) and it's not undefined, we retrieve it
and pass as the return value of the fetch. In case it is in fact undefined (so, not stored), we do a
request to the web for it, but immidiately it gets back, we clone it, so we can both store it in the cache and serve
it as a response.
With this worker script we cache what we need, and once the client gets back to us on a subsequent visit, the resources it requested for in the past go straight from the cache -- and in case we get asked for something we do not have stored yet, we fetch for it, to serve it and store for later.
The Registration
For the above file to be considered a service worker and not a regular, "same-thread" JavaScript file, it must be
registered as such. We could do it wherever (including a rude script tag within HTML), but we'll do it
in the (webpack's & JavaScripts') entry point, namely the src/main.js file, tucked underneath the root
Vue insance.
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('https://mdbcdn.b-cdn.net/service-worker.js').then(() => {
console.log('Service Worker registered!');
});
} else {
console.log('Service Worker not supported :(');
}
In webdev practive, it would be worthwhile ensuring that what we do above, which still takes place on the main
thread, is not blocking us from rendering what we want to show. We could achieve it by simply wrapping the
registration into the the window's load event callback. Luckily, though, the webpack-transpiled
code is being injected at the end of the <body> tag, which suffices for a guarantee in our use
case.
Workflow twaeks
With the register and install life-cycle steps accounted for, we have all the main
indridients for a successful service worker implementation. We might find an issue in the console regarding the
file's MIME type - apparently service-worker.js is not JS. What's more - clicking the link to the file
in the error description leads to opening up a new tab with our project! What is happening?
The reason for this weird behavior lies in our webpack config. Let's start with the fact that
webpack-dev-server, a crucial part of our development environment, is not informed that there is a
JavaScript resource that should be available from within our running web server and, even more suprisingly, is to
not be processed by the bundler. This unavailability results in the puzzling side-effect: when we try to open the
file, we get redirected to our ./index.html, so a new localhost tab within the browser. No
woneder the MIME types don't add up - in this mix-up, we end up trying to register a service worker while
providing a HTML file instead.
How to fix it? We know about the /static directory from where the resources are available for our
locally hosted environment. Moving ./service-worker.js to there, where our manifest.json
already resides, seems like a good way to sneak it in. Unfortunately, getting it to work will not be as
straightforward.
Considering how powerful a feature the service worker is, there are security, anti-misuse measures
undertaken. One of them is the the service worker's scope - its property defining the exact reach
within a project. It oftentimes is assumed, defaulting to {scope: '/'}, but can be customized using
options object passed as the second argument in navigator.serviceWorker.register() function. It means
that you can have custom workers for different places of your web project!
Whatever the assumed or the declared scope is, however, there's a catch: service worker can never reach "up", so
outside the directory where the very file is held. So, putting it in the /static folder would mean it
can cache only the resources there, and never, say, our ./index.html waiting outside. We have to make
the webpack config acknowlage the file instead.
To do it, we need to slightly alter our dev and prod webpack config files in the
/build directory. In both of them we are utilizing the CopyWebpackPlugin to move our files
around. Let's mention our service worker there - that way it will available both from within our dev server and the
/dist directory once we build.
new CopyWebpackPlugin([
{
from: path.resolve(__dirname, '../static'),
to: config.dev.assetsSubDirectory,
ignore: ['.*']
},
{
from: path.resolve(__dirname, '../service-worker.js'),
to: './service-worker.js',
}
])
new CopyWebpackPlugin([
{
from: path.resolve(__dirname, '../static'),
to: config.build.assetsSubDirectory,
ignore: ['.*']
},
{
from: path.resolve(__dirname, '../service-worker.js'),
to: './service-worker.js',
}
])
And voilà! Now we are declaratively giving access to the file to the webpack dev server, while also having the
worker script copied to the /dist when we build. You can check the worker we just registered out in the
Application tab in Chromium DevTools or at about:debugging#/runtime/this-firefox in
Firefox (simply copy and paste the string to the address bar).
Default PWA scaffolding with Vue-CLI
Our purpose here was to do everything by ourselves for learning purposes, but in everyday developer work we could
make use of some already existing tools for the job. For new projects we could prefer to exchange the fine-grained
control provided with a custom webpack config, for a ready-made, scaffolding solution of Vue-CLI. The command line solution provides us with less "to
the metal" configuration options, as the bundler's roughness is hidden beneath the vue-cli-service
scripts' abstraction, while adding some helpful extras - a ready-made project strucutre and some great development
features, including a great PWA support plugin.
A project created this way would have a src/registerServiceWorker.js script included out-of-the-box.
Under the hood it would utilize register-service-worker, a module
created by Evan You, Vue author, that simplifies worker registration and provides hooks to customize worker's
reaction to some of the most common events, like caching, finding an update or going offline. It really makes life
easier when it is the quick prototyping we are after.
Serving over HTTPS
Another requirement for rendering the App Installation Banner by the browser is having the project deployed on a HTTPS-certified domain, which we have, thanks to Firebase Hosting. So, let's update our app:
yarn deploy
Now, let's test it. If you open your website, after a few seconds you should see an install banner. Install the app.

Do you see this? Our nice app icon is staring at us from the smartphone's home screen!

In the next lesson, we will make our application work offline
Something doesn't work for you? Then check the code for this lesson in our repository
Previous lesson Download Live preview Next lesson
Spread the word:
