Menu

Progressive Web Applications (PWA)

Progressive Web Applications (PWAs) bieten die Reichweite und Zugänglichkeit von Webanwendungen in Kombination mit den Funktionen und der Benutzererfahrung nativer mobiler Apps. Mit Next.js können Sie PWAs erstellen, die auf allen Plattformen ein nahtloses, App-ähnliches Erlebnis bieten, ohne dass mehrere Codebasen oder App Store-Genehmigungen erforderlich sind.

PWAs ermöglichen es Ihnen:

  • Updates sofort bereitzustellen, ohne auf App Store-Genehmigungen warten zu müssen
  • Plattformübergreifende Anwendungen mit einer einzigen Codebasis zu erstellen
  • Native Funktionen wie Startbildschirm-Installation und Push-Benachrichtigungen bereitzustellen

Eine PWA mit Next.js erstellen

1. Erstellen des Web-App-Manifests

Next.js bietet integrierte Unterstützung für die Erstellung eines Web-App-Manifests mit dem App Router. Sie können eine statische oder dynamische Manifest-Datei erstellen:

Erstellen Sie beispielsweise eine app/manifest.ts oder app/manifest.json-Datei:

app/manifest.ts
TypeScript
import type { MetadataRoute } from 'next'
 
export default function manifest(): MetadataRoute.Manifest {
  return {
    name: 'Next.js PWA',
    short_name: 'NextPWA',
    description: 'Eine Progressive Web App gebaut mit Next.js',
    start_url: '/',
    display: 'standalone',
    background_color: '#ffffff',
    theme_color: '#000000',
    icons: [
      {
        src: '/icon-192x192.png',
        sizes: '192x192',
        type: 'image/png',
      },
      {
        src: '/icon-512x512.png',
        sizes: '512x512',
        type: 'image/png',
      },
    ],
  }
}

Diese Datei sollte Informationen über den Namen, die Symbole und wie sie als Symbol auf dem Gerät des Benutzers angezeigt werden soll, enthalten. Dies ermöglicht es Benutzern, Ihre PWA auf ihrem Startbildschirm zu installieren und bietet ein natives App-ähnliches Erlebnis.

Sie können Tools wie Favicon-Generatoren verwenden, um die verschiedenen Symbolsätze zu erstellen und die generierten Dateien in Ihrem public/-Ordner zu platzieren.

2. Implementierung von Web-Push-Benachrichtigungen

Web-Push-Benachrichtigungen werden von allen modernen Browsern unterstützt, einschließlich:

  • iOS 16.4+ für auf den Startbildschirm installierte Anwendungen
  • Safari 16 für macOS 13 oder höher
  • Chromium-basierte Browser
  • Firefox

Dies macht PWAs zu einer tragfähigen Alternative zu nativen Apps. Beachtenswert ist, dass Sie Installationsaufforderungen auslösen können, ohne Offline-Unterstützung zu benötigen.

Web-Push-Benachrichtigungen ermöglichen es Ihnen, Benutzer auch dann wieder zu erreichen, wenn sie Ihre App nicht aktiv nutzen. So implementieren Sie sie in einer Next.js-Anwendung:

Zunächst erstellen wir die Hauptseiten-Komponente in app/page.tsx. Wir unterteilen sie in kleinere Teile für ein besseres Verständnis. Fügen wir zunächst einige der Importe und Hilfsfunktionen hinzu. Es ist in Ordnung, dass die referenzierten Server-Aktionen noch nicht existieren:

'use client'
 
import { useState, useEffect } from 'react'
import { subscribeUser, unsubscribeUser, sendNotification } from './actions'
 
function urlBase64ToUint8Array(base64String: string) {
  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
}
'use client'
 
import { useState, useEffect } from 'react'
import { subscribeUser, unsubscribeUser, sendNotification } from './actions'
 
function 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
}

Fügen wir nun eine Komponente hinzu, um An- und Abmeldung sowie das Senden von Push-Benachrichtigungen zu verwalten.

