PWAs and offline use
A progressive web app, PWA for short, is a web app that behaves like an installed app. Two files make the difference. The manifest describes the name, icons and colors of the app, so the browser and the operating system can install it and start it in a window of its own. The service worker is a script that sits between the app and the network. It answers requests from a cache when the network is gone.
In your project
Section titled “In your project”According to the project assignment, a PWA that works offline earns one bonus point. It pays off for an app people use on the go, for example a list someone opens in a basement without reception. Offline usually means that the app starts and shows the data it loaded last. Sending new data while the network is gone takes a lot more work. A service worker only runs in a secure context, which means HTTPS or localhost, and GitHub Pages serves HTTPS.
A small example
Section titled “A small example”The example under optional/pwa/en is a small catalog built with Vite. The manifest, the service worker and the icons live in the folder public, so Vite copies them into the build unchanged. The addresses in the manifest are relative, which lets the app also work below a base path.
{ "id": "./", "name": "Lorraine Commons, catalog", "short_name": "Lorraine Commons", "lang": "en", "start_url": "./", "scope": "./", "display": "standalone", "background_color": "#ffffff", "theme_color": "#1f6b4f", "icons": [ { "src": "icon-192.png", "sizes": "192x192", "type": "image/png" }, { "src": "icon-512.png", "sizes": "512x512", "type": "image/png" }, { "src": "icon-512.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable" }, { "src": "icon.svg", "sizes": "any", "type": "image/svg+xml" } ]}When it installs, the service worker stores the shell of the app in a cache, which means index.html and the manifest. When it activates, it deletes its own caches from an older version.
self.addEventListener('install', (event) => { event.waitUntil(caches.open(CACHE).then((cache) => cache.addAll(SHELL)))})
// A new version removes the old caches of this app and leaves those of other apps aloneself.addEventListener('activate', (event) => { event.waitUntil( caches .keys() .then((keys) => Promise.all(keys.filter((key) => key.startsWith(PREFIX) && key !== CACHE).map((key) => caches.delete(key)))), )})From then on, every request of the app passes through the listener for fetch. Pages, which are requests with the mode navigate, come from the network first and from the cache when offline. Since an SPA needs the same index.html for every address, the service worker always takes that file when offline. Data under /api/ also comes from the network first, so it stays current. The files of the app only change with a new build, so they come from the cache first.
self.addEventListener('fetch', (event) => { const { request } = event if (request.method !== 'GET' || new URL(request.url).origin !== self.location.origin) return
// Every address of the SPA is answered by index.html, offline from the cache if (request.mode === 'navigate') { event.respondWith(networkFirst(request, './index.html')) } else if (new URL(request.url).pathname.includes('/api/')) { event.respondWith(networkFirst(request)) } else { event.respondWith(cacheFirst(request)) }})The app only registers the service worker in the build. During development it would serve old files from the cache, and you would not see your changes. So try the example with npm run build and npm run preview.
// The built app registers the service worker below its base path. During development it would// keep old files in the cache, so npm run dev leaves it out.export function registerServiceWorker() { if (!import.meta.env.PROD || !('serviceWorker' in navigator)) return window.addEventListener('load', () => { navigator.serviceWorker.register(`${import.meta.env.BASE_URL}sw.js`, { scope: import.meta.env.BASE_URL }) })}The tests in tests/sw.test.js run the service worker in Node with small stand-ins for self, caches and fetch. That way they check without a browser that an address gets index.html when offline and that files come from the network only once.
Chrome and Edge offer installation when the manifest has a name, icons of 192 and 512 pixels, start_url and display, and the page comes over HTTPS. Safari puts any website into the Dock of a Mac or onto the Home Screen of an iPhone on request, and desktop Firefox does not install PWAs from a manifest. All apps of the same origin also share their caches, for example all projects under one GitHub Pages account. That is why the service worker recognizes its own caches by the prefix catalog- and deletes only those. If you want to put many files into the cache in advance, let vite-plugin-pwa generate the service worker, which builds on Workbox.
Official links
Section titled “Official links”Text and figures are licensed under CC BY-SA 4.0, the code under the MIT License and the course data under CC0 1.0. The figures use the font Kalam under the SIL Open Font License 1.1. The sources are on GitHub.