Workbox

Preamble

Pardon the interruption in the Alexa Skill and serverless series, but a couple of things happened. First, I started using the next version of the Alexa SDK for node.js. Second, I saw an excellent Alexa Skill Developer promotion that I had to write a new skill for, so that took up my free time in October, other than working on my kid's Halloween costume; I'll be updating my next post in the series to reflect the new API. Lastly, I've started diving into improved configurations of service workers, which has led me to be a firm proponent of workbox.

Service Workers

A service worker is a segregated bit of JavaScript, executed in a separate context from the main window (in the "background"), and registers against a given origin and path. It's a sort of event driven worker, but if you're looking for more information on what they are and documentation, I recommend:

Why Use One

The service worker API relies on a few things, such as the fetch API and Promise API. That being said, what it means at a functional level is that the service worker, once registered, can listen in on and, potentially, intercept failing network requests. As I'm sure you can imagine, this can be immensely powerful, once they're configured, registered, and working correctly.

Other Requirements

Service workers require being loaded/registered over HTTPS. For local testing, you can either implement a development server that supports HTTPS and, as is preferable, a certificate configured for your localhost environment.

Since it executes outside the context of the main page, it must be within a separate file.

Support

Ever since iOS 11.3, service workers are supported with some rather minor caveats. Android Chrome has been fully ready for a while. On the desktop, Firefox and Chrome have been playing ball for a while and MS Edge has supported for a little while. This means that unless you're locked into an abusive relationship with yet another version of IE (11), we can play with the cool toys.

Registering

Feature detection, and environment detection, can be pretty easily done, especially when you're building your source application files. I have a preference of late to parcel, which allows for making use of the pretty common node.js convention of checking whether the NODE_ENV environment variable is set to production, this means that, once it is set up, I can avoid loading it in my local environment, for simplicity. All that's left is to create the service worker itself, here referenced as sw.js.

// script at bottom of index.html or main.js
if ('serviceWorker' in navigator && process.env.NODE_ENV === 'production') {
window.addEventListener('load', () => {
navigator.serviceWorker
.register('sw.js')
.then(r => {
console.log('service worker registered in scope: ', r.scope);
})
.catch(e => console.log('SW error: ', e));
});
}

*note: since I'm using parcel, it's intelligent enough to try and find all my referenced js files, such as sw.js. As I'm not creating the "real" version of this file until the production build is created, and parcel isn't checking the if condition of 'serviceWorker' in navigator, that means I've created a stubbed, but otherwise emtpy, file called sw.js in the source.

Enter Workbox

Workbox describes itself as "...a library that bakes in a set of best practices and removes the boilerplate every developer writes when working with service workers." Workbox describes itself as being the successor to both sw-precache and sw-toolbox.

My experience has been pretty close to eliminating the need for manual creation of a service worker, and it has been easier than my experience with either above mentioned library. This is likely why I've enjoyed it so much, I don't need to do the manual work while keeping any specifics to a simple configuration.

Flavors

There are a few flavors of workbox which you can use. These generally amount to a node module, a webpack plugin, and a cli tool. Each is relatively easy to get started with, but I'm going to focus on using the CLI tool.

Install and Configuration

I've found the workbox-cli tool works rather well in an agnostic fashion when used with parcel. There do appear to be a couple parcel plugins which aim to achieve the same, but one is for sw-precache, and the one I found on GitHub targeting workbox isn't even published to npm. So here I am, thankfully it's an easy path to follow.

  1. npm install --save-dev workbox-cli, so simple, just about any dev should be able to do it
  2. run the wizard, which will prompt you with questions to assist in creating your configuration file
  • if you've installed workblox globally, you can call it with the name workbox, workbox wizard
  • if you've installed it as a development dependency, as I outline in step 1, you can access it in the .bin directory inside of node_modules, ./node_modules/.bin/workbox wizard
  • that script by name will work in an npm script, which includes the nested .bin directory in its PATH
  1. add the build of the service worker to your build pipeline, such as npm scripts

Creating the Configuration File

The wizard will ask you things like "where is your build app deployed from?" I'm building an application to the dist/ directory in my project repo. This is the directory I have defined. Then, having scanned the contents of my dist/ directory, it prompts me on what to cache, by file type (extension). This is an interactive list, so you can de-select as the case may be, but that's probably unlikely. Lastly it will ask you what you want your file to be named. For consistency with the script I have to register it, it will be sw.js. Here's my config file:

// config/workbox-config.js
module.exports = {
"globDirectory": "dist/",
"globPatterns": [
"**/*.{png,ico,html,css,js}"
],
"swDest": "dist/sw.js"
};

Updating The Build Process

I have been using an npm script build to perform my build of a production version of the app with npm run build. This is preserved and only added to, as I then created a build:sw script in my package.json's scripts block. Now, in my deploy config, it calls the unit tests, build, and build of the service worker as such:

- npm test
- npm run build
- npm run build:sw

The Service Worker Build Script

You can use one of a couple options in generating the service worker. The CLI tool outlines the following as actions the CLI tool can take:

  • wizard: a step-by-step guide to set up Workbox for your project
  • generateSW: generates a complete service worker for you
  • injectManifest: injects the assets to precache into your project
  • copyLibraries: copy the Workbox libraries into a directory
wizard

I've already discussed what the wizard task will prompt for setting up a configuration file.

generateSW

If you have basic pre-caching needs without any advanced configuration, you'll want to use generateSW, as it's the simplest. The example project I mention below uses this approach, and the invocation is workbox generateSW <path-to>/workbox-config.js. It uses its precacheAndRoute implementation out of the box.

injectManifest

This is perhaps the most interesting one, as it's certainly powerful. When your needs exceed simple precaching of assets, you'll want to specify a few things in a source service worker file. In that src/sw.js file that was a stub earlier in my described example, now it becomes closer to the final version of the generated service worker; don't worry though, we won't need to copy in any files.

First, we update our workbox-config.js file to accomodate the source for the sw.js file; it's the swSrc property.

// config/workbox-config.js, with source sw defined
module.exports = {
"globDirectory": "dist/",
"globPatterns": [
"**/*.{png,ico,html,css,js}"
],
"swDest": "dist/sw.js",
"swSrc": "src/sw.js"
};

Now that that's set, it's time to fill out our src/sw.js file. We begin it with the importPackage of the latest workbox-sw script, in this case from Google's CDN. Then we define the precache strategy that we'll use as the base, leaving an empty array. This will let the glob matching defined in the config file build and populate that array with what's needed.

Lastly, we update the command to generate the service worker with the task passed to the workbox cli; workbox injectManifest <path-to>/workbox-config.js.

*note: for more on workbox caching strategies, there's plenty to read up on them

Finally, we have the two big exceptions that drove me to use injectManifest in this example. In the repo for my GitHub user page, I pull down both the JSON response for my user account that drives the information in the corresponding vue component. This is the https://api.github.com/users/<username> call. This also has a link to an avatar image, https://avatars3.githubusercontent.com/.... Those two specific responses I want to include as cached, or at least cacheable, for this site. Hence, the following in the src/sw.js file.

// advanced config for injectManifest approach
importScripts('https://storage.googleapis.com/workbox-cdn/releases/3.6.3/workbox-sw.js');
workbox.precaching.precacheAndRoute([]);
workbox.routing.registerRoute(
new RegExp('https://some-other.com/routed-response/i-want-cached'),
workbox.strategies.cacheFirst()
);

The strategy I used is cahce first, which will cache and prefer the cahced copy of this/these assets. This is a choice on my part and, if I expected these items to change with any great frequency, I would probably want to give ita network first strategy, so the cache would be the fail over, not the norm.

copyLibraries

The copyLibraries task will do just that, copying the needed libraries that the generated service worker will use, to your server, as opposed to pulling them from the Google CDN. You can invoke it like so:

workbox copyLibraries <path-to>/workbox/

An Example

If you're looking for an example, I recently put together a simple app in vue.js which uses each of the above configs and build/deploy tasks, with one change, the workbox config also bundles up some small audio files. The application is a pointing sound board, in that it is a sound board geared for a humorous addition to pointing poker. It works well in the browser, is installable to a mobile device's home screen (Android and iOS), and works entirely offline. The app is simple, but the tooling around it is pretty impressive, and it's not that hard to set up.

Shout Out

The pointing sound board app is created off of a boiler plate I put together to be the foundation for such applications. It's called vue-parcel-starter, and it simplifies a couple of the minor things I had to do to get the tooling I wanted working harmoneously. I also published an npm initializer, so you can create an instance of the latest of that starter file with either:

npm init vue-parcel <app-name>

or

npx create-vue-parcel <app-name>

Summary

I hope this has been informative, or at least a bit insightful, as to how to go about setting things up. I didn't cover some of the other PWA basics, such as a manifest JSON file, meta headers in your index.html, or icon files. Otherwise, it's all there and you can find that other information in my example repository.

Until next time, let's build better apps!