function PushNotificationManager() {
  const [isSupported, setIsSupported] = useState(false)
  const [subscription, setSubscription] = useState<PushSubscription | null>(
    null
  )
  const [message, setMessage] = useState('')
 
  useEffect(() => {
    if ('serviceWorker' in navigator && 'PushManager' in window) {
      setIsSupported(true)
      registerServiceWorker()
    }
  }, [])
 
  async function registerServiceWorker() {
    const registration = await navigator.serviceWorker.register('/sw.js', {
      scope: '/',
      updateViaCache: 'none',
    })
    const sub = await registration.pushManager.getSubscription()
    setSubscription(sub)
  }
 
  async function subscribeToPush() {
    const registration = await navigator.serviceWorker.ready
    const sub = await registration.pushManager.subscribe({
      userVisibleOnly: true,
      applicationServerKey: urlBase64ToUint8Array(
        process.env.NEXT_PUBLIC_VAPID_PUBLIC_KEY!
      ),
    })
    setSubscription(sub)
    await subscribeUser(sub)
  }
 
  async function unsubscribeFromPush() {
    await subscription?.unsubscribe()
    setSubscription(null)
    await unsubscribeUser()
  }
 
  async function sendTestNotification() {
    if (subscription) {
      await sendNotification(message)
      setMessage('')
    }
  }
 
  if (!isSupported) {
    return <p>Push-Benachrichtigungen werden in diesem Browser nicht unterstützt.</p>
  }
 
  return (
    <div>
      <h3>Push-Benachrichtigungen</h3>
      {subscription ? (
        <>
          <p>Sie sind für Push-Benachrichtigungen angemeldet.</p>
          <button onClick={unsubscribeFromPush}>Abmelden</button>
          <input
            type="text"
            placeholder="Benachrichtigungsnachricht eingeben"
            value={message}
            onChange={(e) => setMessage(e.target.value)}
          />
          <button onClick={sendTestNotification}>Test senden</button>
        </>
      ) : (
        <>
          <p>Sie sind nicht für Push-Benachrichtigungen angemeldet.</p>
          <button onClick={subscribeToPush}>Anmelden</button>
        </>
      )}
    </div>
  )
}
function PushNotificationManager() {
  const [isSupported, setIsSupported] = useState(false);
  const [subscription, setSubscription] = useState(null);
  const [message, setMessage] = useState('');
 
  useEffect(() => {
    if ('serviceWorker' in navigator && 'PushManager' in window) {
      setIsSupported(true);
      registerServiceWorker();
    }
  }, []);
 
  async function registerServiceWorker() {
    const registration = await navigator.serviceWorker.register('/sw.js', {
      scope: '/',
      updateViaCache: 'none',
    });
    const sub = await registration.pushManager.getSubscription();
    setSubscription(sub);
  }
 
  async function subscribeToPush() {
    const registration = await navigator.serviceWorker.ready;
    const sub = await registration.pushManager.subscribe({
      userVisibleOnly: true,
      applicationServerKey: urlBase64ToUint8Array(
        process.env.NEXT_PUBLIC_VAPID_PUBLIC_KEY!
      ),
    });
    setSubscription(sub);
    await subscribeUser(sub);
  }
 
  async function unsubscribeFromPush() {
    await subscription?.unsubscribe();
    setSubscription(null);
    await unsubscribeUser();
  }
 
  async function sendTestNotification() {
    if (subscription) {
      await sendNotification(message);
      setMessage('');
    }
  }
 
  if (!isSupported) {
    return <p>Push-Benachrichtigungen werden in diesem Browser nicht unterstützt.</p>;
  }
 
  return (
    <div>
      <h3>Push-Benachrichtigungen</h3>
      {subscription ? (
        <>
          <p>Sie sind für Push-Benachrichtigungen angemeldet.</p>
          <button onClick={unsubscribeFromPush}>Abmelden</button>
          <input
            type="text"
            placeholder="Benachrichtigungsnachricht eingeben"
            value={message}
            onChange={(e) => setMessage(e.target.value)}
          />
          <button onClick={sendTestNotification}>Test senden</button>
        </>
      ) : (
        <>
          <p>Sie sind nicht für Push-Benachrichtigungen angemeldet.</p>
          <button onClick={subscribeToPush}>Anmelden</button>
        </>
      )}
    </div>
  );
}

Zuerst erstellen wir eine Komponente, um iOS-Geräten eine Nachricht anzuzeigen, die sie auffordert, die App auf ihrem Startbildschirm zu installieren, und diese nur anzuzeigen, wenn die App noch nicht installiert ist.

