Dynamic Cache Name As A Static Value
Updated: Dec 13, 2024
If you have ever engineered a Progressive Web App (PWA), you're well aware that the service-worker.js file is at the heart of this effort. It really does control the offline-capable aspects of your PWA.
The beating heart inside this beating heart is the Cache Storage functionality. The files you want available, even though there's no network connection. Logic tells us that when you update/re-build your site/app, this browser cache should be updated as well, and a versioned cacheName does just that.
On To The Show
To begin this discussion, you could ideally update the cacheName, which is declared right away in your service-worker.js file sent to the browser.
service-worker.jslet cacheName = "myAppCache-v1";
But updating the cacheName version string manually every time there's an update is a giant opportunity to forget all about it. How about a dynamic version name, something like this?
service-worker.jslet cacheName = "myAppCache-qkGOtAv";
Sure seems like a good practice, and with three lines of code, easy and painless:
service-worker.jslet result = Math.random().toString(36).substring(2,9);
result.toString();
let cacheName = `cacheName-${result}`;
Done, right? Oh no! For you see, the browser starts executing your service-worker.js file from the top every time it has service worker lifecycle stuff to do (install, activate, fetch), so it follows that a new cacheName, per the above code, is generated on every service worker type execution in the browser, not just every time you build your site.
The latter being your desired behavior seems to me.
Eleventy and NodeJS are quite capable of generating a dynamic cacheName, but sending to the browser a static cacheName, nonetheless re-versioned. Let's start by creating in the directory of your choosing/creation a file called cache-name.js, and filling it thusly:
cache-name.jslet newName = () => {
// declare all characters
const characters ='ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
let result = ' ';
const charactersLength = characters.length;
for ( let i = 0; i < 7; i++ ) {
result += characters.charAt(Math.floor(Math.random() * charactersLength));
};
// just to be safe
result.toString();
// very curious, but the generated string has a leading whitespace
// so....
// get rid of it!
let resultOne = result.trimStart();
let cacheName = 'myAppCache-' + resultOne;
return cacheName
};
// this thing is now importable from another file
module.exports = newName();
In the same directory, create a file called service-worker-base.js. This is where you define all the service worker behavior typically used. I'll just outline it here as the details are a dark and scary rabbit hole.
service-worker-base.js.
.
self.addEventListener('install', event => {});
.
.
self.addEventListener('activate', event => {});
.
.
self.addEventListener('fetch', event => {});
.
.
Now we're set to bring this all together and create a service worker file that's usuable by the browser. Finally, in the same directory, create a NodeJS file called service-worker-create.js.
service-worker-create.jsconst fs = require('fs');
// your location here
const cache = require('./cache-name.js');
function dealWithContent(callback) {
// read the service-worker-base file contents
fs.readFile('src/my/path/service-worker-base.js', 'utf8', (err, data) => {
if(err) {
throw err;
} else {
// if all's well, send the file content with this callback function
// to be put to use below
callback(null, data);
};
});
};
dealWithContent((err, content) => {
// a template literal to mash the two data sources into one document:
// the dynamically created but now static versioned cacheName
// and the service worker lifecycle instructions retreived from service-worker-base.js
let newServiceWorkerFile = `
(() => {
"use strict";
let cacheName = ${cache};
${content};
})();
`;
// write the new service worker file to your root directory, or wherever you want
// tell .eleventy.js where to find it, with an addPassthroughCopy to get it into your production directory
fs.writeFile('./my-service-worker.js', newServiceWorkerFile, (err) => {
if (err) {
console.log('error writing:', err);
} else {
console.log('success writing');
}
});
});
NodeJS and npm will do the heavy lifting for you when you build the updated version of your site.
package.json"scripts": {
"serviceworker:create": "node src/my/path/service-worker-create.js",
"serviceworker:min": "terser my-service-worker.js -o my-service-worker-min.js",
"build:start": "rm -rf dist && ELEVENTY_RUN_MODE=build npx @11ty/eleventy",
"build": "run-s serviceworker:create serviceworker:min build:start"
}
And presto! A dynamically generated cachName value shipped to the browser as a static value in your service worker file. Send it!
So To Recap
- The service worker technology is full of dark and mysterious gotchas
- A few simple javascript files helps solve one of them
- A well-oiled PWA is worth it
- Eleventy and NodeJS are a powerful problem-solving duo.
TOP