HTTPS and service workers


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.

Install baner

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('/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?

MIME type error

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.

Install baner

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

Homescren View

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:
Do you need help? Use our support forum

About the author