Progressive Web-Apps schrittweise optimieren

Für moderne Browser entwickeln und kontinuierlich verbessern, als wäre es 2003

Im März 2003 verblüfften Nick Finck und Steve Champeon die Webdesign-Welt mit dem Konzept der progressiven Verbesserung, einer Strategie für das Webdesign, bei der zuerst die Hauptinhalte der Webseite geladen werden und dann nach und nach differenziertere und technisch anspruchsvollere Präsentationsebenen und Funktionen hinzugefügt werden. 2003 ging es bei der progressiven Verbesserung darum, die damals modernen CSS-Funktionen, unaufdringliches JavaScript und sogar nur skalierbare Vektorgrafiken zu verwenden. Bei der progressiven Verbesserung im Jahr 2020 und darüber hinaus geht es darum, moderne Browserfunktionen zu nutzen.

Inklusiv-Webdesign für die Zukunft mit progressiver Verbesserung Titelfolie aus der ursprünglichen Präsentation von Finck und Champeon.
Folie: Inklusives Webdesign für die Zukunft mit Progressive Enhancement. (Quelle)

Modernes JavaScript

Apropos JavaScript: Die Browserunterstützung für die neuesten JavaScript-Kernfunktionen von ES 2015 ist hervorragend. Der neue Standard umfasst Promis, Module, Klassen, Vorlagenliterale, Pfeilfunktionen, let und const, Standardparameter, Generatoren, die destruktive Zuweisung, Ruhe und Verteilung, Map/Set, WeakMap/WeakSet und viele mehr. Alle werden unterstützt.

Die CanIUse-Supporttabelle für ES6-Funktionen zeigt die Unterstützung in allen gängigen Browsern.
Die Tabelle zur Browserunterstützung von ECMAScript 2015 (ES6). (Quelle)

Asynchrone Funktionen sind eine Funktion von ES 2017 und eine meiner persönlichen Favoriten. Sie können in allen gängigen Browsern verwendet werden. Mit den Keywords async und await kann asynchrones, versprechensbasiertes Verhalten übersichtlicher geschrieben werden, sodass Promise-Ketten nicht explizit konfiguriert werden müssen.

Die CanIUse-Unterstützungstabelle für asynchrone Funktionen, die die Unterstützung in allen gängigen Browsern zeigt
Tabelle zur Browserunterstützung für asynchrone Funktionen. (Quelle)

Und selbst die jüngsten Spracherweiterungen in ES 2020 wie optionale Verknüpfung und Nullzusammenführung wurden sehr schnell unterstützt. Unten sehen Sie ein Codebeispiel. Was die wichtigsten JavaScript-Funktionen angeht, ist das Gras heute so grün wie nie zuvor.

const adventurer = {
  name: 'Alice',
  cat: {
    name: 'Dinah',
  },
};
console.log(adventurer.dog?.name);
// Expected output: undefined
console.log(0 ?? 42);
// Expected output: 0
Das ikonische grüne Gras-Hintergrundbild von Windows XP.
Bei den wichtigsten JavaScript-Funktionen ist das Gras grüner. (Bildschirmfoto eines Microsoft-Produkts, mit Genehmigung verwendet.)

Die Beispiel-App: Fugu Greetings

In diesem Artikel verwende ich eine einfache PWA namens Fugu Greetings (GitHub). Der Name dieser App ist eine wichtige Voraussetzung für Project Fugu 🐡, das dem Web alle Funktionen von Android-, iOS- und Desktop-Apps zur Verfügung stellen soll. Weitere Informationen zum Projekt finden Sie auf der Landingpage.

Fugu Greetings ist eine Zeichen-App, mit der Sie virtuelle Grußkarten erstellen und an Ihre Lieben senden können. Sie veranschaulicht die Kernkonzepte von PWAs. Sie ist zuverlässig und vollständig offlinefähig. Sie können sie also auch ohne Netzwerk nutzen. Sie kann auch auf dem Startbildschirm eines Geräts installiert werden und lässt sich als eigenständige Anwendung nahtlos in das Betriebssystem einbinden.

Fugu Greetings-PWA mit einer Zeichnung, die dem Logo der PWA-Community ähnelt
Die Beispiel-App Fugu Greetings.

Progressive Enhancement

Jetzt ist es an der Zeit, über progressive Verbesserung zu sprechen. Im Glossar der MDN-Webdokumente wird das Konzept so definiert:

Progressive Verbesserung ist eine Designphilosophie, die so vielen Nutzern wie möglich grundlegende Inhalte und Funktionen bietet und gleichzeitig nur Nutzern der modernsten Browser, die den gesamten erforderlichen Code ausführen können, die bestmögliche Nutzererfahrung bietet.

