Używamy cookies, żeby zwiększyć Twoje doświadczenia na stronie
CodeWorlds

Progressive Web Apps i Service Workers

Progressive Web Apps (PWA) to nowoczesne aplikacje webowe, które łączą najlepsze cechy aplikacji internetowych i natywnych. Next.js oferuje doskonale wsparcie dla PWA, pozwalając na tworzenie szybkich, niezawodnych i angażujących aplikacji, które działają także offline. W tym module poznamy, jak zaimplementować PWA w Next.js przy użyciu Service Workers.

Czym są Progressive Web Apps?

Progressive Web Apps to aplikacje webowe, które wykorzystują nowoczesne technologie webowe, aby zapewnić użytkownikom doświadczenie podobne do aplikacji natywnych. Kluczowe cechy PWA to:

  1. Progressive - działają dla każdego użytkownika, niezależnie od przeglądarki
  2. Responsive - dopasowują się do różnych rozmiarów ekranów
  3. Connectivity Independent - działają offline lub przy słabym połączeniu
  4. App-like - zapewniają nawigację i interakcje jak w aplikacjach natywnych
  5. Fresh - zawsze aktualne dzięki Service Worker
  6. Safe - serwowane przez HTTPS
  7. Discoverable - identyfikowane jako "aplikacje" przez wyszukiwarki
  8. Re-engageable - umożliwiają ponowne zaangażowanie przez push notifications
  9. Installable - mogą być zainstalowane na urządzeniu bez app store
  10. Linkable - łatwe udostepnianie przez URL

Service Workers - fundament PWA

Service Workers to skrypty JavaScript, które działają w tle, niezależnie od głównego wątku aplikacji. Pozwalają na:

  • Cachowanie zasobów dla pracy offline
  • Przechwytywanie i modyfikowanie żądań sieciowych
  • Wysyłanie push notifications
  • Synchronizację w tle
  • Instalację aplikacji

Cykl życia Service Worker

  1. Registration - rejestracja Service Worker
  2. Installation - instalacja i konfiguracja cache
  3. Activation - aktywacja i czyszczenie starych cache
  4. Fetch - przechwytywanie żądań sieciowych
  5. Update - aktualizacja do nowej wersji

Implementacja PWA w Next.js

1. Instalacja i konfiguracja next-pwa

Najprostszym sposobem dodania funkcjonalności PWA do aplikacji Next.js jest użycie pakietu

next-pwa
:

1npm install next-pwa
2# lub
3yarn add next-pwa

Zaktualizuj

next.config.js
:

1// next.config.js
2const withPWA = require('next-pwa')({
3  dest: 'public',
4  register: true,
5  skipWaiting: true,
6  disable: process.env.NODE_ENV === 'development', // Wyłącz w development
7  runtimeCaching: [
8    {
9      urlPattern: /^https?.*/,
10      handler: 'NetworkFirst',
11      options: {
12        cacheName: 'offlineCache',
13        expiration: {
14          maxEntries: 200,
15          maxAgeSeconds: 24 * 60 * 60, // 24 godziny
16        },
17      },
18    },
19  ],
20});
21
22module.exports = withPWA({
23  // Twoja standardowa konfiguracja Next.js
24  reactStrictMode: true,
25  swcMinify: true,
26});

2. Manifest aplikacji

Manifest to plik JSON, który zawiera metadane o aplikacji PWA. Utwórz plik

public/manifest.json
:

1{
2  "name": "Moja Aplikacja PWA",
3  "short_name": "MojaApp",
4  "description": "Przykładowa aplikacja Progressive Web App zbudowana z Next.js",
5  "start_url": "/",
6  "display": "standalone",
7  "background_color": "#ffffff",
8  "theme_color": "#000000",
9  "orientation": "portrait-primary",
10  "icons": [
11    {
12      "src": "/icons/icon-72x72.png",
13      "sizes": "72x72",
14      "type": "image/png",
15      "purpose": "maskable any"
16    },
17    {
18      "src": "/icons/icon-96x96.png",
19      "sizes": "96x96",
20      "type": "image/png",
21      "purpose": "maskable any"
22    },
23    {
24      "src": "/icons/icon-128x128.png",
25      "sizes": "128x128",
26      "type": "image/png",
27      "purpose": "maskable any"
28    },
29    {
30      "src": "/icons/icon-144x144.png",
31      "sizes": "144x144",
32      "type": "image/png",
33      "purpose": "maskable any"
34    },
35    {
36      "src": "/icons/icon-152x152.png",
37      "sizes": "152x152",
38      "type": "image/png",
39      "purpose": "maskable any"
40    },
41    {
42      "src": "/icons/icon-192x192.png",
43      "sizes": "192x192",
44      "type": "image/png",
45      "purpose": "maskable any"
46    },
47    {
48      "src": "/icons/icon-384x384.png",
49      "sizes": "384x384",
50      "type": "image/png",
51      "purpose": "maskable any"
52    },
53    {
54      "src": "/icons/icon-512x512.png",
55      "sizes": "512x512",
56      "type": "image/png",
57      "purpose": "maskable any"
58    }
59  ],
60  "categories": ["productivity", "utilities"],
61  "screenshots": [
62    {
63      "src": "/screenshots/desktop.png",
64      "sizes": "1280x720",
65      "type": "image/png",
66      "form_factor": "wide"
67    },
68    {
69      "src": "/screenshots/mobile.png",
70      "sizes": "750x1334",
71      "type": "image/png",
72      "form_factor": "narrow"
73    }
74  ]
75}

3. Dodanie meta tagów do HTML

Zaktualizuj główny layout aplikacji, aby dodać niezbędne meta tagi:

1// app/layout.tsx
2import type { Metadata, Viewport } from 'next';
3
4export const metadata: Metadata = {
5  title: 'Moja Aplikacja PWA',
6  description: 'Przykładowa aplikacja Progressive Web App',
7  generator: 'Next.js',
8  manifest: '/manifest.json',
9  keywords: ['nextjs', 'pwa', 'react'],
10  authors: [
11    { name: 'Twój Zespół' },
12  ],
13  creator: 'Twój Zespół',
14  formatDetection: {
15    email: false,
16    address: false,
17    telephone: false,
18  },
19  metadataBase: new URL('https://twoja-domena.com'),
20  alternates: {
21    canonical: '/',
22  },
23  openGraph: {
24    type: 'website',
25    siteName: 'Moja Aplikacja PWA',
26    title: {
27      default: 'Moja Aplikacja PWA',
28      template: '%s | Moja Aplikacja PWA',
29    },
30    description: 'Przykładowa aplikacja Progressive Web App',
31  },
32  twitter: {
33    card: 'summary',
34    title: {
35      default: 'Moja Aplikacja PWA',
36      template: '%s | Moja Aplikacja PWA',
37    },
38    description: 'Przykładowa aplikacja Progressive Web App',
39  },
40  appleWebApp: {
41    title: 'Moja Aplikacja PWA',
42    statusBarStyle: 'default',
43    capable: true,
44  },
45};
46
47export const viewport: Viewport = {
48  themeColor: [
49    { media: '(prefers-color-scheme: light)', color: '#ffffff' },
50    { media: '(prefers-color-scheme: dark)', color: '#000000' },
51  ],
52  minimumScale: 1,
53  initialScale: 1,
54  width: 'device-width',
55  shrinkToFit: false,
56  viewportFit: 'cover',
57};
58
59export default function RootLayout({
60  children,
61}: {
62  children: React.ReactNode;
63}) {
64  return (
65    <html lang="pl">
66      <head>
67        <link rel="icon" href="/favicon.ico" />
68        <link rel="apple-touch-icon" href="/icons/icon-192x192.png" />
69      </head>
70      <body>
71        {children}
72      </body>
73    </html>
74  );
75}

Własna implementacja Service Worker

Jeśli potrzebujesz większej kontroli nad Service Worker, możesz stworzyć własną implementację:

1. Utworzenie Service Worker

