🎯 Рекомендуемые коллекции
Балансированные коллекции примеров кода из различных категорий, которые вы можете исследовать
Service Workers для Офлайн Веб-приложений
Service Workers для офлайн функциональности, фоновой синхронизации и перехвата сети
💻 Базовая Настройка Service Worker javascript
🟢 simple
⭐⭐
Регистрация service worker и включение офлайн кэширования
⏱️ 15 min
🏷️ service worker, offline, cache, pwa
Prerequisites:
JavaScript, Basic web development
// Basic Service Worker Setup
// service-worker.js
const CACHE_NAME = 'my-app-v1';
const urlsToCache = [
'/',
'/index.html',
'/styles.css',
'/app.js',
'/icon.png'
];
// Install event - cache files
self.addEventListener('install', event => {
event.waitUntil(
caches.open(CACHE_NAME)
.then(cache => {
console.log('Opened cache');
return cache.addAll(urlsToCache);
})
);
});
// Fetch event - serve from cache when offline
self.addEventListener('fetch', event => {
event.respondWith(
caches.match(event.request)
.then(response => {
// Cache hit - return response
if (response) {
return response;
}
// Clone the request
const fetchRequest = event.request.clone();
return fetch(fetchRequest).then(response => {
// Check if valid response
if(!response || response.status !== 200 || response.type !== 'basic') {
return response;
}
// Clone the response
const responseToCache = response.clone();
caches.open(CACHE_NAME)
.then(cache => {
cache.put(event.request, responseToCache);
});
return response;
});
})
);
});
// Activate event - clean up old caches
self.addEventListener('activate', event => {
event.waitUntil(
caches.keys().then(cacheNames => {
return Promise.all(
cacheNames.map(cacheName => {
if (cacheName !== CACHE_NAME) {
console.log('Deleting old cache:', cacheName);
return caches.delete(cacheName);
}
})
);
})
);
});
// main.js - Register service worker
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/service-worker.js')
.then(registration => {
console.log('ServiceWorker registration successful with scope: ', registration.scope);
// Check for updates
registration.addEventListener('updatefound', () => {
const newWorker = registration.installing;
newWorker.addEventListener('statechange', () => {
if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
// New content is available, show update notification
if (confirm('New version available! Reload to update?')) {
window.location.reload();
}
}
});
});
})
.catch(err => {
console.log('ServiceWorker registration failed: ', err);
});
});
// Listen for controlling service worker
navigator.serviceWorker.addEventListener('controllerchange', () => {
console.log('ServiceWorker controller changed');
window.location.reload();
});
}
💻 Фоновая Синхронизация javascript
🟡 intermediate
⭐⭐⭐
Синхронизация данных при восстановлении сетевого соединения
⏱️ 25 min
🏷️ background sync, offline, pwa, indexeddb
Prerequisites:
JavaScript, Service Workers, IndexedDB
// Background Sync Service Worker
// service-worker.js
self.addEventListener('sync', event => {
if (event.tag === 'background-sync') {
event.waitUntil(syncData());
}
});
async function syncData() {
try {
// Get all pending sync requests from IndexedDB
const pendingRequests = await getPendingRequests();
for (const request of pendingRequests) {
try {
const response = await fetch(request.url, {
method: request.method,
headers: request.headers,
body: request.body
});
if (response.ok) {
// Remove successful request from pending
await removePendingRequest(request.id);
console.log('Sync successful for request:', request.id);
} else {
console.error('Sync failed for request:', request.id);
}
} catch (error) {
console.error('Network error during sync:', error);
throw error; // This will retry the sync
}
}
// Show notification if sync completed
if (await getPendingRequestsCount() === 0) {
self.registration.showNotification('Sync Complete', {
body: 'All your data has been synchronized',
icon: '/sync-icon.png'
});
}
} catch (error) {
console.error('Background sync failed:', error);
}
}
// IndexedDB helpers for pending requests
async function getPendingRequests() {
return new Promise((resolve, reject) => {
const request = indexedDB.open('sync-db', 1);
request.onerror = () => reject(request.error);
request.onsuccess = () => {
const db = request.result;
const transaction = db.transaction(['requests'], 'readonly');
const store = transaction.objectStore('requests');
const getAll = store.getAll();
getAll.onsuccess = () => resolve(getAll.result);
getAll.onerror = () => reject(getAll.error);
};
request.onupgradeneeded = () => {
const db = request.result;
db.createObjectStore('requests', { keyPath: 'id' });
};
});
}
async function removePendingRequest(id) {
return new Promise((resolve, reject) => {
const request = indexedDB.open('sync-db', 1);
request.onerror = () => reject(request.error);
request.onsuccess = () => {
const db = request.result;
const transaction = db.transaction(['requests'], 'readwrite');
const store = transaction.objectStore('requests');
const deleteRequest = store.delete(id);
deleteRequest.onsuccess = () => resolve();
deleteRequest.onerror = () => reject(deleteRequest.error);
};
});
}
// main.js - Register background sync
class BackgroundSyncManager {
constructor() {
this.registration = null;
}
async init() {
this.registration = await navigator.serviceWorker.ready;
}
async registerSync() {
if ('serviceWorker' in navigator && 'sync' in window.ServiceWorkerRegistration.prototype) {
try {
await this.registration.sync.register('background-sync');
console.log('Background sync registered');
return true;
} catch (error) {
console.error('Background sync registration failed:', error);
return false;
}
} else {
console.warn('Background sync not supported');
return false;
}
}
async addPendingRequest(requestData) {
const request = {
id: Date.now().toString(),
...requestData,
timestamp: Date.now()
};
// Save to IndexedDB
const db = await this.openDB();
const transaction = db.transaction(['requests'], 'readwrite');
const store = transaction.objectStore('requests');
await store.add(request);
// Try to register background sync
const syncRegistered = await this.registerSync();
if (!syncRegistered) {
// Fallback: try immediate sync
this.tryImmediateSync(request);
}
return request.id;
}
async openDB() {
return new Promise((resolve, reject) => {
const request = indexedDB.open('sync-db', 1);
request.onerror = () => reject(request.error);
request.onsuccess = () => resolve(request.result);
request.onupgradeneeded = () => {
const db = request.result;
db.createObjectStore('requests', { keyPath: 'id' });
};
});
}
async tryImmediateSync(request) {
try {
const response = await fetch(request.url, {
method: request.method,
headers: request.headers,
body: request.body
});
if (response.ok) {
await this.removePendingRequest(request.id);
console.log('Immediate sync successful');
} else {
console.log('Immediate sync failed, keeping in pending queue');
}
} catch (error) {
console.log('Network unavailable, keeping in pending queue');
}
}
}
// Usage example
const syncManager = new BackgroundSyncManager();
// Initialize when app loads
window.addEventListener('load', async () => {
await syncManager.init();
});
// Example: Save data with background sync
async function saveDataOffline(data) {
const requestData = {
url: '/api/save',
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
};
const requestId = await syncManager.addPendingRequest(requestData);
// Show user that data will be synced
alert('Your data will be synced when you're back online');
}
💻 Push-уведомления javascript
🟡 intermediate
⭐⭐⭐⭐
Получение и обработка push-уведомлений с service workers
⏱️ 30 min
🏷️ push notifications, pwa, service worker, messaging
Prerequisites:
JavaScript, Service Workers, HTTPS setup
// Push Notifications with Service Worker
// service-worker.js
self.addEventListener('push', event => {
const options = {
body: event.data ? event.data.text() : 'New notification',
icon: '/notification-icon.png',
badge: '/notification-badge.png',
vibrate: [100, 50, 100],
data: {
dateOfArrival: Date.now(),
primaryKey: 1
},
actions: [
{
action: 'explore',
title: 'Explore this new world',
icon: '/images/checkmark.png'
},
{
action: 'close',
title: 'Close notification',
icon: '/images/xmark.png'
}
]
};
event.waitUntil(
self.registration.showNotification('Push Notification', options)
);
});
// Handle notification clicks
self.addEventListener('notificationclick', event => {
event.notification.close();
if (event.action === 'explore') {
// Open the app to specific page
event.waitUntil(
clients.openWindow('/explore')
);
} else if (event.action === 'close') {
// Just close the notification
console.log('Notification closed');
} else {
// Default action - open main app
event.waitUntil(
clients.matchAll().then(clientList => {
for (const client of clientList) {
if (client.url === '/' && 'focus' in client) {
return client.focus();
}
}
if (clients.openWindow) {
return clients.openWindow('/');
}
})
);
}
});
// Handle notification close
self.addEventListener('notificationclose', event => {
console.log('Notification closed:', event.notification);
});
// main.js - Push notification subscription
class PushNotificationManager {
constructor() {
this.subscription = null;
this.publicKey = 'YOUR_VAPID_PUBLIC_KEY';
}
async init() {
// Check for service worker support
if (!('serviceWorker' in navigator)) {
console.error('Service Worker not supported');
return false;
}
// Check for push notification support
if (!('PushManager' in window)) {
console.error('Push notifications not supported');
return false;
}
// Register service worker
const registration = await navigator.serviceWorker.register('/push-service-worker.js');
console.log('Service Worker registered');
this.registration = registration;
return true;
}
async subscribeToPush() {
try {
// Subscribe to push notifications
this.subscription = await this.registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: this.urlBase64ToUint8Array(this.publicKey)
});
console.log('User is subscribed:', this.subscription);
// Send subscription to server
await this.sendSubscriptionToServer(this.subscription);
return this.subscription;
} catch (error) {
console.error('Failed to subscribe to push notifications:', error);
return null;
}
}
async unsubscribeFromPush() {
try {
const result = await this.subscription.unsubscribe();
console.log('Unsubscribed:', result);
// Remove subscription from server
await this.removeSubscriptionFromServer(this.subscription);
this.subscription = null;
return result;
} catch (error) {
console.error('Failed to unsubscribe:', error);
return false;
}
}
async sendSubscriptionToServer(subscription) {
try {
const response = await fetch('/api/subscribe', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(subscription)
});
if (!response.ok) {
throw new Error('Failed to send subscription to server');
}
return await response.json();
} catch (error) {
console.error('Error sending subscription to server:', error);
throw error;
}
}
async removeSubscriptionFromServer(subscription) {
try {
const response = await fetch('/api/unsubscribe', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(subscription)
});
if (!response.ok) {
throw new Error('Failed to remove subscription from server');
}
} catch (error) {
console.error('Error removing subscription from server:', error);
throw error;
}
}
async requestNotificationPermission() {
const permission = await Notification.requestPermission();
if (permission === 'granted') {
console.log('Notification permission granted');
return true;
} else {
console.log('Notification permission denied');
return false;
}
}
// Helper function to convert VAPID key
urlBase64ToUint8Array(base64String) {
const padding = '='.repeat((4 - base64String.length % 4) % 4);
const base64 = (base64String + padding)
.replace(/-/g, '+')
.replace(/_/g, '/');
const rawData = window.atob(base64);
const outputArray = new Uint8Array(rawData.length);
for (let i = 0; i < rawData.length; ++i) {
outputArray[i] = rawData.charCodeAt(i);
}
return outputArray;
}
async checkSubscriptionStatus() {
this.subscription = await this.registration.pushManager.getSubscription();
if (this.subscription) {
console.log('User is already subscribed');
return this.subscription;
} else {
console.log('User is not subscribed');
return null;
}
}
}
// Usage example
const pushManager = new PushNotificationManager();
// Initialize push notifications
async function initPushNotifications() {
const initialized = await pushManager.init();
if (!initialized) return;
// Request permission
const permissionGranted = await pushManager.requestNotificationPermission();
if (!permissionGranted) return;
// Check if already subscribed
const existingSubscription = await pushManager.checkSubscriptionStatus();
if (!existingSubscription) {
// Subscribe to push notifications
await pushManager.subscribeToPush();
}
}
// Subscribe button handler
document.getElementById('subscribe-btn').addEventListener('click', async () => {
await initPushNotifications();
});
// Unsubscribe button handler
document.getElementById('unsubscribe-btn').addEventListener('click', async () => {
await pushManager.unsubscribeFromPush();
});
// Test local notification
document.getElementById('test-notification').addEventListener('click', async () => {
if (Notification.permission === 'granted') {
new Notification('Test Notification', {
body: 'This is a test notification!',
icon: '/icon.png'
});
} else {
console.log('Notification permission not granted');
}
});
💻 Продвинутые Стратегии Кэширования javascript
🟡 intermediate
⭐⭐⭐⭐
Реализация различных стратегий кэширования для оптимальной производительности
⏱️ 35 min
🏷️ caching, performance, offline, pwa, strategies
Prerequisites:
JavaScript, Service Workers, Performance optimization
// Advanced Service Worker Caching Strategies
// service-worker.js
// Different cache strategies
class CacheStrategies {
// Cache First - try cache first, fallback to network
static cacheFirst(request) {
return caches.match(request)
.then(response => {
return response || fetch(request);
});
}
// Network First - try network first, fallback to cache
static networkFirst(request) {
return fetch(request)
.then(response => {
// Cache successful responses
if (response.status === 200) {
const responseClone = response.clone();
caches.open('dynamic-cache-v1')
.then(cache => cache.put(request, responseClone));
}
return response;
})
.catch(() => {
// Fallback to cache
return caches.match(request);
});
}
// Stale While Revalidate - serve from cache, update in background
static staleWhileRevalidate(request) {
const cachePromise = caches.match(request);
const networkPromise = fetch(request)
.then(response => {
const responseClone = response.clone();
caches.open('dynamic-cache-v1')
.then(cache => cache.put(request, responseClone));
return response;
});
return cachePromise
.then(response => response || networkPromise)
.catch(() => networkPromise);
}
// Network Only - always fetch from network
static networkOnly(request) {
return fetch(request);
}
// Cache Only - always serve from cache
static cacheOnly(request) {
return caches.match(request);
}
}
// Cache management
class CacheManager {
constructor() {
this.caches = {
static: 'static-cache-v1',
dynamic: 'dynamic-cache-v1',
api: 'api-cache-v1',
images: 'images-cache-v1'
};
this.cacheRules = [
// Static assets - cache first
{
match: request =>
request.destination === 'script' ||
request.destination === 'style' ||
request.destination === 'font',
strategy: 'cacheFirst',
cacheName: this.caches.static
},
// Images - stale while revalidate
{
match: request => request.destination === 'image',
strategy: 'staleWhileRevalidate',
cacheName: this.caches.images,
maxEntries: 100,
maxAge: 30 * 24 * 60 * 60 * 1000 // 30 days
},
// API calls - network first with timeout
{
match: request => request.url.includes('/api/'),
strategy: 'networkFirst',
cacheName: this.caches.api,
timeout: 3000, // 3 seconds
maxEntries: 50
},
// HTML pages - network first
{
match: request =>
request.destination === 'document' ||
request.mode === 'navigate',
strategy: 'networkFirst',
cacheName: this.caches.dynamic
},
// External CDN - cache first
{
match: request =>
request.url.includes('cdn.jsdelivr.net') ||
request.url.includes('cdnjs.cloudflare.com'),
strategy: 'cacheFirst',
cacheName: this.caches.static
}
];
}
async handleRequest(request) {
const rule = this.findMatchingRule(request);
if (!rule) {
return fetch(request);
}
try {
// Apply strategy with timeout if specified
if (rule.timeout) {
return await this.withTimeout(
CacheStrategies[rule.strategy](request),
rule.timeout,
caches.match(request)
);
} else {
const response = await CacheStrategies[rule.strategy](request);
// Apply cache management rules
if (response && response.ok && rule.cacheName) {
await this.manageCache(rule.cacheName, request, rule);
}
return response;
}
} catch (error) {
console.error('Cache strategy failed:', error);
// Fallback to cache if available
return caches.match(request) || new Response('Offline', {
status: 503,
statusText: 'Service Unavailable'
});
}
}
findMatchingRule(request) {
return this.cacheRules.find(rule => rule.match(request));
}
async withTimeout(promise, timeout, fallback) {
return Promise.race([
promise,
new Promise((_, reject) =>
setTimeout(() => reject(new Error('Timeout')), timeout)
)
]).catch(error => {
if (fallback) return fallback;
throw error;
});
}
async manageCache(cacheName, request, rule) {
if (rule.maxEntries || rule.maxAge) {
const cache = await caches.open(cacheName);
const requests = await cache.keys();
// Remove old entries if maxEntries exceeded
if (rule.maxEntries && requests.length > rule.maxEntries) {
const toDelete = requests.slice(0, requests.length - rule.maxEntries);
await Promise.all(toDelete.map(req => cache.delete(req)));
}
// Remove old entries based on age
if (rule.maxAge) {
const now = Date.now();
for (const cacheRequest of requests) {
const response = await cache.match(cacheRequest);
const dateHeader = response.headers.get('date');
if (dateHeader && (now - new Date(dateHeader).getTime()) > rule.maxAge) {
cache.delete(cacheRequest);
}
}
}
}
}
}
// Initialize cache manager
const cacheManager = new CacheManager();
// Service worker event handlers
self.addEventListener('fetch', event => {
event.respondWith(cacheManager.handleRequest(event.request));
});
// Install event - precache critical assets
self.addEventListener('install', event => {
const staticAssets = [
'/',
'/index.html',
'/offline.html',
'/styles/main.css',
'/scripts/app.js',
'/images/logo.png'
];
event.waitUntil(
caches.open(cacheManager.caches.static)
.then(cache => cache.addAll(staticAssets))
);
});
// Activate event - clean up old caches
self.addEventListener('activate', event => {
event.waitUntil(
caches.keys().then(cacheNames => {
return Promise.all(
cacheNames
.filter(name => !Object.values(cacheManager.caches).includes(name))
.map(name => caches.delete(name))
);
})
);
});
// Background sync for offline analytics
self.addEventListener('sync', event => {
if (event.tag === 'analytics-sync') {
event.waitUntil(syncAnalytics());
}
});
async function syncAnalytics() {
const offlineData = await getOfflineAnalytics();
for (const data of offlineData) {
try {
await fetch('/api/analytics', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
await removeOfflineAnalytics(data.id);
} catch (error) {
console.error('Failed to sync analytics:', error);
break;
}
}
}
// IndexedDB helpers for offline analytics
async function getOfflineAnalytics() {
// Implementation for getting analytics from IndexedDB
return [];
}
async function removeOfflineAnalytics(id) {
// Implementation for removing analytics from IndexedDB
}
// main.js - Client-side analytics that works offline
class OfflineAnalytics {
constructor() {
this.db = null;
}
async init() {
this.db = await this.openDB();
}
async openDB() {
return new Promise((resolve, reject) => {
const request = indexedDB.open('analytics-db', 1);
request.onerror = () => reject(request.error);
request.onsuccess = () => resolve(request.result);
request.onupgradeneeded = () => {
const db = request.result;
db.createObjectStore('events', { keyPath: 'id', autoIncrement: true });
};
});
}
async track(eventName, properties = {}) {
const event = {
name: eventName,
properties: {
...properties,
timestamp: Date.now(),
url: window.location.href,
userAgent: navigator.userAgent
}
};
try {
// Try to send immediately
const response = await fetch('/api/analytics', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(event)
});
if (!response.ok) throw new Error('Analytics request failed');
} catch (error) {
// Store offline
await this.storeOffline(event);
// Register background sync if available
if ('serviceWorker' in navigator && 'sync' in window.ServiceWorkerRegistration.prototype) {
const registration = await navigator.serviceWorker.ready;
registration.sync.register('analytics-sync');
}
}
}
async storeOffline(event) {
const transaction = this.db.transaction(['events'], 'readwrite');
const store = transaction.objectStore('events');
await store.add(event);
}
}
// Usage
const analytics = new OfflineAnalytics();
analytics.init();
// Track page view
analytics.track('page_view', {
page: window.location.pathname,
title: document.title
});
// Track user interaction
document.getElementById('cta-button').addEventListener('click', () => {
analytics.track('cta_clicked', {
button_text: 'Get Started',
location: 'homepage'
});
});