Die Funktionserkennung wird in der Regel verwendet, um festzustellen, ob Browser modernere Funktionen verarbeiten können, während Polyfills häufig verwendet werden, um fehlende Funktionen mit JavaScript hinzuzufügen.

[…]

Progressive Verbesserung ist eine nützliche Methode, mit der sich Webentwickler darauf konzentrieren können, die bestmöglichen Websites zu entwickeln und gleichzeitig dafür zu sorgen, dass diese Websites auf mehreren unbekannten User-Agents funktionieren. Graceful Degradation ist ähnlich, aber nicht dasselbe. Es wird oft als gegenteilige Strategie zur progressiven Verbesserung angesehen. In Wirklichkeit sind beide Ansätze gültig und können sich oft gegenseitig ergänzen.

MDN-Beitragende

Jede Grußkarte von Grund auf neu zu erstellen, kann sehr mühsam sein. Wie wäre es also mit einer Funktion, mit der Nutzer Bilder importieren und direkt loslegen können? Bei einem traditionellen Ansatz hätten Sie dazu ein <input type=file>-Element verwendet. Zuerst erstellen Sie das Element, legen seine type auf 'file' fest und fügen der accept-Eigenschaft MIME-Typen hinzu. Anschließend klicken Sie programmatisch darauf und warten auf Änderungen. Wenn Sie ein Bild auswählen, wird es direkt in den Canvas importiert.

const importImage = async () => {
  return new Promise((resolve) => {
    const input = document.createElement('input');
    input.type = 'file';
    input.accept = 'image/*';
    input.addEventListener('change', () => {
      resolve(input.files[0]);
    });
    input.click();
  });
};

Wenn es eine Importfunktion gibt, sollte es wahrscheinlich auch eine Exportfunktion geben, damit Nutzer ihre Grußkarten lokal speichern können. Die traditionelle Methode zum Speichern von Dateien besteht darin, einen Ankerlink mit dem Attribut download und einer Blob-URL als href zu erstellen. Sie können auch programmatisch darauf klicken, um den Download auszulösen. Um Speicherlecks zu vermeiden, sollten Sie unbedingt die Blob-Objekt-URL widerrufen.

const exportImage = async (blob) => {
  const a = document.createElement('a');
  a.download = 'fugu-greeting.png';
  a.href = URL.createObjectURL(blob);
  a.addEventListener('click', (e) => {
    setTimeout(() => URL.revokeObjectURL(a.href), 30 * 1000);
  });
  a.click();
};

Aber Moment. Sie haben die Grußkarte nicht „heruntergeladen“, sondern „gespeichert“. Anstatt ein Dialogfeld zum Speichern anzuzeigen, in dem Sie auswählen können, wo die Datei gespeichert werden soll, hat der Browser die Grußkarte direkt ohne Nutzerinteraktion heruntergeladen und direkt in Ihren Downloadordner verschoben. Das ist nicht gut.

Was wäre, wenn es eine bessere Lösung gäbe? Was wäre, wenn Sie eine lokale Datei einfach öffnen, bearbeiten und die Änderungen entweder in einer neuen Datei oder in der ursprünglichen Datei speichern könnten, die Sie ursprünglich geöffnet hatten? Mit der File System Access API können Sie Dateien und Verzeichnisse öffnen und erstellen sowie ändern und speichern.

Wie kann ich also die Funktionen einer API erkennen? Die File System Access API stellt die neue Methode window.chooseFileSystemEntries() zur Verfügung. Daher muss ich abhängig davon, ob diese Methode verfügbar ist, bedingt verschiedene Import- und Exportmodule laden. Unten habe ich gezeigt, wie das geht.

const loadImportAndExport = () => {
  if ('chooseFileSystemEntries' in window) {
    Promise.all([
      import('./import_image.mjs'),
      import('./export_image.mjs'),
    ]);
  } else {
    Promise.all([
      import('./import_image_legacy.mjs'),
      import('./export_image_legacy.mjs'),
    ]);
  }
};

Bevor ich jedoch auf die Details der File System Access API eingehe, möchte ich kurz das Muster der progressiven Verbesserung hervorheben. In Browsern, die die File System Access API derzeit nicht unterstützen, lade ich die alten Scripts. Unten sehen Sie die Netzwerktabs von Firefox und Safari.

Safari Web Inspector mit den geladenen älteren Dateien
Netzwerktab des Safari Web Inspectors.
Firefox-Entwicklertools, in denen die geladenen Legacy-Dateien zu sehen sind
Netzwerktab der Firefox-Entwicklertools.