1// public/sw.js
2const CACHE_NAME = 'moja-app-v1';
3const urlsToCache = [
4  '/',
5  '/static/js/bundle.js',
6  '/static/css/main.css',
7  '/manifest.json',
8];
9
10// Install event - cachowanie zasobów
11self.addEventListener('install', (event) => {
12  event.waitUntil(
13    caches.open(CACHE_NAME)
14      .then((cache) => {
15        console.log('Cache otwarty');
16        return cache.addAll(urlsToCache);
17      })
18      .then(() => {
19        // Wymusza aktywację nowego Service Worker
20        return self.skipWaiting();
21      })
22  );
23});
24
25// Activate event - czyszczenie starych cache
26self.addEventListener('activate', (event) => {
27  event.waitUntil(
28    caches.keys().then((cacheNames) => {
29      return Promise.all(
30        cacheNames.map((cacheName) => {
31          if (cacheName !== CACHE_NAME) {
32            console.log('Usuwanie starego cache:', cacheName);
33            return caches.delete(cacheName);
34          }
35        })
36      );
37    }).then(() => {
38      // Przejmij kontrolę nad wszystkimi klientami
39      return self.clients.claim();
40    })
41  );
42});
43
44// Fetch event - strategia cache-first
45self.addEventListener('fetch', (event) => {
46  event.respondWith(
47    caches.match(event.request)
48      .then((response) => {
49        // Zwróć z cache jeśli dostępne
50        if (response) {
51          return response;
52        }
53        
54        // W przeciwnym razie pobierz z sieci
55        return fetch(event.request).then((response) => {
56          // Sprawdź, czy otrzymaliśmy poprawną odpowiedź
57          if (!response || response.status !== 200 || response.type !== 'basic') {
58            return response;
59          }
60          
61          // Sklonuj odpowiedź
62          const responseToCache = response.clone();
63          
64          caches.open(CACHE_NAME)
65            .then((cache) => {
66              cache.put(event.request, responseToCache);
67            });
68          
69          return response;
70        }).catch(() => {
71          // Jeśli żądanie nie powiodło się, zwróć stronę offline
72          if (event.request.destination === 'document') {
73            return caches.match('/offline.html');
74          }
75        });
76      })
77  );
78});
79
80// Background sync
81self.addEventListener('sync', (event) => {
82  if (event.tag === 'background-sync') {
83    event.waitUntil(doBackgroundSync());
84  }
85});
86
87function doBackgroundSync() {
88  // Implementacja synchronizacji w tle
89  return fetch('/api/sync')
90    .then(response => response.json())
91    .then(data => {
92      console.log('Background sync completed:', data);
93    })
94    .catch(err => {
95      console.error('Background sync failed:', err);
96    });
97}

2. Rejestracja Service Worker

1// lib/serviceWorker.ts
2export function registerSW() {
3  if (typeof window !== 'undefined' && 'serviceWorker' in navigator) {
4    window.addEventListener('load', () => {
5      navigator.serviceWorker.register('/sw.js')
6        .then((registration) => {
7          console.log('SW zarejestrowany: ', registration);
8          
9          // Sprawdzenie aktualizacji
10          registration.addEventListener('updatefound', () => {
11            const newWorker = registration.installing;
12            if (newWorker) {
13              newWorker.addEventListener('statechange', () => {
14                if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
15                  // Nowy Service Worker jest dostępny
16                  showUpdateAvailableNotification();
17                }
18              });
19            }
20          });
21        })
22        .catch((error) => {
23          console.log('Rejestracja SW nie powiodła się: ', error);
24        });
25    });
26  }
27}
28
29export function unregisterSW() {
30  if (typeof window !== 'undefined' && 'serviceWorker' in navigator) {
31    navigator.serviceWorker.ready.then((registration) => {
32      registration.unregister();
33    });
34  }
35}
36
37function showUpdateAvailableNotification() {
38  // Pokaż powiadomienie o dostępnej aktualizacji
39  if (confirm('Nowa wersja aplikacji jest dostępna. Czy chcesz ją zaaktualizować?')) {
40    window.location.reload();
41  }
42}

3. Użycie w komponencie

1// app/page.tsx
2'use client';
3
4import { useEffect } from 'react';
5import { registerSW } from '@/lib/serviceWorker';
6
7export default function HomePage() {
8  useEffect(() => {
9    registerSW();
10  }, []);
11  
12  return (
13    <div>
14      <h1>Moja Aplikacja PWA</h1>
15      <p>Witaj w aplikacji Progressive Web App!</p>
16    </div>
17  );
18}

Zaawansowane strategie cachowania

Różne typy zawartości wymagają różnych strategii cachowania:

1. Cache First - dla statycznych zasobów

1// Strategia Cache First
2self.addEventListener('fetch', (event) => {
3  if (event.request.destination === 'image' || 
4      event.request.destination === 'style' || 
5      event.request.destination === 'script') {
6    event.respondWith(
7      caches.match(event.request).then((cachedResponse) => {
8        return cachedResponse || fetch(event.request).then((response) => {
9          const responseClone = response.clone();
10          caches.open(CACHE_NAME).then((cache) => {
11            cache.put(event.request, responseClone);
12          });
13          return response;
14        });
15      })
16    );
17  }
18});

2. Network First - dla dynamycznej zawartości

1// Strategia Network First
2self.addEventListener('fetch', (event) => {
3  if (event.request.url.includes('/api/')) {
4    event.respondWith(
5      fetch(event.request)
6        .then((response) => {
7          const responseClone = response.clone();
8          caches.open(CACHE_NAME).then((cache) => {
9            cache.put(event.request, responseClone);
10          });
11          return response;
12        })
13        .catch(() => {
14          return caches.match(event.request);
15        })
16    );
17  }
18});

3. Stale While Revalidate - dla często zmieniającej się zawartości

1// Strategia Stale While Revalidate
2self.addEventListener('fetch', (event) => {
3  if (event.request.url.includes('/news/')) {
4    event.respondWith(
5      caches.match(event.request).then((cachedResponse) => {
6        const fetchPromise = fetch(event.request).then((networkResponse) => {
7          caches.open(CACHE_NAME).then((cache) => {
8            cache.put(event.request, networkResponse.clone());
9          });
10          return networkResponse;
11        });
12        
13        return cachedResponse || fetchPromise;
14      })
15    );
16  }
17});

Implementacja funkcji offline

1. Strona offline

Utwórz dedykowaną stronę offline:

1// app/offline/page.tsx
2export default function OfflinePage() {
3  return (
4    <div className="min-h-screen flex items-center justify-center bg-gray-100">
5      <div className="text-center">
6        <div className="mb-4">
7          <svg className="mx-auto h-16 w-16 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
8            <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M18.364 5.636l-3.536 3.536m0 5.656l3.536 3.536M9.172 9.172L5.636 5.636m3.536 9.192L5.636 18.364M12 2.25a9.75 9.75 0 1 0 0 19.5 9.75 9.75 0 0 0 0-19.5Z" />
9          </svg>
10        </div>
11        <h1 className="text-2xl font-bold text-gray-900 mb-2">Jesteś offline</h1>
12        <p className="text-gray-600 mb-4">
13          Nie możemy połączyć się z internetem. Sprawdź połączenie i spróbuj ponownie.
14        </p>
15        <button 
16          onClick={() => window.location.reload()}
17          className="bg-blue-500 hover:bg-blue-600 text-white font-bold py-2 px-4 rounded"
18        >
19          Spróbuj ponownie
20        </button>
21      </div>
22    </div>
23  );
24}

2. Detekcja stanu online/offline

1// hooks/useOnlineStatus.ts
2import { useState, useEffect } from 'react';
3
4export function useOnlineStatus() {
5  const [isOnline, setIsOnline] = useState(true);
6  
7  useEffect(() => {
8    function handleOnline() {
9      setIsOnline(true);
10    }
11    
12    function handleOffline() {
13      setIsOnline(false);
14    }
15    
16    window.addEventListener('online', handleOnline);
17    window.addEventListener('offline', handleOffline);
18    
19    // Sprawdź początkowy stan
20    setIsOnline(navigator.onLine);
21    
22    return () => {
23      window.removeEventListener('online', handleOnline);
24      window.removeEventListener('offline', handleOffline);
25    };
26  }, []);
27  
28  return isOnline;
29}

3. Komponent wskaźnika stanu połączenia

1// components/OnlineStatusIndicator.tsx
2'use client';
3
4import { useOnlineStatus } from '@/hooks/useOnlineStatus';
5
6export default function OnlineStatusIndicator() {
7  const isOnline = useOnlineStatus();
8  
9  if (isOnline) {
10    return null; // Nie pokazuj niczego gdy jest połączenie
11  }
12  
13  return (
14    <div className="fixed top-0 left-0 right-0 bg-red-500 text-white text-center py-2 z-50">
15      <span className="text-sm font-medium">
16        📡 Brak połączenia z internetem - działasz w trybie offline
17      </span>
18    </div>
19  );
20}

Push Notifications

Push notifications pozwalają na ponowne zaangażowanie użytkowników:

1. Rejestracja subskrypcji

1// lib/notifications.ts
2
3// Funkcja do konwersji VAPID key
4function urlBase64ToUint8Array(base64String: string) {
5  const padding = '='.repeat((4 - base64String.length % 4) % 4);
6  const base64 = (base64String + padding)
7    .replace(/-/g, '+')
8    .replace(/_/g, '/');
9  
10  const rawData = window.atob(base64);
11  const outputArray = new Uint8Array(rawData.length);
12  
13  for (let i = 0; i < rawData.length; ++i) {
14    outputArray[i] = rawData.charCodeAt(i);
15  }
16  return outputArray;
17}
18
19export async function subscribeToPushNotifications() {
20  if (!('serviceWorker' in navigator) || !('PushManager' in window)) {
21    throw new Error('Push notifications are not supported');
22  }
23  
24  const registration = await navigator.serviceWorker.ready;
25  
26  // Sprawdź, czy już jest subskrypcja
27  let subscription = await registration.pushManager.getSubscription();
28  
29  if (!subscription) {
30    const vapidPublicKey = process.env.NEXT_PUBLIC_VAPID_PUBLIC_KEY;
31    if (!vapidPublicKey) {
32      throw new Error('VAPID public key is not configured');
33    }
34    
35    subscription = await registration.pushManager.subscribe({
36      userVisibleOnly: true,
37      applicationServerKey: urlBase64ToUint8Array(vapidPublicKey),
38    });
39  }
40  
41  // Wyślij subskrypcję na serwer
42  await fetch('/api/notifications/subscribe', {
43    method: 'POST',
44    headers: {
45      'Content-Type': 'application/json',
46    },
47    body: JSON.stringify(subscription),
48  });
49  
50  return subscription;
51}
52
53export async function unsubscribeFromPushNotifications() {
54  const registration = await navigator.serviceWorker.ready;
55  const subscription = await registration.pushManager.getSubscription();
56  
57  if (subscription) {
58    await subscription.unsubscribe();
59    
60    // Powiadom serwer o anulowaniu subskrypcji
61    await fetch('/api/notifications/unsubscribe', {
62      method: 'POST',
63      headers: {
64        'Content-Type': 'application/json',
65      },
66      body: JSON.stringify(subscription),
67    });
68  }
69}

2. Obsługa push notifications w Service Worker

1// public/sw.js (dodatek do wcześniejszego kodu)
2
3// Obsługa push notifications
4self.addEventListener('push', (event) => {
5  const options = {
6    body: 'Masz nową wiadomość!',
7    icon: '/icons/icon-192x192.png',
8    badge: '/icons/badge-72x72.png',
9    vibrate: [100, 50, 100],
10    data: {
11      dateOfArrival: Date.now(),
12      primaryKey: 1
13    },
14    actions: [
15      {
16        action: 'explore',
17        title: 'Zobacz więcej',
18        icon: '/icons/checkmark.png'
19      },
20      {
21        action: 'close',
22        title: 'Zamknij',
23        icon: '/icons/xmark.png'
24      }
25    ]
26  };
27  
28  if (event.data) {
29    const data = event.data.json();
30    options.body = data.body || options.body;
31    options.data = { ...options.data, ...data.data };
32  }
33  
34  event.waitUntil(
35    self.registration.showNotification('Moja Aplikacja PWA', options)
36  );
37});
38
39// Obsługa kliknięcia w notification
40self.addEventListener('notificationclick', (event) => {
41  event.notification.close();
42  
43  if (event.action === 'explore') {
44    // Otwórz określoną stronę
45    event.waitUntil(
46      clients.openWindow('/notifications')
47    );
48  } else if (event.action === 'close') {
49    // Zamknij powiadomienie
50    return;
51  } else {
52    // Domyślna akcja - otwórz główną stronę
53    event.waitUntil(
54      clients.openWindow('/')
55    );
56  }
57});

