A Very Simple Service Worker
Here’s a recent example of how I got Clinic Helper to work offline in less than fifty lines of JavaScript, no fancy-pants libraries, and no build steps.
let version = "1"
let cacheName = `v${version}_data`
let cachedAssetPaths = [
// …
]
// install (pre-activation) event
async function install() {
let cache = await caches.open(cacheName)
for (let p of cachedAssetPaths) {
try {
await cache.add(p)
} catch ({name, message}) {
console.error(`SW ${version}: failed to pre-download asset @ ${p}`)
} finally {
continue
}
}
}
self.addEventListener('install', event => {
self.skipWaiting()
event.waitUntil(install())
})
// activation (post-installation) event
async function activate() {
let allCaches = await caches.keys()
let badCaches = allCaches.filter((key) => { return key != cacheName })
for (let c of badCaches) {
caches.delete(c)
}
await self.clients.claim()
}
self.addEventListener('activate', (e) => {
e.waitUntil(activate())
})
// fetch (every request)
self.addEventListener('fetch', async (e) => {
// https://stackoverflow.com/a/49719964
if (e.request.cache === 'only-if-cached' && e.request.mode !== 'same-origin') return
// match in cache
let match = await caches.match(e.request)
if (match) e.respondWith(match)
// fall back to network
e.respondWith(await fetch(e.request))
})
There she is in all her glory. Forty nine measly lines.
To get it plumbed in to your site, add this to your index.html
:
<script>
if ('serviceWorker' in navigator) {
const registration = navigator.serviceWorker.register('/sw.js')
}
</script>
Lessons Learned
.waitUntil()
is more like .waitForThisPromiseToResolve()
All of the example code I could find online made liberal use of .then()
and .catch()
synax, which I was keen to translate into modern async/await
code. My first few attemps looked a bit like this:
self.addEventListener('install', event => {
self.skipWaiting()
event.waitUntil(async () => {
let cache = await caches.open(cacheName)
cache.addAll(cachedAssetPaths)
})
})
I was very frustrated to find that my code wasn’t being called at all.
Turns out, .skipWaiting()
takes a Promise
, not a function. I solved my problem by factoring install()
and activate()
into standalone async
functions and calling them from .waitUntil()
without the await
keyword to ensure I got a Promise
, not the return type of install()
.
Beware .addAll()
The .addAll()
is concise but there’s a catch: a single 404
nukes the whole caching process. I opted to iterate over the paths manually so I could catch errors without halting the installation altogether.
In Production
You can find the version of this service worker that I used in production over on GitHub.