function InstallPrompt() {
  const [isIOS, setIsIOS] = useState(false)
  const [isStandalone, setIsStandalone] = useState(false)
 
  useEffect(() => {
    setIsIOS(
      /iPad|iPhone|iPod/.test(navigator.userAgent) && !(window as any).MSStream
    )
 
    setIsStandalone(window.matchMedia('(display-mode: standalone)').matches)
  }, [])
 
  if (isStandalone) {
    return null // Installationsbutton nicht anzeigen, wenn bereits installiert
  }
 
  return (
    <div>
      <h3>App installieren</h3>
      <button>Zum Startbildschirm hinzufügen</button>
      {isIOS && (
        <p>
          Um diese App auf Ihrem iOS-Gerät zu installieren, tippen Sie auf die Teilen-Schaltfläche
          <span role="img" aria-label="Teilen-Symbol">
            {' '}
            ⎋{' '}
          </span>
          und dann auf "Zum Startbildschirm hinzufügen"
          <span role="img" aria-label="Plus-Symbol">
            {' '}
            ➕{' '}
          </span>.
        </p>
      )}
    </div>
  )
}
 
export default function Page() {
  return (
    <div>
      <PushNotificationManager />
      <InstallPrompt />
    </div>
  )
}
function InstallPrompt() {
  const [isIOS, setIsIOS] = useState(false);
  const [isStandalone, setIsStandalone] = useState(false);
 
  useEffect(() => {
    setIsIOS(
      /iPad|iPhone|iPod/.test(navigator.userAgent) && !(window as any).MSStream
    );
 
    setIsStandalone(window.matchMedia('(display-mode: standalone)').matches);
  }, []);
 
  if (isStandalone) {
    return null; // Installationsbutton nicht anzeigen, wenn bereits installiert
  }
 
  return (
    <div>
      <h3>App installieren</h3>
      <button>Zum Startbildschirm hinzufügen</button>
      {isIOS && (
        <p>
          Um diese App auf Ihrem iOS-Gerät zu installieren, tippen Sie auf die Teilen-Schaltfläche
          <span role="img" aria-label="Teilen-Symbol">
            {' '}
            ⎋{' '}
          </span>
          und dann auf "Zum Startbildschirm hinzufügen"
          <span role="img" aria-label="Plus-Symbol">
            {' '}
            ➕{' '}
          </span>
          .
        </p>
      )}
    </div>
  );
}
 
export default function Page() {
  return (
    <div>
      <PushNotificationManager />
      <InstallPrompt />
    </div>
  );
}

Nun erstellen wir die Server-Aktionen, die diese Datei aufruft.

3. Server-Aktionen implementieren

Erstellen Sie eine neue Datei für Ihre Aktionen unter app/actions.ts. Diese Datei wird das Erstellen, Löschen von Abonnements und das Senden von Benachrichtigungen übernehmen.

app/actions.ts
TypeScript
'use server'
 
import webpush from 'web-push'
 
webpush.setVapidDetails(
  '<mailto:[email protected]>',
  process.env.NEXT_PUBLIC_VAPID_PUBLIC_KEY!,
  process.env.VAPID_PRIVATE_KEY!
)
 
let subscription: PushSubscription | null = null
 
export async function subscribeUser(sub: PushSubscription) {
  subscription = sub
  // In einer Produktionsumgebung würden Sie das Abonnement in einer Datenbank speichern
  // Zum Beispiel: await db.subscriptions.create({ data: sub })
  return { success: true }
}
 
export async function unsubscribeUser() {
  subscription = null
  // In einer Produktionsumgebung würden Sie das Abonnement aus der Datenbank entfernen
  // Zum Beispiel: await db.subscriptions.delete({ where: { ... } })
  return { success: true }
}
 
export async function sendNotification(message: string) {
  if (!subscription) {
    throw new Error('Kein Abonnement verfügbar')
  }
 
  try {
    await webpush.sendNotification(
      subscription,
      JSON.stringify({
        title: 'Testbenachrichtigung',
        body: message,
        icon: '/icon.png',
      })
    )
    return { success: true }
  } catch (error) {
    console.error('Fehler beim Senden der Push-Benachrichtigung:', error)
    return { success: false, error: 'Senden der Benachrichtigung fehlgeschlagen' }
  }
}

Das Senden einer Benachrichtigung wird von unserem Service Worker in Schritt 5 behandelt.

