Service Workers for offline web apps
I recently worked on getting my web tools to also work offline.
While there are tools like Workbox and also a wrapper for Workbox for vite, those feel quite "big", and config heavy.
My tools mostly don't feel like a PWA to me, that you would install on your phone.
So I wanted to try it myself, and learn about service workers.
About the code:
- it is a vite/rollup plugin, that gathers all the output files during the build, and puts them into the service worker code
- the cache name is made from the hashes filenames (vite also adds filehashes in them, so the cache name should change, along with code changes)
- add the worker install to the index.html file
import { createHash } from "node:crypto";
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { type Plugin } from "vite";
const getServiceWorkerCode = (
cacheName: string,
fileToCache: string[],
): string => {
const builder: string[] = [];
builder.push(`const cacheName = "${cacheName}";`);
builder.push(`self.addEventListener('install', (e) => {
console.info('[Service Worker] Install');
e.waitUntil((async () => {
const cache = await caches.open(cacheName);
console.info('[Service Worker] Caching all: app shell and content');
await cache.addAll(${JSON.stringify(fileToCache)});
})());
});`);
builder.push(`self.addEventListener('fetch', (e) => {
// Cache http and https only, skip unsupported chrome-extension:// and file://...
if (!(
e.request.url.startsWith('http:') || e.request.url.startsWith('https:')
)) {
return;
}
e.respondWith((async () => {
const r = await caches.match(e.request);
console.info(\`[Service Worker] Fetching resource: \${e.request.url}\`);
if (r) return r;
const response = await fetch(e.request);
const cache = await caches.open(cacheName);
console.info(\`[Service Worker] Caching new resource: \${e.request.url}\`);
cache.put(e.request, response.clone());
return response;
})());
});`);
return builder.join("\n");
};
export const OfflineServiceWorkerPlugin = (
options: { projectName: string },
): Plugin => {
return {
name: "offline-service-worker-plugin", // this name will show up in logs and errors
enforce: "post",
apply: "build",
transformIndexHtml: {
order: "post",
handler: () => {
return [{
tag: "script",
children: `if ("serviceWorker" in navigator) {
navigator.serviceWorker.register("/sw-${options.projectName}.js", {scope: "/${options.projectName}"});
}`,
injectTo: "body-prepend",
}];
},
},
generateBundle: (bundleOptions, bundle) => {
// const project = options.dir.split("/").at(-1);
const files = Object.keys(bundle);
const buildHash = createHash("md5").update(files.join(",")).digest(
"hex",
).slice(0, 8);
if (!existsSync(bundleOptions.dir)) {
mkdirSync(bundleOptions.dir, { recursive: true });
}
writeFileSync(
join(bundleOptions.dir, "../", `sw-${options.projectName}.js`),
getServiceWorkerCode(
buildHash,
files,
),
);
},
};
};
Add this to your repo, and your vite app's config in plugins. Then it should also work for your projects
Some things I learned:
- Debugging issues with the service worker install is annoying. Firefox didn't give me much in the console. Chromium was slightly better
- service worker can only apply to pages at the same folder depth, or deeper
- I ended up putting the service workers at the root. While the apps
index.htmlwas in the same folder as the service worker, this didn't work, when the page url ended up being/tool, rather than/tool/. Since/tool/sw.jsis then deeper than/tool - and again, the browser may or may not tell you this ':)
- I ended up putting the service workers at the root. While the apps
- even though the service worker was then in a different folder, the asset urls then still need to be relative to the current page
- so, keep the asset path in your service worker relative to the
index.html - and don't care about the path relative to the service worker file
- I didn't add the service worker to its own cache, and I think you're also not supposed to :D
- so, keep the asset path in your service worker relative to the
You can try the result with all my tools, for instance here with the comic reader.
(I mostly wanted offline support for this one :D To read comics in trains, which sometimes don't have internet)