Przejdź do treści
Tworzenie Stron

PWA z offline mode - Jak stworzyć aplikację działającą bez internetu

Dowiedz się jak zbudować Progressive Web App z pełnym wsparciem offline. Praktyczny przewodnik po Service Workers, strategiach cachowania i implementacji w Next.js.

26 stycznia 2025
14 min
Zagor Digital

Szybka odpowiedź

Progressive Web App (PWA) to aplikacja webowa działająca jak natywna aplikacja mobilna, z możliwością pracy offline dzięki Service Workers. Service Worker to skrypt JavaScript działający w tle, przechwytujący żądania sieciowe i zarządzający cache'em. Kluczowe elementy to: manifest.json definiujący wygląd aplikacji, strategia cachowania zasobów, oraz implementacja background sync dla synchronizacji danych po powrocie połączenia.

Wprowadzenie

W dobie rosnących oczekiwań użytkowników, aplikacje webowe muszą działać niezawodnie - nawet bez połączenia internetowego. Progressive Web Apps (PWA) oferują rozwiązanie tego problemu, łącząc najlepsze cechy aplikacji webowych i natywnych. Możliwość pracy offline to już nie luksus, ale standard oczekiwany przez użytkowników.

W tym przewodniku pokażemy kompleksową implementację PWA z trybem offline, od podstaw Service Workers po zaawansowane strategie cachowania. Szczególny nacisk położymy na praktyczne przykłady w Next.js oraz najlepsze praktyki na lata 2024-2025.

Architektura PWA i tryb offline

Kluczowe komponenty

PWA z trybem offline opiera się na trzech filarach:

  1. Service Worker - skrypt JavaScript działający jako proxy między aplikacją a siecią
  2. Cache API - przechowywanie zasobów lokalnie w przeglądarce
  3. Manifest - plik JSON definiujący wygląd i zachowanie aplikacji

Jak działa Service Worker

Service Worker to serce funkcjonalności offline. Działa jako warstwa pośrednicząca między aplikacją a siecią:

// Cykl życia Service Worker
// 1. Rejestracja
if ('serviceWorker' in navigator) {
  window.addEventListener('load', () => {
    navigator.serviceWorker.register('/sw.js')
      .then(registration => console.log('SW zarejestrowany:', registration))
      .catch(error => console.log('Błąd rejestracji SW:', error));
  });
}

// 2. Instalacja
self.addEventListener('install', event => {
  console.log('Service Worker instalowany');
  // Precaching krytycznych zasobów
});

// 3. Aktywacja
self.addEventListener('activate', event => {
  console.log('Service Worker aktywowany');
  // Czyszczenie starych cache'y
});

// 4. Przechwytywanie żądań
self.addEventListener('fetch', event => {
  console.log('Przechwycono żądanie:', event.request.url);
  // Logika obsługi żądań
});

Implementacja podstawowego Service Worker

Krok 1: Rejestracja Service Worker

// app.js lub index.js
if ('serviceWorker' in navigator && process.env.NODE_ENV === 'production') {
  window.addEventListener('load', async () => {
    try {
      const registration = await navigator.serviceWorker.register('/sw.js', {
        scope: '/'
      });
      
      console.log('Service Worker zarejestrowany:', registration.scope);
      
      // Sprawdzanie aktualizacji
      registration.addEventListener('updatefound', () => {
        const newWorker = registration.installing;
        newWorker.addEventListener('statechange', () => {
          if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
            // Nowa wersja dostępna
            showUpdateNotification();
          }
        });
      });
    } catch (error) {
      console.error('Błąd rejestracji Service Worker:', error);
    }
  });
}

Krok 2: Implementacja Service Worker

// public/sw.js
const CACHE_NAME = 'zagor-pwa-v1';
const urlsToCache = [
  '/',
  '/styles/main.css',
  '/scripts/app.js',
  '/offline.html',
  // Dodaj wszystkie krytyczne zasoby
];

// Instalacja i precaching
self.addEventListener('install', event => {
  event.waitUntil(
    caches.open(CACHE_NAME)
      .then(cache => {
        console.log('Cache otwarty');
        return cache.addAll(urlsToCache);
      })
      .then(() => self.skipWaiting()) // Natychmiastowa aktywacja
  );
});

// Aktywacja i czyszczenie starych cache'y
self.addEventListener('activate', event => {
  const cacheWhitelist = [CACHE_NAME];
  
  event.waitUntil(
    caches.keys().then(cacheNames => {
      return Promise.all(
        cacheNames.map(cacheName => {
          if (!cacheWhitelist.includes(cacheName)) {
            return caches.delete(cacheName);
          }
        })
      );
    }).then(() => self.clients.claim()) // Przejęcie kontroli
  );
});

// Strategie cachowania
self.addEventListener('fetch', event => {
  event.respondWith(
    caches.match(event.request)
      .then(response => {
        // Cache hit - zwróć z cache
        if (response) {
          return response;
        }
        
        // Klonuj żądanie
        const fetchRequest = event.request.clone();
        
        return fetch(fetchRequest).then(response => {
          // Sprawdź czy odpowiedź jest prawidłowa
          if (!response || response.status !== 200 || response.type !== 'basic') {
            return response;
          }
          
          // Klonuj odpowiedź
          const responseToCache = response.clone();
          
          caches.open(CACHE_NAME)
            .then(cache => {
              cache.put(event.request, responseToCache);
            });
          
          return response;
        });
      })
      .catch(() => {
        // Offline fallback
        return caches.match('/offline.html');
      })
  );
});

Strategie cachowania

1. Cache First (Cache, falling back to network)

// Idealna dla zasobów statycznych
async function cacheFirst(request) {
  const cache = await caches.open(CACHE_NAME);
  const cached = await cache.match(request);
  
  if (cached) {
    return cached;
  }
  
  try {
    const response = await fetch(request);
    await cache.put(request, response.clone());
    return response;
  } catch (error) {
    // Obsługa błędu
    return new Response('Offline', { status: 503 });
  }
}

2. Network First (Network, falling back to cache)

// Idealna dla dynamicznych treści
async function networkFirst(request) {
  try {
    const response = await fetch(request);
    const cache = await caches.open(CACHE_NAME);
    await cache.put(request, response.clone());
    return response;
  } catch (error) {
    const cached = await caches.match(request);
    return cached || new Response('Offline', { status: 503 });
  }
}

3. Stale While Revalidate

// Zwraca z cache, ale aktualizuje w tle
async function staleWhileRevalidate(request) {
  const cache = await caches.open(CACHE_NAME);
  const cached = await cache.match(request);
  
  const fetchPromise = fetch(request).then(response => {
    cache.put(request, response.clone());
    return response;
  });
  
  return cached || fetchPromise;
}

Wybór strategii w Service Worker

self.addEventListener('fetch', event => {
  const { request } = event;
  const url = new URL(request.url);
  
  // Różne strategie dla różnych typów zasobów
  if (url.pathname.startsWith('/api/')) {
    // API - Network First
    event.respondWith(networkFirst(request));
  } else if (request.destination === 'image') {
    // Obrazy - Cache First
    event.respondWith(cacheFirst(request));
  } else if (request.mode === 'navigate') {
    // Nawigacja - Stale While Revalidate
    event.respondWith(staleWhileRevalidate(request));
  } else {
    // Domyślnie - Cache First
    event.respondWith(cacheFirst(request));
  }
});

Implementacja PWA w Next.js

Instalacja i konfiguracja

# Instalacja niezbędnych pakietów
npm install next-pwa
npm install --save-dev @types/node

Konfiguracja next.config.js

// next.config.js
const withPWA = require('next-pwa')({
  dest: 'public',
  register: true,
  skipWaiting: true,
  disable: process.env.NODE_ENV === 'development',
  buildExcludes: [/middleware-manifest.json$/],
  runtimeCaching: [
    {
      urlPattern: /^https:\/\/fonts\.(?:googleapis|gstatic)\.com\/.*/i,
      handler: 'CacheFirst',
      options: {
        cacheName: 'google-fonts',
        expiration: {
          maxEntries: 4,
          maxAgeSeconds: 365 * 24 * 60 * 60 // 1 rok
        }
      }
    },
    {
      urlPattern: /^https:\/\/api\.example\.com\/.*/i,
      handler: 'NetworkFirst',
      options: {
        cacheName: 'api-cache',
        networkTimeoutSeconds: 10,
        expiration: {
          maxEntries: 50,
          maxAgeSeconds: 24 * 60 * 60 // 1 dzień
        }
      }
    },
    {
      urlPattern: /\.(?:jpg|jpeg|gif|png|svg|ico|webp)$/i,
      handler: 'StaleWhileRevalidate',
      options: {
        cacheName: 'images',
        expiration: {
          maxEntries: 60,
          maxAgeSeconds: 30 * 24 * 60 * 60 // 30 dni
        }
      }
    }
  ]
})

module.exports = withPWA({
  reactStrictMode: true,
  swcMinify: true,
  // Inne konfiguracje Next.js
})

Manifest aplikacji

// public/manifest.json
{
  "name": "Zagor Digital PWA",
  "short_name": "Zagor PWA",
  "description": "Profesjonalne strony internetowe z trybem offline",
  "theme_color": "#FF6B35",
  "background_color": "#ffffff",
  "display": "standalone",
  "orientation": "portrait",
  "scope": "/",
  "start_url": "/?source=pwa",
  "icons": [
    {
      "src": "/icons/icon-72x72.png",
      "sizes": "72x72",
      "type": "image/png"
    },
    {
      "src": "/icons/icon-96x96.png",
      "sizes": "96x96",
      "type": "image/png"
    },
    {
      "src": "/icons/icon-128x128.png",
      "sizes": "128x128",
      "type": "image/png"
    },
    {
      "src": "/icons/icon-144x144.png",
      "sizes": "144x144",
      "type": "image/png"
    },
    {
      "src": "/icons/icon-152x152.png",
      "sizes": "152x152",
      "type": "image/png"
    },
    {
      "src": "/icons/icon-192x192.png",
      "sizes": "192x192",
      "type": "image/png",
      "purpose": "any maskable"
    },
    {
      "src": "/icons/icon-384x384.png",
      "sizes": "384x384",
      "type": "image/png"
    },
    {
      "src": "/icons/icon-512x512.png",
      "sizes": "512x512",
      "type": "image/png"
    }
  ]
}

Dodanie meta tagów w _document.tsx

// src/pages/_document.tsx lub src/app/layout.tsx
import { Html, Head, Main, NextScript } from 'next/document'

export default function Document() {
  return (
    <Html lang="pl">
      <Head>
        <link rel="manifest" href="/manifest.json" />
        <link rel="apple-touch-icon" href="/icons/icon-192x192.png" />
        <meta name="theme-color" content="#FF6B35" />
        <meta name="apple-mobile-web-app-capable" content="yes" />
        <meta name="apple-mobile-web-app-status-bar-style" content="default" />
        <meta name="apple-mobile-web-app-title" content="Zagor PWA" />
        <meta name="mobile-web-app-capable" content="yes" />
        <meta name="msapplication-TileColor" content="#FF6B35" />
        <meta name="msapplication-tap-highlight" content="no" />
      </Head>
      <body>
        <Main />
        <NextScript />
      </body>
    </Html>
  )
}

Zaawansowane funkcje offline

Background Sync

// Rejestracja Background Sync
async function registerBackgroundSync() {
  const registration = await navigator.serviceWorker.ready;
  
  try {
    await registration.sync.register('sync-data');
    console.log('Background sync zarejestrowany');
  } catch (error) {
    console.log('Background sync nie jest wspierany');
  }
}

// W Service Worker
self.addEventListener('sync', event => {
  if (event.tag === 'sync-data') {
    event.waitUntil(syncData());
  }
});

async function syncData() {
  // Pobierz dane z IndexedDB
  const pendingRequests = await getPendingRequests();
  
  for (const request of pendingRequests) {
    try {
      await fetch(request.url, request.options);
      await removePendingRequest(request.id);
    } catch (error) {
      console.error('Błąd synchronizacji:', error);
    }
  }
}

Offline Analytics

// Śledzenie zdarzeń offline
class OfflineAnalytics {
  constructor() {
    this.queue = [];
    this.db = null;
    this.initDB();
  }
  
  async initDB() {
    const request = indexedDB.open('analytics', 1);
    
    request.onerror = () => console.error('Błąd IndexedDB');
    
    request.onsuccess = event => {
      this.db = event.target.result;
    };
    
    request.onupgradeneeded = event => {
      const db = event.target.result;
      if (!db.objectStoreNames.contains('events')) {
        db.createObjectStore('events', { keyPath: 'id', autoIncrement: true });
      }
    };
  }
  
  async trackEvent(category, action, label, value) {
    const event = {
      category,
      action,
      label,
      value,
      timestamp: Date.now(),
      offline: !navigator.onLine
    };
    
    if (navigator.onLine) {
      await this.sendToAnalytics(event);
    } else {
      await this.queueEvent(event);
    }
  }
  