In einer Produktionsumgebung würden Sie das Abonnement in einer Datenbank speichern, um es über Serverneustarts hinweg zu erhalten und Abonnements mehrerer Benutzer zu verwalten.

4. VAPID-Schlüssel generieren

Um die Web Push API zu nutzen, müssen Sie VAPID-Schlüssel generieren.

Erstellen Sie eine Skriptdatei, z.B. generate-vapid-keys.js:

./generate-vapid-keys.js
const webpush = require('web-push')
const vapidKeys = webpush.generateVAPIDKeys()
 
console.log('Fügen Sie die folgenden Schlüssel in Ihrer .env-Datei ein:')
console.log('-------------------')
console.log('NEXT_PUBLIC_VAPID_PUBLIC_KEY=', vapidKeys.publicKey)
console.log('VAPID_PRIVATE_KEY=', vapidKeys.privateKey)

Führen Sie dieses Skript mit Node.js aus, um Ihre VAPID-Schlüssel zu generieren:

Terminal
node generate-vapid-keys.js

Kopieren Sie die Ausgabe und fügen Sie sie in Ihre .env-Datei ein.

5. Service Worker erstellen

Erstellen Sie eine public/sw.js-Datei für Ihren Service Worker:

public/sw.js
self.addEventListener('push', function (event) {
  if (event.data) {
    const data = event.data.json()
    const options = {
      body: data.body,
      icon: data.icon || '/icon.png',
      badge: '/badge.png',
      vibrate: [100, 50, 100],
      data: {
        dateOfArrival: Date.now(),
        primaryKey: '2',
      },
    }
    event.waitUntil(self.registration.showNotification(data.title, options))
  }
})
 
self.addEventListener('notificationclick', function (event) {
  console.log('Benachrichtigungsklick empfangen.')
  event.notification.close()
  event.waitUntil(clients.openWindow('<https://ihre-website.com>'))
})

Dieser Service Worker unterstützt benutzerdefinierte Bilder und Benachrichtigungen. Er verarbeitet eingehende Push-Ereignisse und Benachrichtigungsklicks.

  • Sie können benutzerdefinierte Symbole für Benachrichtigungen mit den Eigenschaften icon und badge festlegen.
  • Das Vibrationsmuster kann angepasst werden, um benutzerdefinierte Vibrationssignale auf unterstützten Geräten zu erzeugen.
  • Zusätzliche Daten können mit der data-Eigenschaft an die Benachrichtigung angehängt werden.

Denken Sie daran, Ihren Service Worker gründlich zu testen, um sicherzustellen, dass er auf verschiedenen Geräten und Browsern wie erwartet funktioniert. Stellen Sie außerdem sicher, dass Sie den Link 'https://ihre-website.com' im notificationclick-Ereignislistener durch die entsprechende URL für Ihre Anwendung ersetzen.

6. Zum Startbildschirm hinzufügen

Die in Schritt 2 definierte InstallPrompt-Komponente zeigt eine Nachricht für iOS-Geräte, um sie aufzufordern, die App auf ihrem Startbildschirm zu installieren.

Um sicherzustellen, dass Ihre Anwendung auf einem mobilen Startbildschirm installiert werden kann, benötigen Sie:

  1. Ein gültiges Web-App-Manifest (in Schritt 1 erstellt)
  2. Die Website wird über HTTPS bereitgestellt

Moderne Browser zeigen Benutzern automatisch eine Installationsaufforderung an, wenn diese Kriterien erfüllt sind. Sie können eine benutzerdefinierte Installationsschaltfläche mit beforeinstallprompt bereitstellen. Wir empfehlen dies jedoch nicht, da es nicht browserübergreifend und plattformunabhängig funktioniert (funktioniert nicht auf Safari iOS).

7. Lokales Testen

Um sicherzustellen, dass Sie Benachrichtigungen lokal anzeigen können, stellen Sie Folgendes sicher:

  • Sie führen lokal einen Entwicklungsserver mit HTTPS aus
    • Verwenden Sie next dev --experimental-https zum Testen
  • In Ihrem Browser (Chrome, Safari, Firefox) sind Benachrichtigungen aktiviert
    • Akzeptieren Sie bei lokaler Aufforderung die Berechtigungen zur Nutzung von Benachrichtigungen
    • Stellen Sie sicher, dass Benachrichtigungen nicht global für den gesamten Browser deaktiviert sind
    • Wenn Sie immer noch keine Benachrichtigungen sehen, versuchen Sie, einen anderen Browser zum Debuggen zu verwenden

