Announcing Spartan Obstacles 🏃: A Site to Help Normal People Have More Fun At OCR and An Example of a PWA Built Using Workbox 🔨
Today I am pretty excited to finally announce Spartan Obstacles is live!
This is a personal passion project for me and one that is way overdue. I started playing with the concept of the brand over a year ago and finally feel like I have a production ready enough site to announce.
The Spartan Obstacles brand is more than just a web site, it is a collection of content and media channels that also includes a YouTube channel, Facebook, Twitter and Instagram.
To top it all off I have also released a new book, The Normal Guy's Guide to Obstacle Course Races. The book is the product of helping other Spartans and OCR participants with questions about races and how to do obstacles.
So what does this have to do with web development and progressive web apps?
Content wise, nothing really, except I hold the personal belief that developers should make intentional efforts to be fit and in shape. Spartan Races and obstacle course racing is a great way to motivate anyone to get in shape and stay in shape.
A Progressive Web App
The site is a progressive web app, I mean would you expect anything less from me?
As with all PWAs the site is served via HTTPS. If your site is not using HTTPS then you need to immediately stop reading this article and upgrade now!
There is also a web manifest file with a complete set of site icons.
The Service Worker
This is the first public site I have shipped with a service worker generated from Workbox. Workbox is a tool created by the Google Chrome team to help developers quickly write complex service workers.
If you know me I am not normally a fan of using other people's code. Large frameworks like Workbox almost never meet my requirements and I spend more time hacking a real solution than it would take to write it from scratch.
But that's not the case here. Especially since this JavaScript does not run in the UI thread and therefor does not interfere with the user experience.
So why Workbox?
I have written some complex service workers over the past couple of years. The more I wrote those service workers I would check on Workbox and its predecessors, sw_toolbox and sw_precache.
I found the predecessors less than helpful. It was hard to bend these tools to my application requirements. Basically I would write the code I needed more efficiently than the tools could generate. See, I meant what I previously said.
Workbox started that way, but has improved in the past year. I started playing with it as I finished my upcoming PWA book (read more). The last chapter features different tools to help build good progressive web apps. Workbox is the last tool I demonstrate.
The scaffolding experience is useful, but you still need to tweak the generated code to make the service worker do what you need. The scaffolding it more to generate a list of URLs to precache with a corresponding hash value.
I am rather fond of the way Workbox service workers handle precache because it is clean and elegant. One of the more common issues with service worker pre-caching is keeping those responses current.
I like to cache as many common pages and site assets as possible in the precache. But those files will update from time to time. So how do you keep your precache responses current?
The most common way is to ship a new service worker, so the install event is triggered. I teach new service worker programmers to just create a new named cache and delete the old one when the new service worker is activated.
This is a very scorched earth approach and not very efficient, especially in the mobile context.
The way I gravitated toward was maintaining a manifest file, specifying what files to cache and a corresponding hash value. I added logic to my service workers to periodically check this cache file to see what files need to be updated. IndexedDB is used to persist the manifest so updates can compare the hash values to identify what URLs need to be refreshed.
This is a pretty good first pass at solving this issue. After using it a few times I started working on something a little more sophisticated where I would check against the server without needing a separate manifest file. I have not quite perfected this technique.
Fortunately Workbox has. I analyzed how they do the verification and it very close to what I was working toward. Honestly, they have more time and resources to complete the solution. And of course they made it free!
Configuring Workbox
You are free to code directly against the Workbox library, and you will need to do that for your dynamic routes. But for pre-caching I think it wiser to use the command line tool and workbox-build module. The build tool is a node module you can use as part of your build process and with task runners like Grunt, Gulp or WebPack.
Within your service worker you need the following line:
workbox.precaching.precacheAndRoute([]);
This serves as a target for the build tool to inject a fresh list of resources to cache with their corresponding hash values.
I build/render my sites using a custom node script, well AWS lambdas, so the workbox-build module is perfect. The only issue running inside a Lambda function is needing to keep a copy of the source files in the lambda. I wont complicate matters with dealing with that for now. Let's just focus on the code!
const buildSW = () => { // This will return a Promise return workboxBuild.injectManifest({ "globDirectory": "../public/www", "globPatterns": [ "***.*", "img/**/*.*", "race/**/*.*", "meta/**/*.*", "workbox-v3.4.1/**/*.*", "sw/**/*.*", "shop/**/*.*", "js/libs/polyfil/*.*", "font/**/*.*", "sw.js", "sw-generated.js" ], "swSrc": '../public/src/sw-src.js', "swDest": "../public/www/sw.js" }).then(({ count, size, warnings }) => { // Optionally, log any warnings and details. warnings.forEach(console.warn); console.log(`${count} files will be precached, totaling ${size} bytes.`); }); }
Here you can see the injectManifest configuration I use to create a list of files to precache. The globDirectory points to the source folder or the location of your site's files. The globPatterns array is a regular expression the tool uses to match files that can be precached.
You don't want to precache every file in the site. If you do you will wind up with many megabytes of responses cached unnecessarily. Remember there is a constraint or limit to how much space you have cache responses. Here you can see the report generated by the CLI indicating how large all the files combine to be, way too much!
Instead you need to limit the files to be cached. This is the globIgnores property. This is an array of regular expressions corresponding to explicit files, folders and other patterns to exclude. Now you only have the files you really need.
The last two properties tell the tool where the source service worker template is located and where the final service worker should be written.
The final output looks something like the following:
workbox.precaching.precacheAndRoute([ { "url": "404/index.html", "revision": "2b0d50c334089f87cbbb8444b1505bcb" }, { "url": "about/index.html", "revision": "774ed84fd867b59f38c8254807ee93f4" }, { "url": "blog/index.html", "revision": "580cbeb154492f3feba9401d80a20837" }, { "url": "book/index.html", "revision": "a235d6e6452e8eea26c5a59f2cfa9b51" }, { "url": "contact/index.html", "revision": "1cc978f4178e0e48b8bbe1373c30df57" }, ... ]
Now when you deploy the final assets to the server you have a fresh service worker that will only refresh files that have been updated since they were last cached.
Handling Dynamic Routes in Workbox
Precaching requires a bit more finesse than dynamic routes, which seems counter intuitive. But dynamic caching is very simple. It is also handled by configuration.
workbox.routing.registerRoute( new RegExp('/race/'), workbox.strategies.cacheFirst({ cacheName: 'races', plugins: [ new workbox.expiration.Plugin({ maxEntries: 40, maxAgeSeconds: 60 * 60 * 24, // 1 Day }) ] }) );
In the example code you see how to register a dynamic route and customize the named cache and how invalidation is handled. There are really two ways to limit how long responses are cached, by the number of responses in a cache and how long they have been cached.
Invalidation is managed by the workbox expiration plugin. I wont go into the details of how to create a plugin here, but the architecture is designed to be very extensible.
Here I use a combination of both volume and time. No more than 40 responses and no longer than 24 hours. If a request is made for a cached response and either of those limits has been exceeded then a network request is made and the fresh response is cache according to the desire strategy.
The rest of the registration is the registerRoute method, which has a couple of parameters, a route or regular expression and a strategy. The example is for any page prefixed with '/race/', this is because I have a page for each upcoming Spartan Race on the site and they are stored in the race folder.
Workbox has several caching strategies built-in, here is the cacheFirst strategy. This is where the request is intercepted and the service worker checks to see if there is a cached response available. If there is one the cached version is returned. If not, then the network request is made and the response cached.
Workbox includes 5 common caching strategies:
- Cache First
- Cache Only
- Network First
- Network Only
- Stale While Revalidate
I go over these and about 15 more in my Progressive Web App course and most in the book. These five I consider the foundation to other strategies.
Most of the time the Cache First strategy is the one I go with. Of course every scenario needs evalaution to pick the best strategy. This is why you want to create unique named caches and assign rules to specific route patterns. It allows the most flexibility when crafting an application's caching strategy.
Summary
Workbox is a very robust library that makes creating very complex service workers much easier. Workbox only manages cache related concerns, it does not have any logic built in for push notifications. It does make doing background sync much easier. I have stayed away from background sync for several reasons, one is the extra layers of complexity it adds to your logic. Workbox abstracts those complications away.
As with any powerful tool, like Workbox, I encourage you to write your service workers without the use of Workbox when you are new to service workers. I always take the approach that you need to have intimate knowledge with what is happening behind the library or tool. That way you can use and maintain (troubleshoot) the tool better.
The Workbox documentation is well written and structured. I can't emphasize this enough. Good documentation is one of the reasons jQuery gained widespread popularity over 10 years ago. Most libraries, modules and frameworks suffer from poor documentation that not only is hard to follow, but often out of sync with the library. I can't tell you how many times bad documentation has ruined a popular tool for me.
After you feel comfortable with service worker caching then try using Workbox. I feel confident enough with how service worker caching works I could use my own code and create a lot of custom code for each application, but Workbox makes things a lot easier. I think this is one large library I can get behind because it delivers more than promised.