  async queueEvent(event) {
    const transaction = this.db.transaction(['events'], 'readwrite');
    const store = transaction.objectStore('events');
    await store.add(event);
  }
  
  async syncEvents() {
    const transaction = this.db.transaction(['events'], 'readonly');
    const store = transaction.objectStore('events');
    const events = await store.getAll();
    
    for (const event of events) {
      await this.sendToAnalytics(event);
      await this.removeEvent(event.id);
    }
  }
}

Optymalizacja wydajności PWA

1. Inteligentne precaching

// Precaching tylko krytycznych zasobów
const criticalAssets = [
  '/',
  '/offline.html',
  '/css/critical.css',
  '/js/app.js'
];

// Lazy loading pozostałych zasobów
self.addEventListener('install', event => {
  event.waitUntil(
    caches.open(CACHE_NAME).then(cache => {
      // Najpierw krytyczne zasoby
      return cache.addAll(criticalAssets).then(() => {
        // Następnie pozostałe w tle
        return cache.addAll(nonCriticalAssets).catch(() => {
          // Nie blokuj instalacji jeśli nie-krytyczne zasoby zawiodą
        });
      });
    })
  );
});

2. Cache versioning i update

// Strategia wersjonowania cache
const CACHE_VERSION = 'v2';
const CACHE_PREFIX = 'zagor-pwa-';
const CACHE_NAME = CACHE_PREFIX + CACHE_VERSION;

// Automatyczne czyszczenie starych wersji
self.addEventListener('activate', event => {
  event.waitUntil(
    caches.keys().then(cacheNames => {
      return Promise.all(
        cacheNames
          .filter(cacheName => cacheName.startsWith(CACHE_PREFIX))
          .filter(cacheName => cacheName !== CACHE_NAME)
          .map(cacheName => caches.delete(cacheName))
      );
    })
  );
});

3. Adaptive loading

// Dostosowanie strategii do warunków sieci
async function adaptiveStrategy(request) {
  const connection = navigator.connection || navigator.mozConnection || navigator.webkitConnection;
  
  if (connection) {
    // Wolne połączenie - priorytet cache
    if (connection.effectiveType === '2g' || connection.saveData) {
      return cacheFirst(request);
    }
    
    // Szybkie połączenie - priorytet sieci
    if (connection.effectiveType === '4g') {
      return networkFirst(request);
    }
  }
  
  // Domyślnie - stale while revalidate
  return staleWhileRevalidate(request);
}

Bezpieczeństwo PWA

Najlepsze praktyki bezpieczeństwa

Praktyka Implementacja
HTTPS Enforcement Wymuszaj HTTPS dla wszystkich zasobów PWA
Content Security Policy Ustaw CSP headers dla Service Workers
Sanityzacja danych Waliduj cache przed użyciem
Ograniczenia CORS Kontroluj dostęp do zewnętrznych API
Token expiration Automatyczne odświeżanie tokenów w cache

Przykład bezpiecznego Service Worker

// Bezpieczna obsługa wrażliwych danych
self.addEventListener('fetch', event => {
  const url = new URL(event.request.url);
  
  // Nie cachuj wrażliwych endpointów
  const sensitiveRoutes = ['/api/user', '/api/payment', '/api/auth'];
  if (sensitiveRoutes.some(route => url.pathname.startsWith(route))) {
    event.respondWith(fetch(event.request));
    return;
  }
  
  // Sprawdź pochodzenie żądania
  if (url.origin !== self.location.origin) {
    // Zewnętrzne żądanie - zastosuj politykę CORS
    event.respondWith(handleCORSRequest(event.request));
    return;
  }
  
  // Normalna obsługa dla bezpiecznych zasobów
  event.respondWith(handleRequest(event.request));
});

Testowanie trybu offline

1. Chrome DevTools

// Symulacja offline w konsoli
// 1. Otwórz DevTools (F12)
// 2. Zakładka Network
// 3. Zaznacz "Offline"
// 4. Odśwież stronę

// Programowe testowanie
if ('serviceWorker' in navigator) {
  navigator.serviceWorker.ready.then(registration => {
    // Sprawdź stan
    console.log('SW Active:', registration.active);
    
    // Wymuś update
    registration.update();
  });
}

2. Lighthouse PWA Audit

# Uruchom audit PWA
npx lighthouse https://localhost:3000 --view

# Lub w Chrome DevTools
# Zakładka Lighthouse > Generate report

3. Testy jednostkowe Service Worker

// __tests__/service-worker.test.js
import { mockServiceWorker } from 'msw';

describe('Service Worker', () => {
  beforeEach(() => {
    global.caches = {
      open: jest.fn(),
      match: jest.fn()
    };
  });
  
  test('should cache critical assets on install', async () => {
    const cache = {
      addAll: jest.fn()
    };
    global.caches.open.mockResolvedValue(cache);
    
    // Trigger install event
    await serviceWorker.install();
    
    expect(cache.addAll).toHaveBeenCalledWith(
      expect.arrayContaining(['/index.html', '/offline.html'])
    );
  });
});

Monitorowanie i analityka

Metryki PWA

// Monitorowanie wydajności cache
class CacheMetrics {
  constructor() {
    this.hits = 0;
    this.misses = 0;
    this.errors = 0;
  }
  
  async trackCachePerformance(request) {
    const startTime = performance.now();
    
    try {
      const cachedResponse = await caches.match(request);
      const endTime = performance.now();
      
      if (cachedResponse) {
        this.hits++;
        this.logMetric('cache_hit', endTime - startTime);
      } else {
        this.misses++;
        this.logMetric('cache_miss', endTime - startTime);
      }
      
      return cachedResponse;
    } catch (error) {
      this.errors++;
      this.logMetric('cache_error', 0);
      throw error;
    }
  }
  
  getCacheHitRate() {
    const total = this.hits + this.misses;
    return total > 0 ? (this.hits / total) * 100 : 0;
  }
}

Najczęściej zadawane pytania

Czy PWA działa na iOS?

Tak, ale z pewnymi ograniczeniami. iOS wspiera Service Workers od wersji 11.3, ale nie wszystkie API są dostępne (np. instalacja z Safari jest ograniczona).

Jak duży może być cache PWA?

Limity różnią się między przeglądarkami. Chrome pozwala na około 6% dostępnego miejsca na dysku, Firefox podobnie. Zawsze monitoruj użycie quota API.

Czy PWA zastąpi aplikacje natywne?

PWA są świetne dla większości przypadków użycia, ale aplikacje natywne nadal mają przewagę w dostępie do niektórych API systemowych i wydajności.

Jak zaktualizować Service Worker?

Service Worker automatycznie sprawdza aktualizacje przy każdej nawigacji. Możesz też wymusić update przez registration.update().

Czy mogę używać PWA bez HTTPS?

Nie w produkcji. Service Workers wymagają HTTPS ze względów bezpieczeństwa. Wyjątek to localhost podczas developmentu.

Jak debugować Service Worker?

Użyj Chrome DevTools > Application > Service Workers. Możesz też użyć chrome://inspect/#service-workers.

Czy PWA wpływa na SEO?

PWA może poprawić SEO poprzez lepszą wydajność i user experience. Google preferuje szybkie, responsywne strony.

Podsumowanie i kolejne kroki

Progressive Web Apps z trybem offline to przyszłość aplikacji webowych. Kluczowe elementy sukcesu to przemyślana strategia cachowania, bezpieczeństwo i monitoring wydajności. Service Workers dają nam potężne narzędzia do tworzenia aplikacji działających niezależnie od połączenia internetowego.

Następne kroki:

  1. Zainstaluj next-pwa w swoim projekcie
  2. Skonfiguruj podstawowy Service Worker
  3. Zdefiniuj strategie cachowania dla różnych typów zasobów
  4. Przetestuj tryb offline w różnych scenariuszach
  5. Monitoruj metryki i optymalizuj

Potrzebujesz pomocy we wdrożeniu PWA dla swojej firmy? Skontaktuj się z nami - zespół Zagor Digital pomoże stworzyć aplikację, która działa zawsze i wszędzie.


Ostatnia aktualizacja: 26 stycznia 2025

Tworzenie Stron WWW

Zamow profesjonalna strone internetowa

Tagi:

PWA
Progressive Web App
offline mode
Service Worker
Next.js
cachowanie

Udostępnij artykuł:

ZD

Zagor Digital

Eksperci w dziedzinie marketingu internetowego i pozycjonowania lokalnego.

Tworzenie Stron WWW

Zamow profesjonalna strone internetowa

Porady SEO co tydzien

Dolacz do newslettera i otrzymuj sprawdzone strategie pozycjonowania.