3. API endpoint do wysyłania powiadomień

1// app/api/notifications/send/route.ts
2import { NextRequest, NextResponse } from 'next/server';
3import webpush from 'web-push';
4import { prisma } from '@/lib/prisma';
5
6// Konfiguracja VAPID
7webpush.setVapidDetails(
8  'mailto:your-email@example.com',
9  process.env.VAPID_PUBLIC_KEY!,
10  process.env.VAPID_PRIVATE_KEY!
11);
12
13export async function POST(request: NextRequest) {
14  try {
15    const { title, body, url } = await request.json();
16    
17    // Pobierz wszystkie subskrypcje z bazy danych
18    const subscriptions = await prisma.pushSubscription.findMany();
19    
20    const payload = JSON.stringify({
21      title,
22      body,
23      url,
24      icon: '/icons/icon-192x192.png',
25    });
26    
27    // Wyślij powiadomienia do wszystkich subskrypcji
28    const promises = subscriptions.map(async (subscription) => {
29      try {
30        await webpush.sendNotification(
31          {
32            endpoint: subscription.endpoint,
33            keys: {
34              p256dh: subscription.p256dh,
35              auth: subscription.auth,
36            },
37          },
38          payload
39        );
40      } catch (error) {
41        console.error('Error sending notification:', error);
42        
43        // Jeśli subskrypcja jest nieważna, usuń ją
44        if (error.statusCode === 410) {
45          await prisma.pushSubscription.delete({
46            where: { id: subscription.id },
47          });
48        }
49      }
50    });
51    
52    await Promise.all(promises);
53    
54    return NextResponse.json({ success: true });
55  } catch (error) {
56    console.error('Error sending notifications:', error);
57    return NextResponse.json(
58      { error: 'Failed to send notifications' },
59      { status: 500 }
60    );
61  }
62}

Instalacja aplikacji (Add to Home Screen)

1. Komponent InstallPrompt

1// components/InstallPrompt.tsx
2'use client';
3
4import { useState, useEffect } from 'react';
5
6interface BeforeInstallPromptEvent extends Event {
7  readonly platforms: string[];
8  readonly userChoice: Promise<{
9    outcome: 'accepted' | 'dismissed';
10    platform: string;
11  }>;
12  prompt(): Promise<void>;
13}
14
15export default function InstallPrompt() {
16  const [deferredPrompt, setDeferredPrompt] = useState<BeforeInstallPromptEvent | null>(null);
17  const [showInstallButton, setShowInstallButton] = useState(false);
18  
19  useEffect(() => {
20    const handler = (e: Event) => {
21      // Zapobiegaj automatycznego wyświetlania promptu
22      e.preventDefault();
23      setDeferredPrompt(e as BeforeInstallPromptEvent);
24      setShowInstallButton(true);
25    };
26    
27    window.addEventListener('beforeinstallprompt', handler);
28    
29    // Sprawdź, czy aplikacja jest już zainstalowana
30    window.addEventListener('appinstalled', () => {
31      console.log('PWA została zainstalowana');
32      setShowInstallButton(false);
33      setDeferredPrompt(null);
34    });
35    
36    return () => {
37      window.removeEventListener('beforeinstallprompt', handler);
38    };
39  }, []);
40  
41  const handleInstallClick = async () => {
42    if (!deferredPrompt) return;
43    
44    // Pokaż prompt instalacji
45    deferredPrompt.prompt();
46    
47    // Poczekaj na wybór użytkownika
48    const { outcome } = await deferredPrompt.userChoice;
49    
50    if (outcome === 'accepted') {
51      console.log('Użytkownik zaakceptował instalację');
52    } else {
53      console.log('Użytkownik odrzucił instalację');
54    }
55    
56    setDeferredPrompt(null);
57    setShowInstallButton(false);
58  };
59  
60  if (!showInstallButton) {
61    return null;
62  }
63  
64  return (
65    <div className="fixed bottom-4 right-4 bg-blue-500 text-white p-4 rounded-lg shadow-lg">
66      <div className="flex items-center space-x-3">
67        <div>
68          <h3 className="font-semibold">Zainstaluj aplikację</h3>
69          <p className="text-sm opacity-90">Dodaj do ekranu głównego dla szybszego dostępu</p>
70        </div>
71        <div className="flex space-x-2">
72          <button
73            onClick={handleInstallClick}
74            className="bg-white text-blue-500 px-3 py-1 rounded text-sm font-medium"
75          >
76            Zainstaluj
77          </button>
78          <button
79            onClick={() => setShowInstallButton(false)}
80            className="text-white opacity-75 hover:opacity-100"
81          >
8283          </button>
84        </div>
85      </div>
86    </div>
87  );
88}

Testowanie PWA

1. Lighthouse Audit

Lighthouse to narzędzie Google do audytu PWA. Możesz go uruchomić:

  • W Chrome DevTools (zakładka Lighthouse)
  • Z linii komend:
    npm install -g lighthouse
  • Online: https://web.dev/measure/

2. PWA Builder

Microsoft PWA Builder (https://www.pwabuilder.com/) to narzędzie do testowania i budowania PWA.

3. Manualne testowanie

1// utils/pwa-test.ts
2export function testPWAFeatures() {
3  const results = {
4    serviceWorker: 'serviceWorker' in navigator,
5    pushNotifications: 'PushManager' in window,
6    notifications: 'Notification' in window,
7    backgroundSync: 'serviceWorker' in navigator && 'sync' in window.ServiceWorkerRegistration.prototype,
8    installPrompt: true, // Wykrywane dynamicznie
9    offline: 'onLine' in navigator,
10  };
11  
12  console.table(results);
13  return results;
14}

Najlepsze praktyki PWA

1. Performance

  • Używaj odpowiednich strategii cachowania
  • Optymalizuj rozmiar bundle'a
  • Implementuj lazy loading
  • Używaj Service Worker do wstępnego ładowania krytycznych zasobów

2. User Experience

  • Zapewnij płynne przejścia między trybem online/offline
  • Implementuj skeleton screens podczas ładowania
  • Dodaj wskaźniki ładowania
  • Zapewnij jasne komunikaty o stanie połączenia

3. Security

  • Zawsze używaj HTTPS
  • Waliduj wszystkie dane wejściowe
  • Implementuj Content Security Policy (CSP)
  • Regularnie aktualizuj Service Worker

4. Accessibility

  • Zapewnij wsparcie dla czytników ekranu
  • Używaj odpowiednich kontrastów kolorów
  • Implementuj nawigację klawiaturą
  • Dodaj odpowiednie aria-labels

Podsumowanie

Progressive Web Apps to potężna technologia, która pozwala tworzyć aplikacje webowe o funkcjonalności zbliżonej do aplikacji natywnych. Next.js z pakietem next-pwa lub własną implementacją Service Worker oferuje wszystkie narzędzia potrzebne do stworzenia w pełni funkcjonalnej PWA.

Kluczowe elementy implementacji PWA w Next.js:

  1. Service Worker - dla cachowania i pracy offline
  2. Web App Manifest - dla metadanych aplikacji
  3. HTTPS - wymagane dla PWA
  4. Responsive Design - dla różnych urządzeń
  5. Push Notifications - do zaangażowania użytkowników
  6. Install Prompts - dla instalacji na urządzeniu

PWA pozwala na dostarczenie lepszego doświadczenia użytkownika, zwiększenie zaangażowania i poprawę wydajności aplikacji, szczególnie w warunkach słabego połączenia internetowego.

Przejdź do CodeWorlds