In Chrome, einem Browser, der die API unterstützt, werden jedoch nur die neuen Scripts geladen. Das ist dank dynamischer import() möglich, die von allen modernen Browsern unterstützt wird. Wie ich bereits sagte, ist das Gras derzeit ziemlich grün.

Chrome-Entwicklertools, in denen die modernen Dateien geladen werden
Chrome-Entwicklertools: Tab „Netzwerk“

File System Access API

Nachdem ich das geklärt habe, sehen wir uns die tatsächliche Implementierung an, die auf der File System Access API basiert. Zum Importieren eines Bildes rufe ich window.chooseFileSystemEntries() auf und übergebe ihm das Attribut accepts, in dem ich angeben, dass ich Bilddateien möchte. Sowohl Dateiendungen als auch MIME-Typen werden unterstützt. Dies führt zu einem Dateihandle, über den ich die tatsächliche Datei durch Aufrufen von getFile() abrufen kann.

const importImage = async () => {
  try {
    const handle = await window.chooseFileSystemEntries({
      accepts: [
        {
          description: 'Image files',
          mimeTypes: ['image/*'],
          extensions: ['jpg', 'jpeg', 'png', 'webp', 'svg'],
        },
      ],
    });
    return handle.getFile();
  } catch (err) {
    console.error(err.name, err.message);
  }
};

Der Export eines Bildes ist fast identisch, aber dieses Mal muss ich den Typparameter 'save-file' an die Methode chooseFileSystemEntries() übergeben. Daraufhin wird ein Dialogfeld zum Speichern der Datei angezeigt. Wenn die Datei geöffnet ist, ist das nicht erforderlich, da 'open-file' der Standard ist. Ich habe den accepts-Parameter ähnlich wie zuvor festgelegt, aber diesmal nur auf PNG-Bilder. Wieder wird ein Dateihandle zurückgegeben, aber anstatt die Datei abzurufen, erstelle ich diesmal einen beschreibbaren Stream, indem ich createWritable() aufrufe. Als Nächstes schreibe ich den Blob, also das Bild meiner Grußkarte, in die Datei. Schließlich schließe ich den beschreibbaren Stream.

Es kann immer etwas schiefgehen: Auf dem Laufwerk ist möglicherweise kein Speicherplatz mehr verfügbar, es kann ein Schreib- oder Lesefehler auftreten oder der Nutzer schließt das Dateidialogfeld einfach nur wieder. Aus diesem Grund umschließen ich die Aufrufe immer in einer try...catch-Anweisung.

const exportImage = async (blob) => {
  try {
    const handle = await window.chooseFileSystemEntries({
      type: 'save-file',
      accepts: [
        {
          description: 'Image file',
          extensions: ['png'],
          mimeTypes: ['image/png'],
        },
      ],
    });
    const writable = await handle.createWritable();
    await writable.write(blob);
    await writable.close();
  } catch (err) {
    console.error(err.name, err.message);
  }
};

Mithilfe der File System Access API kann ich eine Datei wie gewohnt öffnen. Die importierte Datei wird direkt auf dem Canvas gezeichnet. Ich kann meine Änderungen vornehmen und sie dann über ein echtes Speicherdialogfeld speichern, in dem ich den Namen und Speicherort der Datei auswählen kann. Jetzt ist die Datei für die Ewigkeit gesichert.

Fugu Greetings App mit einem Dialogfeld zum Öffnen einer Datei
Das Dialogfeld zum Öffnen der Datei.
Die Fugu Greetings App kann jetzt mit einem importierten Bild verwendet werden.
Das importierte Bild.
Fugu Greetings App mit dem geänderten Bild
Das geänderte Bild in einer neuen Datei speichern.

Web Share API und Web Share Target API

Außer für die Ewigkeit aufzubewahren, möchte ich meine Grußkarte vielleicht auch teilen. Das ist mit der Web Share API und der Web Share Target API möglich. Mobil- und seit Kurzem auch Desktop-Betriebssysteme haben integrierte Freigabemechanismen. Unten sehen Sie beispielsweise das Freigabe-Sheet von Safari auf dem Mac, das über einen Artikel in meinem Blog aufgerufen wurde. Wenn Sie auf die Schaltfläche Artikel teilen klicken, können Sie einen Link zum Artikel mit einem Freund teilen, z. B. über die macOS-Nachrichten-App.

Das Share Sheet von Safari unter macOS wird über die Schaltfläche „Teilen“ eines Artikels ausgelöst
Web Share API in Safari auf dem Computer unter macOS.

