Framework7 PWA and Service Workers — Offline Support and App Installation
In this tutorial, you will learn about Framework7 PWA and Service Workers. We cover key concepts, practical examples, and best practices to help you master this topic.
Framework7 integrates Progressive Web App features — service worker registration, Caching strategies, offline support, manifest generation, and install prompts — for native-like mobile experiences.
What You'll Learn
- Service worker registration in Framework7
- Caching strategies for app assets
- Offline fallback pages
- Web app manifest configuration
- Push notifications
- PWA deployment
Why It Matters
PWAs bridge the gap between web and native apps — installable on the home screen, working offline, and receiving push notifications. Framework7's PWA integration makes these features configurable with minimal code.
Real-World Use
An e-commerce PWA that works offline (viewing products from cache), sends push notifications for order updates, and can be installed on the phone's home screen with a custom splash screen.
PWA Architecture
flowchart TD
A[PWA] --> B[Service Worker]
A --> C[Manifest]
A --> D[Cache]
A --> E[Push]
B --> F[Install]
B --> G[Activate]
B --> H[Fetch]
C --> I[App Name]
C --> J[Icons]
C --> K[Splash]
D --> L[Assets]
D --> M[Pages]
D --> N[API Data]
style A fill:#e6f3ff,stroke:#4a90d9,stroke-width:2px
Service Worker Registration
// Framework7 provides built-in service worker registration
var app = new Framework7({
root: '#app',
serviceWorker: {
path: '/sw.js',
scope: '/'
}
});
// Or manual registration
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js')
.then(function(registration) {
console.log('SW registered:', registration.scope);
})
.catch(function(error) {
console.error('SW registration failed:', error);
});
}
// Service worker status
if (app.serviceWorker && app.serviceWorker.active) {
console.log('Service worker is active');
}
Expected output: The service worker registers on app initialization. Subsequent visits use the service worker to serve cached assets.
Service Worker Script
// sw.js - Service worker
var CACHE_NAME = 'my-app-v1';
var urlsToCache = [
'/',
'/index.html',
'/css/app.css',
'/js/app.js',
'/images/logo.png',
'/fonts/',
'/pages/home.html',
'/pages/offline.html'
];
// Install: cache app shell
self.addEventListener('install', function(event) {
event.waitUntil(
caches.open(CACHE_NAME)
.then(function(cache) {
console.log('Cache opened');
return cache.addAll(urlsToCache);
})
.then(function() {
return self.skipWaiting();
})
);
});
// Activate: clean old caches
self.addEventListener('activate', function(event) {
event.waitUntil(
caches.keys().then(function(cacheNames) {
return Promise.all(
cacheNames.map(function(cacheName) {
if (cacheName !== CACHE_NAME) {
console.log('Deleting old cache:', cacheName);
return caches.delete(cacheName);
}
})
);
}).then(function() {
return self.clients.claim();
})
);
});
// Fetch: serve from cache, fallback to network
self.addEventListener('fetch', function(event) {
event.respondWith(
caches.match(event.request)
.then(function(response) {
if (response) {
return response; // Cache hit
}
return fetch(event.request).then(function(networkResponse) {
// Cache new responses
if (event.request.url.startsWith(self.location.origin)) {
var responseClone = networkResponse.clone();
caches.open(CACHE_NAME).then(function(cache) {
cache.put(event.request, responseClone);
});
}
return networkResponse;
}).catch(function() {
// Offline fallback
return caches.match('/pages/offline.html');
});
})
);
});
// Handle push notifications
self.addEventListener('push', function(event) {
var data = event.data ? event.data.json() : {};
var options = {
body: data.body || 'New update available',
icon: '/images/icon-192.png',
badge: '/images/badge-72.png',
vibrate: [200, 100, 200],
data: {
url: data.url || '/'
}
};
event.waitUntil(
self.registration.showNotification(data.title || 'App Update', options)
);
});
// Notification click
self.addEventListener('notificationclick', function(event) {
event.notification.close();
event.waitUntil(
clients.openWindow(event.notification.data.url)
);
});
Expected output: The service worker caches the app shell on install, serves cached pages on repeat visits, falls back to offline page when network is unavailable, and handles push notifications.
Web App Manifest
{
"manifest.json"
{
"name": "My Framework7 App",
"short_name": "F7 App",
"description": "A mobile-first PWA built with Framework7",
"start_url": "/?source=pwa",
"scope": "/",
"display": "standalone",
"orientation": "portrait-primary",
"background_color": "#1a237e",
"theme_color": "#1a237e",
"lang": "en",
"icons": [
{
"src": "/images/icon-72.png",
"sizes": "72x72",
"type": "image/png"
},
{
"src": "/images/icon-96.png",
"sizes": "96x96",
"type": "image/png"
},
{
"src": "/images/icon-128.png",
"sizes": "128x128",
"type": "image/png"
},
{
"src": "/images/icon-144.png",
"sizes": "144x144",
"type": "image/png"
},
{
"src": "/images/icon-152.png",
"sizes": "152x152",
"type": "image/png"
},
{
"src": "/images/icon-192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/images/icon-512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "any maskable"
}
],
"screenshots": [
{
"src": "/images/screenshot-1.png",
"sizes": "1080x1920",
"type": "image/png"
}
],
"categories": ["business", "productivity"],
"iarc_rating_id": "e.g. 4+",
"prefer_related_applications": false
}
}
<!-- In index.html -->
<link rel="manifest" href="/manifest.json" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="apple-mobile-web-app-title" content="F7 App" />
<link rel="apple-touch-icon" href="/images/icon-152.png" />
<link rel="apple-touch-startup-image" href="/images/splash.png" />
Expected output: The manifest file tells the browser the app name, icons, theme color, and display mode. Supporting meta tags enable iOS home screen installation.
Install Prompt Handling
// Handle the beforeinstallprompt event
var deferredPrompt;
window.addEventListener('beforeinstallprompt', function(event) {
// Prevent the default prompt
event.preventDefault();
// Save the event for later
deferredPrompt = event;
// Show a custom install button
$$('#install-btn').show();
});
// Custom install button
$$('#install-btn').on('click', function() {
if (deferredPrompt) {
// Show the install prompt
deferredPrompt.prompt();
// Wait for the user's response
deferredPrompt.userChoice.then(function(choiceResult) {
if (choiceResult.outcome === 'accepted') {
console.log('User accepted install');
$$('#install-btn').hide();
} else {
console.log('User dismissed install');
}
deferredPrompt = null;
});
}
});
// Track install
window.addEventListener('appinstalled', function(event) {
console.log('App was installed');
// Send analytics event
});
Expected output: On supported browsers, an install button appears. When clicked, the browser shows the native install prompt. After installation, the app opens in standalone mode.
Offline Support
// Check online status
if (app.online) {
console.log('App is online');
$$('#offline-banner').hide();
} else {
console.log('App is offline');
$$('#offline-banner').show();
}
// Network status changes
window.addEventListener('online', function() {
console.log('Back online');
$$('#offline-banner').hide();
// Sync pending data
syncPendingData();
});
window.addEventListener('offline', function() {
console.log('Went offline');
$$('#offline-banner').show();
});
// Background sync
if ('sync' in navigator.serviceWorker) {
navigator.serviceWorker.ready.then(function(registration) {
// Register a sync for pending data
registration.sync.register('sync-orders');
});
}
<!-- Offline banner -->
<div class="offline-banner" id="offline-banner" style="display:none;background:#f44336;color:#fff;text-align:center;padding:8px">
<i class="icon f7-icons">wifi-slash</i> You are offline. Some features may be unavailable.
</div>
<!-- Offline page -->
<div class="page" data-name="offline">
<div class="page-content">
<div class="block text-align-center" style="padding:40px">
<i class="icon f7-icons" style="font-size:64px;color:#999">wifi-slash</i>
<h2>No Connection</h2>
<p>Please check your internet connection and try again.</p>
<button class="button button-fill" onclick="location.reload()">Retry</button>
</div>
</div>
</div>
Expected output: An offline banner appears when the network drops. The app continues working with cached pages. Background sync queues pending data for when the connection returns.
Push Notifications
// Request permission
function requestNotificationPermission() {
if (!('Notification' in window)) {
console.log('Notifications not supported');
return;
}
Notification.requestPermission().then(function(permission) {
if (permission === 'granted') {
console.log('Notification permission granted');
subscribeToPush();
}
});
}
// Subscribe to push
function subscribeToPush() {
navigator.serviceWorker.ready.then(function(registration) {
return registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: urlBase64ToUint8Array('YOUR_PUBLIC_VAPID_KEY')
});
}).then(function(subscription) {
console.log('Push subscription:', subscription);
// Send subscription to server
fetch('/api/push/subscribe', {
method: 'POST',
body: JSON.stringify(subscription),
headers: { 'Content-Type': 'application/json' }
});
}).catch(function(error) {
console.error('Push subscription failed:', error);
});
}
// Convert VAPID key
function urlBase64ToUint8Array(base64String) {
var padding = '='.repeat((4 - base64String.length % 4) % 4);
var base64 = (base64String + padding)
.replace(/\-/g, '+')
.replace(/_/g, '/');
var rawData = window.atob(base64);
var outputArray = new Uint8Array(rawData.length);
for (var i = 0; i < rawData.length; ++i) {
outputArray[i] = rawData.charCodeAt(i);
}
return outputArray;
}
Expected output: After granting permission, the app subscribes to push notifications. The server can send push messages that appear as system notifications even when the app is closed.
Update Handling
// Check for app updates
if (navigator.serviceWorker) {
navigator.serviceWorker.addEventListener('controllerchange', function() {
// New service worker is active
if (app) {
app.onAppUpdate();
}
});
}
// App.js
var app = new Framework7({
on: {
appUpdate: function() {
app.dialog.confirm(
'A new version is available. Reload to update?',
'Update Available',
function() {
window.location.reload();
}
);
}
}
});
// Manual update check
function checkForUpdate() {
if (navigator.serviceWorker) {
navigator.serviceWorker.ready.then(function(registration) {
registration.update();
});
}
}
Expected output: When a new service worker is detected, the app shows an update dialog. The user can reload to get the latest version.
Common Mistakes
Not caching the offline fallback page - If the service worker caches pages but not the offline fallback, users see the browser's default offline page instead of a branded experience.
Caching dynamic API data too long - API responses should use network-first or stale-while-revalidate strategies. Cache-first for APIs causes stale data.
Forgetting to update cache version - When deploying updates, change the CACHE_NAME. Old caches are cleaned in the activate event.
Not handling notification click - Users expect clicking a notification to open the relevant page. Always add a notificationclick handler that opens the correct URL.
Registering service worker on unsecure origins - Service workers require HTTPS (or localhost for development). Deploy to a secure origin for production.
Practice Questions
- How do you register a service worker in Framework7?
- What is the difference between cache-first and network-first strategies?
- How do you handle the beforeinstallprompt event?
- How do you subscribe to push notifications?
- How do you handle app updates via service worker?
Challenge: Build a fully functional PWA with: service worker that caches app shell and pages, an offline fallback page with branded styling, a custom install prompt button, push notification subscription, background sync for pending orders, offline banner when network drops, and an update notification dialog when new version is available.
FAQ
Mini Project
Convert a Framework7 app into a full PWA with: service worker caching app shell and content pages, offline fallback page with retry button, custom install prompt with a banner, push notifications for new content, background sync for form submissions, offline detection with a snackbar-style banner, manifest with all icon sizes, and update notification on new deployment.
What's Next
PWAs make apps installable and offline-ready. Learn how Framework7 Storage and State Management handles data persistence with native-like storage APIs.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro