Sitemap
Yaron Biton

Yaron Biton

Everything Web. misterBIT.co.il CTO & Coding-Academy.org Chief Instructor

Progressive Web Apps: One App for Every Platform

7 min read1 day ago

--

Manifest, service workers, offline support and push notifications, and how Vite gives you all of it almost for free

What is a PWA?

A Progressive Web App is a mobile web app that feels like a native app. It installs to the home screen, opens full screen, works offline and can send push notifications, while still being a website.

The manifest

It starts with a single line in the HTML:

<!DOCTYPE html>
<html lang="en">
<head>
<title>Service Worker Demo</title>
<meta name="viewport"
content="width=device-width, initial-scale=1.0, user-scalable=no, viewport-fit=cover">
<link rel="manifest" href="manifest.json">
</head>
<body>
<h1>Service Worker Demo</h1>
<img src="img/van.jpg" alt="">
<script src="js/main.js"></script>
</body>
</html>

Here is the basic structure of a manifest file:

{
"id": "/",
"name": "My awesome app",
"short_name": "MyApp",
"start_url": ".",
"display": "standalone",
"background_color": "#fff",
"description": "A simple demo app",
"icons": [
{
"src": "img/icons/logo48.png",
"sizes": "48x48",
"type": "image/png"
},
{
"src": "img/icons/logo144.png",
"sizes": "144x144",
"type": "image/png"
}
]
}

That’s enough to make the app installable. The browser offers to install it:

Press enter or click to view image in full size

Once installed, it runs in its own window, like any other app:

Press enter or click to view image in full size

You can inspect the manifest in Chrome DevTools, under Application:

Press enter or click to view image in full size

Offline support

A PWA can also work offline. When the user installs the app, its files are saved in the browser cache, so even when they’re not online, they can still open it. This is done with a service worker.

Press enter or click to view image in full size

What is a service worker?

  • A thread run by the browser, in the background
  • It intercepts network requests
  • It can access the browser’s HTTP cache
Press enter or click to view image in full size

Registering a service worker

// main.js
navigator.serviceWorker.register('../sw.js')
.then(reg => console.log('Service Worker registered', reg))
.catch(err => console.error('Service Worker registration failed', err))

The registered service worker shows up in DevTools:

Press enter or click to view image in full size

The install event

When the service worker installs, we add all the app files to the cache:

// sw.js
self.addEventListener('install', event => {
const urlsToCache = [
'index.html',
'js/main.js',
'img/van.jpg',
'img/icons/logo48.png',
'https://fonts.googleapis.com/css?family=Barrio',
'https://fonts.gstatic.com/s/barrio/v1/x.woff2'
]
event.waitUntil(
caches.open('my-cache').then(cache => {
return cache.addAll(urlsToCache)
})
)
})

Now the app is cached:

Press enter or click to view image in full size

The fetch event

Every request the app makes now goes through the service worker. Here we use a cache-first strategy: answer from the cache, and go to the network only when we have to:

self.addEventListener('fetch', event => {
event.respondWith(
caches.match(event.request).then(response => {
if (response) {
console.log(event.request.url, 'found in cache')
return response
}
console.log(event.request.url, '- Not in cache, fetching from network')
return fetch(event.request)
})
)
})
Press enter or click to view image in full size

And our app works offline:

Press enter or click to view image in full size

Offline data

We can also keep some data locally and show it when offline. A service worker can load data into the cache, but localStorage works too:

if (navigator.onLine) {
console.log('Online - get from network')
} else {
console.log('Offline - get from localStorage')
}

No DOM access

The service worker runs in another thread, and only the main thread can access the DOM. When the service worker needs the page updated, it sends a message.

Both the app and the service worker can post messages to each other:

// App listens to messages from the SW
navigator.serviceWorker.addEventListener('message', (ev) => {
console.log('Got reply from service worker: ' + ev.data)
document.querySelector('pre').innerText += ev.data + '\n'
})

// App sends a message to the SW
function onSend() {
if (navigator.serviceWorker.controller) {
console.log(`Sending 'hi' to SW`)
navigator.serviceWorker.controller.postMessage('hi')
} else {
console.log('No SW to talk to')
}
}
// Service worker
self.addEventListener('message', ev => {
console.log('SW got msg:', ev.data)
ev.source.postMessage(17)
})
Press enter or click to view image in full size

Push notifications

The Push API lets a PWA receive messages pushed from a server, even when the app isn’t currently loaded. Users who opt in get timely notifications and updates, which means better engagement with new content.

The service worker listens for the push event:

self.addEventListener('push', (ev) => {
console.log('Got a PushEvent, text:', ev.data.text())
})

You can test it from DevTools, by pushing a message to the service worker:

Press enter or click to view image in full size

Getting permission

We need the user’s permission for push notifications, and we keep their subscription token on the backend:

// Code is abbreviated (missing some parts)
function requestUserToInstallAndSaveSubscription() {
registration.pushManager.subscribe({
applicationServerKey: 'xxx'
})
.then(subscription => {
// Send and keep the subscription at the backend
fetch('/api/pushSubscription', {
method: 'POST',
headers: {
'Content-type': 'application/json'
},
body: JSON.stringify({ subscription })
})
})
}

Showing the notification

The server sends pushes with a library like web-push. If the user allows it, the service worker displays the notification on the device and brings them back to the app:

self.addEventListener('push', function(event) {
const payload = event.data ? event.data.text() : 'no payload'
// Keep the service worker alive until the notification is created
event.waitUntil(
// (Needs user permission for notifications)
self.registration.showNotification('My ServiceWorker', {
body: payload,
})
)
})

What makes a PWA

  • Installable: saved to the home screen
  • Discoverable: easily found
  • Linkable: pages accessible via URL, with deep linking
  • Connectivity tolerant: works offline or on a poor connection
  • Re-engageable: push notification support

The basic checklist

  • manifest.json contains the info for Add to Home Screen.
  • The start URL (at least) loads fast, also when offline.
  • Pages are responsive on tablets and mobile devices.
  • Page transitions don’t feel like they block on the network.
  • The site is served over HTTPS (localhost is permitted).

The full power

PWAs can do much more than that: notifications, contact picker, media capture, barcode detection, authentication, vibration, geolocation, Bluetooth, payments and more. Here are some of the features available to a web app today:

Press enter or click to view image in full size
Press enter or click to view image in full size

PWA vs native apps

Instead of writing different apps for different devices, we write one PWA that works on all platforms.

Get Yaron Biton’s stories in your inbox

Join Medium for free to get updates from this writer.

And the really good news? With modern frontend frameworks, we get all of this (almost) for free.

The PWA Vite plugin

Press enter or click to view image in full size

Install it:

npm install -D vite-plugin-pwa

And add it to the Vite config:

// vite.config.js
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react-swc'
import { VitePWA } from 'vite-plugin-pwa'

export default defineConfig({
plugins: [
react(),
VitePWA({ registerType: 'autoUpdate' })
],
})

Build the app, and the plugin generates the manifest and a service worker that precaches all the build files:

Press enter or click to view image in full size

Serve the build, switch DevTools to offline, and the app still works:

Press enter or click to view image in full size

The mobile experience

On the phone, the user adds the app to the home screen, and from then on it opens like any native app:

Summary

  1. A PWA is a web app that feels native: installable, offline-capable and re-engageable.
  2. manifest.json makes it installable.
  3. A service worker caches the app files and intercepts requests, so it works offline.
  4. The service worker has no DOM access; it talks to the page with messages.
  5. The Push API brings users back with notifications, with their permission.
  6. With Vite, vite-plugin-pwa gives you all of this almost for free.

Yaron Biton, MisterBit CTO

--

--