Der Code dafür ist ziemlich einfach. Ich rufe navigator.share() auf und übergeben ihm ein optionales title, text und url in einem Objekt. Aber was ist, wenn ich ein Bild anhängen möchte? Level 1 der Web Share API unterstützt dies noch nicht. Die gute Nachricht ist, dass Web Share Level 2 Funktionen zur Dateifreigabe hinzugefügt hat.

try {
  await navigator.share({
    title: 'Check out this article:',
    text: `"${document.title}" by @tomayac:`,
    url: document.querySelector('link[rel=canonical]').href,
  });
} catch (err) {
  console.warn(err.name, err.message);
}

Ich zeige Ihnen, wie das mit der Fugu-Grußkartenanwendung funktioniert. Zuerst muss ich ein data-Objekt mit einem files-Array aus einem Blob und dann eine title und eine text vorbereiten. Als Nächstes verwende ich als Best Practice die neue navigator.canShare()-Methode, die genau das tut, was ihr Name vermuten lässt: Sie gibt mir an, ob das data-Objekt, das ich freigeben möchte, technisch vom Browser freigegeben werden kann. Wenn navigator.canShare() mir mitteilt, dass die Daten freigegeben werden können, kann ich wie zuvor navigator.share() aufrufen. Da alles schiefgehen kann, verwende ich wieder einen try...catch-Block.

const share = async (title, text, blob) => {
  const data = {
    files: [
      new File([blob], 'fugu-greeting.png', {
        type: blob.type,
      }),
    ],
    title: title,
    text: text,
  };
  try {
    if (!(navigator.canShare(data))) {
      throw new Error("Can't share data.", data);
    }
    await navigator.share(data);
  } catch (err) {
    console.error(err.name, err.message);
  }
};

Wie zuvor verwende ich die progressive Verbesserung. Wenn sowohl 'share' als auch 'canShare' im navigator-Objekt vorhanden sind, lade ich share.mjs über dynamische import() nur dann. In Browsern wie Safari für Mobilgeräte, die nur eine der beiden Bedingungen erfüllen, wird die Funktion nicht geladen.

const loadShare = () => {
  if ('share' in navigator && 'canShare' in navigator) {
    import('./share.mjs');
  }
};

Wenn ich in Fugu Greetings in einem unterstützten Browser wie Chrome auf Android auf die Schaltfläche Teilen tippe, wird das integrierte Freigabe-Sheet geöffnet. Ich kann beispielsweise Gmail auswählen und das Widget für den E-Mail-Editor wird mit dem angehängten Bild angezeigt.

Freigabebereich auf Betriebssystemebene mit verschiedenen Apps, über die das Bild geteilt werden kann
App auswählen, für die die Datei freigegeben werden soll.
Das Gmail-Widget zum Verfassen von E-Mails mit angehängtem Bild.
Die Datei wird im Gmail-Editor an eine neue E-Mail angehängt.

Contact Picker API

Als Nächstes möchte ich über Kontakte sprechen, also das Adressbuch oder die Kontaktverwaltungs-App eines Geräts. Wenn Sie eine Grußkarte schreiben, ist es manchmal nicht ganz einfach, den Namen einer Person richtig zu schreiben. Ich habe zum Beispiel einen Freund Sergey, der seinen Namen lieber in kyrillischen Buchstaben schreiben möchte. Ich verwende eine deutsche QWERTZ-Tastatur und habe keine Ahnung, wie ich den Namen eintippen soll. Dieses Problem kann mit der Contact Picker API gelöst werden. Da ich meinen Freund in der Kontakte App meines Smartphones gespeichert habe, kann ich über die Contacts Picker API auf meine Kontakte im Web zugreifen.

Zuerst muss ich die Liste der Unterkünfte angeben, auf die ich zugreifen möchte. In diesem Fall benötige ich nur die Namen, aber für andere Anwendungsfälle könnte ich an Telefonnummern, E-Mail-Adressen, Avatarsymbolen oder Postanschriften interessiert sein. Als Nächstes konfiguriere ich ein options-Objekt und setze multiple auf true, damit ich mehrere Einträge auswählen kann. Schließlich kann ich navigator.contacts.select() aufrufen, wodurch die gewünschten Eigenschaften für die vom Nutzer ausgewählten Kontakte zurückgegeben werden.

const getContacts = async () => {
  const properties = ['name'];
  const options = { multiple: true };
  try {
    return await navigator.contacts.select(properties, options);
  } catch (err) {
    console.error(err.name, err.message);
  }
};

Und Sie haben wahrscheinlich schon das Muster erkannt: Ich lade die Datei nur dann, wenn die API tatsächlich unterstützt wird.

if ('contacts' in navigator) {
  import('./contacts.mjs');
}

