Building PWA Features
Offline Capabilities
Creating resilient experiences that work without a network:
Offline-First Approach:
- Design assuming no connectivity
- Cache critical resources during installation
- Provide meaningful offline experiences
- Sync data when connectivity returns
- Clear feedback about connection status
Example Offline Page:
<!-- offline.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>You're Offline - Weather PWA</title>
<link rel="stylesheet" href="/styles/main.css">
<style>
.offline-container {
text-align: center;
padding: 2rem;
max-width: 600px;
margin: 0 auto;
}
.offline-icon {
font-size: 4rem;
margin-bottom: 1rem;
color: #888;
}
</style>
</head>
<body>
<div class="offline-container">
<div class="offline-icon">📶</div>
<h1>You're currently offline</h1>
<p>The Weather PWA requires an internet connection to show the latest weather data.</p>
<p>Please check your connection and try again.</p>
<div id="last-updated-info">
<p>Last data update: <span id="last-updated">Unknown</span></p>
</div>
<button id="retry-button" class="button">Retry Connection</button>
</div>
<script>
// Check if we have cached data
if ('caches' in window) {
caches.match('/api/last-weather-update')
.then(response => {
if (response) {
return response.json();
}
return null;
})
.then(data => {
if (data && data.timestamp) {
const date = new Date(data.timestamp);
document.getElementById('last-updated').textContent = date.toLocaleString();
}
});
}
// Retry button
document.getElementById('retry-button').addEventListener('click', () => {
window.location.reload();
});
// Listen for online status changes
window.addEventListener('online', () => {
window.location.reload();
});
</script>
</body>
</html>
IndexedDB for Offline Data:
// Using IndexedDB for offline data storage
class WeatherDataStore {
constructor() {
this.dbPromise = this.initDatabase();
}
async initDatabase() {
return new Promise((resolve, reject) => {
const request = indexedDB.open('weather-pwa-db', 1);
request.onerror = event => {
reject('IndexedDB error: ' + event.target.errorCode);
};
request.onsuccess = event => {
resolve(event.target.result);
};
request.onupgradeneeded = event => {
const db = event.target.result;
// Create object stores
const forecastStore = db.createObjectStore('forecasts', { keyPath: 'locationId' });
forecastStore.createIndex('timestamp', 'timestamp', { unique: false });
const locationStore = db.createObjectStore('locations', { keyPath: 'id' });
locationStore.createIndex('name', 'name', { unique: false });
};
});
}
async saveForecast(locationId, forecastData) {
const db = await this.dbPromise;
const tx = db.transaction('forecasts', 'readwrite');
const store = tx.objectStore('forecasts');
const data = {
locationId,
forecast: forecastData,
timestamp: new Date().getTime()
};
await store.put(data);
return tx.complete;
}
async getForecast(locationId) {
const db = await this.dbPromise;
const tx = db.transaction('forecasts', 'readonly');
const store = tx.objectStore('forecasts');
return store.get(locationId);
}
}
Push Notifications
Engaging users with timely updates:
Push Notification Workflow:
- Request user permission
- Subscribe to push service
- Send subscription to server
- Server sends push message
- Service worker receives push event
- Service worker shows notification
Example Push Notification Implementation:
// Request permission and subscribe to push notifications
async function subscribeToPushNotifications() {
try {
// Request permission
const permission = await Notification.requestPermission();
if (permission !== 'granted') {
throw new Error('Permission not granted for notifications');
}
// Get service worker registration
const registration = await navigator.serviceWorker.ready;
// Get push subscription
let subscription = await registration.pushManager.getSubscription();
// Create new subscription if one doesn't exist
if (!subscription) {
// Get server's public key
const response = await fetch('/api/push/public-key');
const { publicKey } = await response.json();
// Convert public key to Uint8Array
const applicationServerKey = urlBase64ToUint8Array(publicKey);
// Subscribe
subscription = await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey
});
}
// Send subscription to server
await saveSubscription(subscription);
return subscription;
} catch (error) {
console.error('Failed to subscribe to push notifications:', error);
throw error;
}
}
// In service worker
self.addEventListener('push', event => {
const data = event.data.json();
const options = {
body: data.body,
icon: '/images/notification-icon.png',
badge: '/images/badge-icon.png',
vibrate: [100, 50, 100],
data: {
url: data.url
}
};
event.waitUntil(
self.registration.showNotification(data.title, options)
);
});
Best Practices for Push Notifications:
- Request permission at appropriate times
- Make notifications relevant and timely
- Provide clear opt-out mechanisms
- Use rich notifications when appropriate
- Respect user preferences and time zones
- Test across different platforms
App Installation
Enabling home screen installation:
Installation Process:
- Browser checks if app is installable
- User triggers installation (browser UI or custom button)
- Browser shows installation prompt
- User confirms installation
- App installed on device
Installation Criteria:
- Valid web app manifest
- Served over HTTPS
- Registered service worker
- Icon defined in manifest
- Display mode not “browser”
- Meets engagement heuristics (varies by browser)
Custom Install Button:
// Track installation status
let deferredPrompt;
const installButton = document.getElementById('install-button');
// Initially hide the install button
installButton.style.display = 'none';
// Listen for beforeinstallprompt event
window.addEventListener('beforeinstallprompt', (event) => {
// Prevent default browser install prompt
event.preventDefault();
// Store the event for later use
deferredPrompt = event;
// Show the install button
installButton.style.display = 'block';
});
// Handle install button click
installButton.addEventListener('click', async () => {
if (!deferredPrompt) {
return;
}
// Show the install prompt
deferredPrompt.prompt();
// Wait for user response
const { outcome } = await deferredPrompt.userChoice;
console.log(`User response to install prompt: ${outcome}`);
// Clear the deferred prompt
deferredPrompt = null;
// Hide the install button
installButton.style.display = 'none';
});