feat(pwa): implement service worker and PWA enhancements
- Added service worker (sw.js) for offline functionality, caching strategies, and performance improvements. - Registered service worker in App component to manage updates and offline readiness. - Introduced ServiceWorkerStatus component to display connection status and provide cache management controls. - Created manifest.json for PWA configuration, including app name, icons, and display settings. - Updated index.html with PWA meta tags and links to support mobile web app capabilities. - Refactored authentication guards to utilize useAuthStatus hook for improved user state management. - Removed deprecated unregister-sw.js file to streamline service worker management.
This commit is contained in:
259
worklenz-frontend/src/utils/README-ServiceWorker.md
Normal file
259
worklenz-frontend/src/utils/README-ServiceWorker.md
Normal file
@@ -0,0 +1,259 @@
|
||||
# Service Worker Implementation
|
||||
|
||||
This directory contains the service worker implementation for Worklenz, providing offline functionality, caching, and performance improvements.
|
||||
|
||||
## Files Overview
|
||||
|
||||
- **`sw.js`** (in `/public/`) - The main service worker file
|
||||
- **`serviceWorkerRegistration.ts`** - Registration and management utilities
|
||||
- **`ServiceWorkerStatus.tsx`** (in `/components/`) - React component for SW status
|
||||
|
||||
## Features
|
||||
|
||||
### 🔄 Caching Strategies
|
||||
|
||||
1. **Cache First** - Static assets (JS, CSS, images)
|
||||
- Serves from cache first, falls back to network
|
||||
- Perfect for unchanging resources
|
||||
|
||||
2. **Network First** - API requests
|
||||
- Tries network first, falls back to cache
|
||||
- Ensures fresh data when online
|
||||
|
||||
3. **Stale While Revalidate** - HTML pages
|
||||
- Serves cached version immediately
|
||||
- Updates cache in background
|
||||
|
||||
### 📱 PWA Features
|
||||
|
||||
- **Offline Support** - App works without internet
|
||||
- **Installable** - Can be installed on devices
|
||||
- **Background Sync** - Sync data when online (framework ready)
|
||||
- **Push Notifications** - Real-time notifications (framework ready)
|
||||
|
||||
## Usage
|
||||
|
||||
### Basic Integration
|
||||
|
||||
The service worker is automatically registered in `App.tsx`:
|
||||
|
||||
```tsx
|
||||
import { registerSW } from './utils/serviceWorkerRegistration';
|
||||
|
||||
useEffect(() => {
|
||||
registerSW({
|
||||
onSuccess: (registration) => {
|
||||
console.log('SW registered successfully');
|
||||
},
|
||||
onUpdate: (registration) => {
|
||||
// Show update notification to user
|
||||
},
|
||||
onOfflineReady: () => {
|
||||
console.log('App ready for offline use');
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
```
|
||||
|
||||
### Using the Hook
|
||||
|
||||
```tsx
|
||||
import { useServiceWorker } from '../utils/serviceWorkerRegistration';
|
||||
|
||||
const MyComponent = () => {
|
||||
const { isOffline, swManager, clearCache, forceUpdate } = useServiceWorker();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p>Status: {isOffline ? 'Offline' : 'Online'}</p>
|
||||
<button onClick={clearCache}>Clear Cache</button>
|
||||
<button onClick={forceUpdate}>Update App</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
### Status Component
|
||||
|
||||
```tsx
|
||||
import ServiceWorkerStatus from '../components/service-worker-status/ServiceWorkerStatus';
|
||||
|
||||
// Minimal offline indicator
|
||||
<ServiceWorkerStatus minimal />
|
||||
|
||||
// Full status with controls
|
||||
<ServiceWorkerStatus showControls />
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
### Cacheable Resources
|
||||
|
||||
Edit the patterns in `sw.js`:
|
||||
|
||||
```javascript
|
||||
// API endpoints that can be cached
|
||||
const CACHEABLE_API_PATTERNS = [
|
||||
/\/api\/project-categories/,
|
||||
/\/api\/task-statuses/,
|
||||
// Add more patterns...
|
||||
];
|
||||
|
||||
// Resources that should never be cached
|
||||
const NEVER_CACHE_PATTERNS = [
|
||||
/\/api\/auth\/login/,
|
||||
/\/socket\.io/,
|
||||
// Add more patterns...
|
||||
];
|
||||
```
|
||||
|
||||
### Cache Names
|
||||
|
||||
Update version to force cache refresh:
|
||||
|
||||
```javascript
|
||||
const CACHE_VERSION = 'v1.0.1'; // Increment when deploying
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
### Testing Offline
|
||||
|
||||
1. Open DevTools → Application → Service Workers
|
||||
2. Check "Offline" to simulate offline mode
|
||||
3. Verify app still functions
|
||||
|
||||
### Debugging
|
||||
|
||||
```javascript
|
||||
// Check service worker status
|
||||
navigator.serviceWorker.ready.then(registration => {
|
||||
console.log('SW ready:', registration);
|
||||
});
|
||||
|
||||
// Check cache contents
|
||||
caches.keys().then(names => {
|
||||
console.log('Cache names:', names);
|
||||
});
|
||||
```
|
||||
|
||||
### Cache Management
|
||||
|
||||
```javascript
|
||||
// Clear all caches
|
||||
caches.keys().then(names =>
|
||||
Promise.all(names.map(name => caches.delete(name)))
|
||||
);
|
||||
|
||||
// Clear specific cache
|
||||
caches.delete('worklenz-api-v1.0.0');
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Cache Strategy Selection
|
||||
|
||||
- **Static Assets**: Cache First (fast loading)
|
||||
- **API Data**: Network First (fresh data)
|
||||
- **User Content**: Network Only (always fresh)
|
||||
- **App Shell**: Cache First (instant loading)
|
||||
|
||||
### 2. Cache Invalidation
|
||||
|
||||
- Increment `CACHE_VERSION` when deploying
|
||||
- Use versioned URLs for assets
|
||||
- Set appropriate cache headers
|
||||
|
||||
### 3. Offline UX
|
||||
|
||||
- Show offline indicators
|
||||
- Queue actions for later sync
|
||||
- Provide meaningful offline messages
|
||||
- Cache critical user data
|
||||
|
||||
### 4. Performance
|
||||
|
||||
- Cache only necessary resources
|
||||
- Set cache size limits
|
||||
- Clean up old caches regularly
|
||||
- Monitor cache usage
|
||||
|
||||
## Monitoring
|
||||
|
||||
### Storage Usage
|
||||
|
||||
```javascript
|
||||
// Check storage quota
|
||||
navigator.storage.estimate().then(estimate => {
|
||||
console.log('Used:', estimate.usage);
|
||||
console.log('Quota:', estimate.quota);
|
||||
});
|
||||
```
|
||||
|
||||
### Cache Hit Rate
|
||||
|
||||
Monitor in DevTools → Network:
|
||||
- Look for "from ServiceWorker" requests
|
||||
- Check cache effectiveness
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
1. **SW not updating**
|
||||
- Hard refresh (Ctrl+Shift+R)
|
||||
- Clear browser cache
|
||||
- Check CACHE_VERSION
|
||||
|
||||
2. **Resources not caching**
|
||||
- Verify URL patterns
|
||||
- Check NEVER_CACHE_PATTERNS
|
||||
- Ensure HTTPS in production
|
||||
|
||||
3. **Offline features not working**
|
||||
- Verify SW registration
|
||||
- Check browser support
|
||||
- Test cache strategies
|
||||
|
||||
### Reset Service Worker
|
||||
|
||||
```javascript
|
||||
// Unregister and reload
|
||||
navigator.serviceWorker.getRegistrations().then(registrations => {
|
||||
registrations.forEach(registration => registration.unregister());
|
||||
window.location.reload();
|
||||
});
|
||||
```
|
||||
|
||||
## Browser Support
|
||||
|
||||
- ✅ Chrome 45+
|
||||
- ✅ Firefox 44+
|
||||
- ✅ Safari 11.1+
|
||||
- ✅ Edge 17+
|
||||
- ❌ Internet Explorer
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
1. **Background Sync**
|
||||
- Queue offline actions
|
||||
- Sync when online
|
||||
|
||||
2. **Push Notifications**
|
||||
- Task assignments
|
||||
- Project updates
|
||||
- Deadline reminders
|
||||
|
||||
3. **Advanced Caching**
|
||||
- Intelligent prefetching
|
||||
- ML-based cache eviction
|
||||
- Compression
|
||||
|
||||
4. **Offline Analytics**
|
||||
- Track offline usage
|
||||
- Cache hit rates
|
||||
- Performance metrics
|
||||
|
||||
---
|
||||
|
||||
*Last updated: January 2025*
|
||||
273
worklenz-frontend/src/utils/serviceWorkerRegistration.ts
Normal file
273
worklenz-frontend/src/utils/serviceWorkerRegistration.ts
Normal file
@@ -0,0 +1,273 @@
|
||||
// Service Worker Registration Utility
|
||||
// Handles registration, updates, and error handling
|
||||
|
||||
import React from 'react';
|
||||
|
||||
const isLocalhost = Boolean(
|
||||
window.location.hostname === 'localhost' ||
|
||||
window.location.hostname === '[::1]' ||
|
||||
window.location.hostname.match(/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/)
|
||||
);
|
||||
|
||||
type Config = {
|
||||
onSuccess?: (registration: ServiceWorkerRegistration) => void;
|
||||
onUpdate?: (registration: ServiceWorkerRegistration) => void;
|
||||
onOfflineReady?: () => void;
|
||||
onError?: (error: Error) => void;
|
||||
};
|
||||
|
||||
export function registerSW(config?: Config) {
|
||||
if ('serviceWorker' in navigator) {
|
||||
// Only register in production or when explicitly testing
|
||||
const swUrl = '/sw.js';
|
||||
|
||||
if (isLocalhost) {
|
||||
// This is running on localhost. Let's check if a service worker still exists or not.
|
||||
checkValidServiceWorker(swUrl, config);
|
||||
|
||||
// Add some additional logging to localhost, pointing developers to the
|
||||
// service worker/PWA documentation.
|
||||
navigator.serviceWorker.ready.then(() => {
|
||||
console.log(
|
||||
'This web app is being served cache-first by a service ' +
|
||||
'worker. To learn more, visit https://bit.ly/CRA-PWA'
|
||||
);
|
||||
});
|
||||
} else {
|
||||
// Is not localhost. Just register service worker
|
||||
registerValidSW(swUrl, config);
|
||||
}
|
||||
} else {
|
||||
console.log('Service workers are not supported in this browser.');
|
||||
}
|
||||
}
|
||||
|
||||
function registerValidSW(swUrl: string, config?: Config) {
|
||||
navigator.serviceWorker
|
||||
.register(swUrl)
|
||||
.then(registration => {
|
||||
console.log('Service Worker registered successfully:', registration);
|
||||
|
||||
registration.onupdatefound = () => {
|
||||
const installingWorker = registration.installing;
|
||||
if (installingWorker == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
installingWorker.onstatechange = () => {
|
||||
if (installingWorker.state === 'installed') {
|
||||
if (navigator.serviceWorker.controller) {
|
||||
// At this point, the updated precached content has been fetched,
|
||||
// but the previous service worker will still serve the older
|
||||
// content until all client tabs are closed.
|
||||
console.log(
|
||||
'New content is available and will be used when all ' +
|
||||
'tabs for this page are closed. See https://bit.ly/CRA-PWA.'
|
||||
);
|
||||
|
||||
// Execute callback
|
||||
if (config && config.onUpdate) {
|
||||
config.onUpdate(registration);
|
||||
}
|
||||
} else {
|
||||
// At this point, everything has been precached.
|
||||
// It's the perfect time to display a
|
||||
// "Content is cached for offline use." message.
|
||||
console.log('Content is cached for offline use.');
|
||||
|
||||
// Execute callback
|
||||
if (config && config.onSuccess) {
|
||||
config.onSuccess(registration);
|
||||
}
|
||||
|
||||
if (config && config.onOfflineReady) {
|
||||
config.onOfflineReady();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error during service worker registration:', error);
|
||||
if (config && config.onError) {
|
||||
config.onError(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function checkValidServiceWorker(swUrl: string, config?: Config) {
|
||||
// Check if the service worker can be found. If it can't reload the page.
|
||||
fetch(swUrl, {
|
||||
headers: { 'Service-Worker': 'script' },
|
||||
})
|
||||
.then(response => {
|
||||
// Ensure service worker exists, and that we really are getting a JS file.
|
||||
const contentType = response.headers.get('content-type');
|
||||
if (
|
||||
response.status === 404 ||
|
||||
(contentType != null && contentType.indexOf('javascript') === -1)
|
||||
) {
|
||||
// No service worker found. Probably a different app. Reload the page.
|
||||
navigator.serviceWorker.ready.then(registration => {
|
||||
registration.unregister().then(() => {
|
||||
window.location.reload();
|
||||
});
|
||||
});
|
||||
} else {
|
||||
// Service worker found. Proceed as normal.
|
||||
registerValidSW(swUrl, config);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
console.log(
|
||||
'No internet connection found. App is running in offline mode.'
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export function unregisterSW() {
|
||||
if ('serviceWorker' in navigator) {
|
||||
navigator.serviceWorker.ready
|
||||
.then(registration => {
|
||||
registration.unregister();
|
||||
console.log('Service Worker unregistered successfully');
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error during service worker unregistration:', error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Utility to communicate with service worker
|
||||
export class ServiceWorkerManager {
|
||||
private registration: ServiceWorkerRegistration | null = null;
|
||||
|
||||
constructor(registration?: ServiceWorkerRegistration) {
|
||||
this.registration = registration || null;
|
||||
}
|
||||
|
||||
// Send message to service worker
|
||||
async sendMessage(type: string, payload?: any): Promise<any> {
|
||||
if (!this.registration || !this.registration.active) {
|
||||
throw new Error('Service Worker not available');
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const messageChannel = new MessageChannel();
|
||||
|
||||
messageChannel.port1.onmessage = (event) => {
|
||||
if (event.data.error) {
|
||||
reject(event.data.error);
|
||||
} else {
|
||||
resolve(event.data);
|
||||
}
|
||||
};
|
||||
|
||||
this.registration!.active!.postMessage(
|
||||
{ type, payload },
|
||||
[messageChannel.port2]
|
||||
);
|
||||
|
||||
// Timeout after 5 seconds
|
||||
setTimeout(() => {
|
||||
reject(new Error('Service Worker message timeout'));
|
||||
}, 5000);
|
||||
});
|
||||
}
|
||||
|
||||
// Get service worker version
|
||||
async getVersion(): Promise<string> {
|
||||
try {
|
||||
const response = await this.sendMessage('GET_VERSION');
|
||||
return response.version;
|
||||
} catch (error) {
|
||||
console.error('Failed to get service worker version:', error);
|
||||
return 'unknown';
|
||||
}
|
||||
}
|
||||
|
||||
// Clear all caches
|
||||
async clearCache(): Promise<boolean> {
|
||||
try {
|
||||
await this.sendMessage('CLEAR_CACHE');
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Failed to clear cache:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Force update service worker
|
||||
async forceUpdate(): Promise<void> {
|
||||
if (!this.registration) return;
|
||||
|
||||
try {
|
||||
await this.registration.update();
|
||||
await this.sendMessage('SKIP_WAITING');
|
||||
window.location.reload();
|
||||
} catch (error) {
|
||||
console.error('Failed to force update service worker:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if app is running offline
|
||||
isOffline(): boolean {
|
||||
return !navigator.onLine;
|
||||
}
|
||||
|
||||
// Get cache storage estimate
|
||||
async getCacheSize(): Promise<StorageEstimate | null> {
|
||||
if ('storage' in navigator && 'estimate' in navigator.storage) {
|
||||
try {
|
||||
return await navigator.storage.estimate();
|
||||
} catch (error) {
|
||||
console.error('Failed to get storage estimate:', error);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Hook to use service worker in React components
|
||||
export function useServiceWorker() {
|
||||
const [isOffline, setIsOffline] = React.useState(!navigator.onLine);
|
||||
const [swManager, setSWManager] = React.useState<ServiceWorkerManager | null>(null);
|
||||
|
||||
React.useEffect(() => {
|
||||
const handleOnline = () => setIsOffline(false);
|
||||
const handleOffline = () => setIsOffline(true);
|
||||
|
||||
window.addEventListener('online', handleOnline);
|
||||
window.addEventListener('offline', handleOffline);
|
||||
|
||||
// Register service worker
|
||||
registerSW({
|
||||
onSuccess: (registration) => {
|
||||
setSWManager(new ServiceWorkerManager(registration));
|
||||
},
|
||||
onUpdate: (registration) => {
|
||||
// You could show a toast here asking user to refresh
|
||||
console.log('New version available');
|
||||
setSWManager(new ServiceWorkerManager(registration));
|
||||
},
|
||||
onOfflineReady: () => {
|
||||
console.log('App ready for offline use');
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('online', handleOnline);
|
||||
window.removeEventListener('offline', handleOffline);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return {
|
||||
isOffline,
|
||||
swManager,
|
||||
clearCache: () => swManager?.clearCache(),
|
||||
forceUpdate: () => swManager?.forceUpdate(),
|
||||
getVersion: () => swManager?.getVersion(),
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user