Wenn ich in Fugu Greeting auf die Schaltfläche Kontakte tippe und meine beiden besten Freunde, Сергей Михайлович Брин und 劳伦斯·爱德华·"拉里"·佩奇, auswähle, sehen Sie, dass in der Kontaktauswahl nur ihre Namen, aber nicht ihre E-Mail-Adressen oder andere Informationen wie ihre Telefonnummern angezeigt werden. Ihre Namen werden dann auf meine Grußkarte gezeichnet.

Kontaktauswahl mit den Namen von zwei Kontakten im Adressbuch
Zwei Namen mit der Kontaktauswahl aus dem Adressbuch auswählen.
Die Namen der beiden zuvor ausgewählten Kontakte wurden auf die Grußkarte gezeichnet.
Die beiden Namen werden dann auf die Grußkarte gezeichnet.

Die Asynchronous Clipboard API

Als Nächstes geht es um das Kopieren und Einfügen. Eine unserer Lieblingsaktionen als Softwareentwickler ist das Kopieren und Einfügen. Als Verfasser von Grußkarten möchte ich das manchmal auch tun. Ich möchte beispielsweise ein Bild in eine Grußkarte einfügen, an der ich gerade arbeite, oder die Grußkarte kopieren, damit ich sie an anderer Stelle weiter bearbeiten kann. Die Async Clipboard API unterstützt sowohl Text als auch Bilder. Ich erkläre Ihnen, wie ich der Fugu Greetings App die Unterstützung für Kopieren und Einfügen hinzugefügt habe.

Wenn ich etwas in die Zwischenablage des Systems kopieren möchte, muss ich darauf schreiben. Die Methode navigator.clipboard.write() verwendet ein Array von Zwischenablageelementen als Parameter. Jedes Element in der Zwischenablage ist im Wesentlichen ein Objekt mit einem Blob als Wert und dem Blob-Typ als Schlüssel.

const copy = async (blob) => {
  try {
    await navigator.clipboard.write([
      new ClipboardItem({
        [blob.type]: blob,
      }),
    ]);
  } catch (err) {
    console.error(err.name, err.message);
  }
};

Zum Einfügen muss ich die Elemente aus der Zwischenablage durchgehen, die ich durch Aufrufen von navigator.clipboard.read() erhalte. Das liegt daran, dass sich möglicherweise mehrere Zwischenablageelemente in unterschiedlichen Darstellungen in der Zwischenablage befinden. Jedes Element in der Zwischenablage hat ein Feld types, in dem die MIME-Typen der verfügbaren Ressourcen angegeben sind. Ich rufe die getType()-Methode des Zwischenablage-Elements auf und übergebe den zuvor abgerufenen MIME-Typ.

const paste = async () => {
  try {
    const clipboardItems = await navigator.clipboard.read();
    for (const clipboardItem of clipboardItems) {
      try {
        for (const type of clipboardItem.types) {
          const blob = await clipboardItem.getType(type);
          return blob;
        }
      } catch (err) {
        console.error(err.name, err.message);
      }
    }
  } catch (err) {
    console.error(err.name, err.message);
  }
};

Das ist mittlerweile fast selbstverständlich. Ich mache das nur in unterstützten Browsern.

if ('clipboard' in navigator && 'write' in navigator.clipboard) {
  import('./clipboard.mjs');
}

Wie funktioniert das in der Praxis? Ich habe ein Bild in der Vorschau-App von macOS geöffnet und kopiere es in die Zwischenablage. Wenn ich auf Einfügen klicke, werde ich von der Fugu Greetings App gefragt, ob ich der App erlauben möchte, Text und Bilder in der Zwischenablage zu sehen.

Fugu Begrüßungs-App mit einer Aufforderung zur Zwischenablage-Berechtigung.
Die Aufforderung zur Berechtigung für die Zwischenablage.

Nachdem Sie die Berechtigung akzeptiert haben, wird das Bild in die Anwendung eingefügt. Andersherum funktioniert es auch. Ich kopiere eine Grußkarte in die Zwischenablage. Wenn ich dann die Vorschau öffne und auf Datei und dann auf Neu aus Zwischenablage klicke, wird die Grußkarte in ein neues unbenanntes Bild eingefügt.

Die macOS-Vorschau-App mit einem unbenannten, gerade eingefügten Bild.
Ein Bild, das in die macOS-Vorschau-App eingefügt wurde.

Die Badging API