8. Sicherung Ihrer Anwendung

Sicherheit ist ein entscheidender Aspekt jeder Webanwendung, insbesondere bei PWAs. Next.js ermöglicht die Konfiguration von Sicherheits-Headern über die next.config.js-Datei. Beispiel:

next.config.js
module.exports = {
  async headers() {
    return [
      {
        source: '/(.*)',
        headers: [
          {
            key: 'X-Content-Type-Options',
            value: 'nosniff',
          },
          {
            key: 'X-Frame-Options',
            value: 'DENY',
          },
          {
            key: 'Referrer-Policy',
            value: 'strict-origin-when-cross-origin',
          },
        ],
      },
      {
        source: '/sw.js',
        headers: [
          {
            key: 'Content-Type',
            value: 'application/javascript; charset=utf-8',
          },
          {
            key: 'Cache-Control',
            value: 'no-cache, no-store, must-revalidate',
          },
          {
            key: 'Content-Security-Policy',
            value: "default-src 'self'; script-src 'self'",
          },
        ],
      },
    ]
  },
}

Werfen wir einen Blick auf die einzelnen Optionen:

  1. Globale Header (für alle Routen angewendet):
    1. X-Content-Type-Options: nosniff: Verhindert MIME-Typ-Sniffing und reduziert das Risiko bösartiger Datei-Uploads.
    2. X-Frame-Options: DENY: Schützt vor Clickjacking-Angriffen, indem die Einbettung der Website in Iframes verhindert wird.
    3. Referrer-Policy: strict-origin-when-cross-origin: Steuert, wie viele Referrer-Informationen mit Anfragen mitgesendet werden und dabei Sicherheit und Funktionalität ausbalanciert.
  2. Service Worker-spezifische Header:
    1. Content-Type: application/javascript; charset=utf-8: Stellt sicher, dass der Service Worker korrekt als JavaScript interpretiert wird.
    2. Cache-Control: no-cache, no-store, must-revalidate: Verhindert das Caching des Service Workers und gewährleistet, dass Benutzer immer die neueste Version erhalten.
    3. Content-Security-Policy: default-src 'self'; script-src 'self': Implementiert eine strikte Content Security Policy für den Service Worker, die nur Skripte aus derselben Quelle zulässt.

Erfahren Sie mehr über die Definition von Content Security Policies mit Next.js.

Nächste Schritte

  1. PWA-Funktionen erkunden: PWAs können verschiedene Web-APIs nutzen, um erweiterte Funktionalitäten bereitzustellen. Erwägen Sie die Erkundung von Funktionen wie Hintergrundsynchronisation, periodische Hintergrundsynchronisation oder die File System Access API, um Ihre Anwendung zu verbessern. Für Inspiration und aktuelle Informationen zu PWA-Funktionen können Sie Ressourcen wie What PWA Can Do Today konsultieren.
  2. Statische Exports: Wenn Ihre Anwendung keinen Server erfordert und stattdessen eine statische Dateiexport benötigt, können Sie die Next.js-Konfiguration aktualisieren. Weitere Informationen finden Sie in der Next.js-Dokumentation zu statischen Exports. Sie müssen jedoch von Server-Aktionen zu Aufrufen einer externen API wechseln und Ihre definierten Header zu Ihrem Proxy verschieben.
  3. Offline-Unterstützung: Um Offline-Funktionalität bereitzustellen, ist eine Option Serwist mit Next.js. Ein Beispiel für die Integration von Serwist mit Next.js finden Sie in deren Dokumentation. Hinweis: Dieses Plugin erfordert derzeit eine Webpack-Konfiguration.
  4. Sicherheitsüberlegungen: Stellen Sie sicher, dass Ihr Service Worker angemessen gesichert ist. Dies umfasst die Verwendung von HTTPS, die Validierung der Quelle von Push-Nachrichten und die Implementierung einer ordnungsgemäßen Fehlerbehandlung.
  5. Benutzererfahrung: Erwägen Sie die Implementierung von progressiven Verbesserungstechniken, um sicherzustellen, dass Ihre App auch dann gut funktioniert, wenn bestimmte PWA-Funktionen vom Browser des Benutzers nicht unterstützt werden.