Eine weitere nützliche API ist die Badging API. Als installierbare PWA hat Fugu Begrüßung natürlich ein App-Symbol, das Nutzer im App-Dock oder auf dem Startbildschirm platzieren können. Eine unterhaltsame und einfache Möglichkeit, die API zu demonstrieren, besteht darin, sie in Fugu Greetings als Zähler für Stiftstriche zu verwenden. Ich habe einen Event-Listener hinzugefügt, der den Zähler für die Stiftstriche erhöht, wenn das Ereignis pointerdown auftritt, und dann das aktualisierte Symbol-Badge festlegt. Wenn der Canvas gelöscht wird, wird der Zähler zurückgesetzt und das Symbol entfernt.

let strokes = 0;

canvas.addEventListener('pointerdown', () => {
  navigator.setAppBadge(++strokes);
});

clearButton.addEventListener('click', () => {
  strokes = 0;
  navigator.setAppBadge(strokes);
});

Diese Funktion ist eine progressive Verbesserung. Die Ladelogik funktioniert also wie gewohnt.

if ('setAppBadge' in navigator) {
  import('./badge.mjs');
}

In diesem Beispiel habe ich die Zahlen 1 bis 7 mit jeweils einem Strich gezeichnet. Der Badge-Zähler auf dem Symbol steht jetzt bei sieben.

Die Zahlen 1 bis 7, die auf die Grußkarte gezeichnet wurden, jeweils mit nur einem Stiftstrich.
Zeichne die Zahlen von 1 bis 7 mit sieben Stiftstrichen.
Symbol in der Fugu Greetings App mit der Zahl 7
Der Zähler für die Striche in Form des App-Symbols.

Die Periodic Background Sync API

Möchtest du jeden Tag mit etwas Neuem beginnen? Eine praktische Funktion der Fugu Greetings App ist, dass Sie jeden Morgen mit einem neuen Hintergrundbild für Ihre Grußkarte inspiriert werden können. Dazu verwendet die App die Periodic Background Sync API.

Im ersten Schritt müssen Sie ein regelmäßiges Synchronisierungsereignis in der Service Worker-Registrierung registrieren. Es wartet auf ein Synchronisierungs-Tag namens 'image-of-the-day' und hat ein Mindestintervall von einem Tag, damit der Nutzer alle 24 Stunden ein neues Hintergrundbild erhält.

const registerPeriodicBackgroundSync = async () => {
  const registration = await navigator.serviceWorker.ready;
  try {
    registration.periodicSync.register('image-of-the-day-sync', {
      // An interval of one day.
      minInterval: 24 * 60 * 60 * 1000,
    });
  } catch (err) {
    console.error(err.name, err.message);
  }
};

Der zweite Schritt besteht darin, im Service Worker auf das Ereignis periodicsync zu warten. Wenn das Ereignis-Tag 'image-of-the-day' ist, also das Ereignis-Tag, das zuvor registriert wurde, wird das Bild des Tages über die Funktion getImageOfTheDay() abgerufen und das Ergebnis an alle Clients weitergegeben, damit diese ihre Canvases und Caches aktualisieren können.

self.addEventListener('periodicsync', (syncEvent) => {
  if (syncEvent.tag === 'image-of-the-day-sync') {
    syncEvent.waitUntil(
      (async () => {
        const blob = await getImageOfTheDay();
        const clients = await self.clients.matchAll();
        clients.forEach((client) => {
          client.postMessage({
            image: blob,
          });
        });
      })()
    );
  }
});

Auch dies ist eine echte progressive Verbesserung. Der Code wird also nur geladen, wenn die API vom Browser unterstützt wird. Das gilt sowohl für den Clientcode als auch für den Service Worker-Code. In nicht unterstützten Browsern wird keines der beiden geladen. Im Service Worker verwende ich anstelle eines dynamischen import() (das in einem Service Worker-Kontext noch nicht unterstützt wird) das klassische importScripts().

// In the client:
const registration = await navigator.serviceWorker.ready;
if (registration && 'periodicSync' in registration) {
  import('./periodic_background_sync.mjs');
}
// In the service worker:
if ('periodicSync' in self.registration) {
  importScripts('./image_of_the_day.mjs');
}

In Fugu Greetings wird durch Drücken der Schaltfläche Hintergrund das Grußkartenbild des Tages angezeigt, das jeden Tag über die Periodic Background Sync API aktualisiert wird.

Die Fugu Greetings App mit einem neuen Grußkartenbild des Tages.
Durch Tippen auf die Schaltfläche Hintergrund wird das Bild des Tages angezeigt.

Notification Triggers API

Manchmal braucht man auch bei viel Inspiration einen kleinen Anstoß, um eine angefangene Grußkarte fertigzustellen. Diese Funktion wird durch die Notification Triggers API aktiviert. Als Nutzer kann ich eine Uhrzeit eingeben, zu der ich aufgefordert werden soll, meine Grußkarte fertigzustellen. Dann erhalte ich eine Benachrichtigung, dass meine Grußkarte bereit ist.

Nachdem Sie nach der gewünschten Uhrzeit gefragt wurden, plant die Anwendung die Benachrichtigung mit einer showTrigger. Das kann ein TimestampTrigger mit dem zuvor ausgewählten Zieldatum sein. Die Erinnerungsbenachrichtigung wird lokal ausgelöst und erfordert weder ein Netzwerk noch eine Serverseite.

const targetDate = promptTargetDate();
if (targetDate) {
  const registration = await navigator.serviceWorker.ready;
  registration.showNotification('Reminder', {
    tag: 'reminder',
    body: "It's time to finish your greeting card!",
    showTrigger: new TimestampTrigger(targetDate),
  });
}

Wie bei allem, was ich bisher gezeigt habe, handelt es sich hierbei um eine progressive Verbesserung. Der Code wird also nur bedingt geladen.

if ('Notification' in window && 'showTrigger' in Notification.prototype) {
  import('./notification_triggers.mjs');
}

Wenn ich in Fugu Greetings das Kästchen Erinnerung anklicke, werde ich gefragt, wann ich daran erinnert werden möchte, meine Grußkarte fertigzustellen.

Die Fugu Greetings App mit einer Aufforderung, in der der Nutzer gefragt wird, wann er daran erinnert werden möchte, seine Grußkarte fertigzustellen.
Eine lokale Benachrichtigung planen, um daran erinnert zu werden, eine Grußkarte fertigzustellen.

Wenn eine geplante Benachrichtigung in Fugu Greetings ausgelöst wird, wird sie wie jede andere Benachrichtigung angezeigt. Wie bereits erwähnt, ist dafür keine Netzwerkverbindung erforderlich.

macOS-Mitteilungszentrale mit einer ausgelösten Benachrichtigung von Fugu Greetings
Die ausgelöste Benachrichtigung wird in der macOS-Mitteilungszentrale angezeigt.

Die Wake Lock API

Ich möchte auch die Wake Lock API einbinden. Manchmal müssen Sie nur lange genug auf den Bildschirm starren, bis Sie die Inspiration küsst. Das Schlimmste, was dann passieren kann, ist, dass sich der Bildschirm abschaltet. Die Wake Lock API kann dies verhindern.

Im ersten Schritt müssen Sie mit der navigator.wakelock.request method() eine Wake-Lock erhalten. Ich übergebe den String 'screen', um eine Sperre für das Aufwecken des Displays zu erhalten. Dann füge ich einen Event-Listener hinzu, der informiert wird, wenn der Wakelock freigegeben wird. Das kann beispielsweise passieren, wenn sich die Sichtbarkeit des Tabs ändert. In diesem Fall kann ich, wenn der Tab wieder sichtbar wird, die Wake-Lock wiedererlangen.

let wakeLock = null;
const requestWakeLock = async () => {
  wakeLock = await navigator.wakeLock.request('screen');
  wakeLock.addEventListener('release', () => {
    console.log('Wake Lock was released');
  });
  console.log('Wake Lock is active');
};

const handleVisibilityChange = () => {
  if (wakeLock !== null && document.visibilityState === 'visible') {
    requestWakeLock();
  }
};

document.addEventListener('visibilitychange', handleVisibilityChange);
document.addEventListener('fullscreenchange', handleVisibilityChange);

Ja, das ist eine progressive Verbesserung. Ich muss sie also nur laden, wenn der Browser die API unterstützt.

if ('wakeLock' in navigator && 'request' in navigator.wakeLock) {
  import('./wake_lock.mjs');
}

In Fugu Greetings gibt es ein Kästchen Insomnia (Schlaflosigkeit). Wenn dieses Kästchen angeklickt ist, bleibt der Bildschirm aktiv.

Wenn das Kästchen für Schlaflosigkeit angeklickt ist, bleibt das Display eingeschaltet.
Wenn Sie das Kästchen Insomnia aktivieren, bleibt die App aktiv.

Die Idle Detection API

Manchmal ist es einfach nutzlos, auch wenn Sie stundenlang auf den Bildschirm starren, und Sie können sich nicht im Geringsten vorstellen, was Sie mit Ihrer Grußkarte machen sollen. Mit der Idle Detection API kann die App die Inaktivität von Nutzern erkennen. Wenn der Nutzer zu lange inaktiv ist, wird die App auf den ursprünglichen Zustand zurückgesetzt und der Canvas gelöscht. Diese API ist derzeit an die Berechtigung „Benachrichtigungen“ gebunden, da viele Produktionsanwendungsfälle der Inaktivitätserkennung mit Benachrichtigungen zusammenhängen, z. B. um nur eine Benachrichtigung an ein Gerät zu senden, das der Nutzer gerade aktiv nutzt.

Nachdem ich sichergestellt habe, dass die Benachrichtigungsberechtigung erteilt wurde, erstelle ich den Inaktivitätsdetektor. Ich registriere einen Event-Listener, der Änderungen bei Inaktivität beobachtet, einschließlich Nutzer- und Bildschirmstatus. Der Nutzer kann aktiv oder inaktiv sein und der Bildschirm kann entsperrt oder gesperrt werden. Wenn der Nutzer inaktiv ist, wird der Canvas gelöscht. Ich lege für den Inaktivitätsmelder einen Grenzwert von 60 Sekunden fest.

const idleDetector = new IdleDetector();
idleDetector.addEventListener('change', () => {
  const userState = idleDetector.userState;
  const screenState = idleDetector.screenState;
  console.log(`Idle change: ${userState}, ${screenState}.`);
  if (userState === 'idle') {
    clearCanvas();
  }
});

await idleDetector.start({
  threshold: 60000,
  signal,
});

Wie immer lade ich diesen Code nur, wenn der Browser ihn unterstützt.

if ('IdleDetector' in window) {
  import('./idle_detection.mjs');
}

In der Fugu Greetings App wird der Canvas gelöscht, wenn das Kästchen Ephemerid angeklickt ist und der Nutzer zu lange inaktiv ist.

Die App „Fugu Greetings“ mit einem leeren Canvas, nachdem der Nutzer zu lange inaktiv war.
Wenn das Kästchen Ephemeral (Ephemer) angeklickt ist und der Nutzer zu lange inaktiv war, wird der Canvas gelöscht.

Abschluss

Puh, was für ein Taxi. So viele APIs in nur einer Beispiel-App. Und denken Sie daran, dass ich nie dafür sage, dass Nutzer die Downloadkosten für eine Funktion zahlen müssen, die ihr Browser nicht unterstützt. Mithilfe der progressiven Verbesserung kann ich dafür sorgen, dass nur der relevante Code geladen wird. Da Anfragen mit HTTP/2 kostengünstig sind, sollte dieses Muster für viele Anwendungen gut funktionieren. Für sehr große Apps sollten Sie jedoch einen Bundler in Betracht ziehen.

Im Chrome DevTools-Netzwerkbereich werden nur Anfragen für Dateien mit Code angezeigt, die vom aktuellen Browser unterstützt werden.
Tab „Network“ in den Chrome-Entwicklertools, auf dem nur Anfragen für Dateien mit Code angezeigt werden, der vom aktuellen Browser unterstützt wird.

Die App kann in jedem Browser etwas anders aussehen, da nicht alle Plattformen alle Funktionen unterstützen. Die Hauptfunktionen sind jedoch immer verfügbar und werden entsprechend den Funktionen des jeweiligen Browsers kontinuierlich erweitert. Diese Funktionen können sich auch in ein und demselben Browser ändern, je nachdem, ob die App als installierte Anwendung oder in einem Browsertab ausgeführt wird.

Auf Android-Geräten ausgeführte Fugu-Grüße werden in Chrome mit vielen verfügbaren Funktionen angezeigt.
Fugu Greetings in Android Chrome.
Fugu-Grüße werden in der Desktopversion von Safari ausgeführt und es werden weniger Funktionen angezeigt.
Fugu Greetings in Safari auf dem Computer.
Fugu Greetings in Chrome auf dem Computer, mit vielen verfügbaren Funktionen
Fugu Greetings in der Desktopversion von Chrome.

Wenn Sie an der Fugu Greetings-App interessiert sind, forken Sie sie auf GitHub.

Fugu Greetings-Repository auf GitHub
Fugu Greetings-App auf GitHub.

Das Chromium-Team arbeitet intensiv daran, das Gras für moderne Fugu-APIs umweltfreundlicher zu gestalten. Durch die Anwendung von progressiver Verbesserung bei der Entwicklung meiner App kann ich dafür sorgen, dass alle Nutzer eine gute, solide Basiserfahrung haben, aber Nutzer, die Browser verwenden, die mehr Webplattform-APIs unterstützen, eine noch bessere Erfahrung haben. Ich bin gespannt, wie Sie die progressive Verbesserung in Ihren Apps einsetzen.

Danksagungen

Ich bin Christian Liebel und Hemanth HM dankbar, die beide an Fugu-Grüßen mitgewirkt haben. Dieser Artikel wurde von Joe Medley und Kayce Basques geprüft. Jake Archibald hat mir geholfen, die Situation mit dynamischen import() in einem Service Worker-Kontext zu klären.