Progressive Web-Apps schrittweise optimieren

Entwicklung für moderne Browser und schrittweise Verbesserungen wie im Jahr 2003

Im März 2003 haben Nick Finck und Steve Champeon verblüfft die Welt des Webdesigns mit dem Konzept der Progressive Verbesserung eine Strategie für das Webdesign, bei der der Schwerpunkt darauf liegt, dass grundlegende Webseiteninhalte zuerst geladen werden. Dann werden nach und nach differenziertere und technisch anspruchsvollen Präsentations- und Funktionsebenen. Im Jahr 2003 ging es bei Progressive Enhancement um die Nutzung der damaligen Zeit CSS-Funktionen, unaufdringliches JavaScript und sogar skalierbare Vektorgrafiken Bei Progressive Enhancement im Jahr 2020 und darüber hinaus geht es darum, modernen Browserfunktionen.

<ph type="x-smartling-placeholder">
</ph> Inklusives Webdesign für die Zukunft mit Progressive Enhancement. Titelfolie aus der ursprünglichen Präsentation von Finck und Champeon. <ph type="x-smartling-placeholder">
</ph> Folie: Inklusives Webdesign für die Zukunft mit Progressive Enhancement. (Quelle)

Modernes JavaScript

Apropos JavaScript, die Browserunterstützung für die neueste Hauptversion von ES 2015 Funktionen ist großartig. Der neue Standard umfasst Promise, Module, Klassen, Vorlagenliterale, Pfeilfunktionen, let und const. Standardparameter, Generatoren, die destruktive Zuweisung, Ruhe und Verteilung, Map/Set, WeakMap/WeakSet und viele weitere. Alle werden unterstützt.

<ph type="x-smartling-placeholder">
</ph> Die CanIUse-Supporttabelle für ES6-Funktionen zeigt die Unterstützung in allen gängigen Browsern. <ph type="x-smartling-placeholder">
</ph> Die Tabelle zur Browserunterstützung von ECMAScript 2015 (ES6) (Quelle)

Async-Funktionen, eine Funktion von ES 2017 und einer meiner persönlichen Favoriten, können verwendet werden, in allen gängigen Browsern. Die Keywords async und await ermöglichen ein asynchrones, Promise-basiertes Verhalten in einem saubereren Stil geschrieben werden, sodass keine Promise-Ketten explizit konfiguriert werden müssen.

<ph type="x-smartling-placeholder">
</ph> Die CanIUse-Supporttabelle für asynchrone Funktionen zeigt die Unterstützung in allen gängigen Browsern. <ph type="x-smartling-placeholder">
</ph> Tabelle mit Browserunterstützung für asynchrone Funktionen (Quelle)

2020 wurden neue Sprachen auf Spanisch hinzugefügt, zum Beispiel: optionale Verkettung und Nullish-Koalescing Support sehr schnell erreicht haben. Unten sehen Sie ein Codebeispiel. Was JavaScript-Kernfunktionen angeht, so könnte das Gras nicht viel umweltfreundlicher sein. ist heute.

const adventurer = {
  name: 'Alice',
  cat: {
    name: 'Dinah',
  },
};
console.log(adventurer.dog?.name);
// Expected output: undefined
console.log(0 ?? 42);
// Expected output: 0
<ph type="x-smartling-placeholder">
</ph> Das ikonische Windows XP-Hintergrundbild mit grünem Gras.
. Für die wichtigsten JavaScript-Funktionen ist das Gras grün. (Screenshot des Microsoft-Produkts, verwendet mit Berechtigung.)

Die Beispiel-App: Fugu Greetings

Für diesen Artikel verwende ich eine einfache PWA namens Fugu-Grüße (GitHub) Der Name dieser App ist nur ein kleiner Pluspunkt für Project Fugu 🐡 – damit das Web im Web die Leistungsfähigkeit von Android-/iOS-/Desktop-Anwendungen. Weitere Informationen zum Projekt finden Sie in der Landingpage.

Mit der Zeichen-App Fugu Greetings können Sie virtuelle Grußkarten erstellen und an deine Liebsten zu senden. Es zeigt beispielhaft Kernkonzepte von PWAs Es ist zuverlässig und vollständig offline aktiviert sind. Netzwerk haben, können Sie es weiterhin verwenden. Außerdem ist sie installierbar. zum Startbildschirm eines Geräts und ist nahtlos in das Betriebssystem integriert. als eigenständige Anwendung.

<ph type="x-smartling-placeholder">
</ph> Die PWA von Fugu hat eine Zeichnung, die dem Logo der PWA-Community ähnelt. <ph type="x-smartling-placeholder">
</ph> Die Beispiel-App Fugu Greetings.

Progressive Enhancement

Jetzt ist es an der Zeit, über Progressive Verbesserung zu sprechen. Das Glossar der MDN-Webdokumente beschreibt Konzept:

Progressive Enhancement ist eine Designphilosophie, die als Basis für wesentlichen Inhalten und Funktionen möglichst vielen Nutzenden zugänglich machen, während die bestmögliche Erfahrung nur den Nutzern mit den modernsten Browser, die den gesamten erforderlichen Code ausführen können.

Funktionserkennung wird im Allgemeinen verwendet, um zu bestimmen, ob Browser modernere Funktionen, während Polyfills werden häufig verwendet, um fehlende Funktionen mit JavaScript hinzuzufügen.

[…]

Progressive Enhancement ist eine nützliche Technik, mit der sich Webentwickler darauf konzentrieren können, die bestmöglichen Websites zu entwickeln und diese gleichzeitig für mehrere unbekannte User-Agents. Graceful Degradation sind verwandt, aber nicht dasselbe und wird oft als entgegengesetzte Richtung betrachtet zu Progressive Enhancement. Tatsächlich sind beide Ansätze sinnvoll 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 der herkömmlichen Herangehensweise <input type=file> -Element zu verwenden. Zuerst erstellen Sie das Element, legen type auf 'file' fest und fügen dem Attribut accept MIME-Typen hinzu. und dann programmatisch auf und auf Veränderungen warten. 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 eine Importfunktion vorhanden ist, sollte wahrscheinlich auch eine Exportfunktion vorhanden sein. damit Nutzer ihre Grußkarten lokal speichern können. Herkömmliche Methoden zum Speichern von Dateien sind Ankerlinks. mit download und mit einer Blob-URL als href. Außerdem würden Sie programmatisch auf um den Download auszulösen, Vergessen Sie nicht, die Blob-Objekt-URL zu widerrufen, um Speicherlecks zu vermeiden.

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 warte mal. Sie haben die Datei noch nicht "heruntergeladen", eine Grußkarte haben, "gespeichert" . Anstatt eine Schaltfläche zum Speichern in dem Sie auswählen können, wo die Datei gespeichert werden soll, Der Browser hat die Grußkarte direkt und ohne Interaktion des Nutzers heruntergeladen. und direkt im Ordner "Downloads" abgelegt. Das ist nicht so toll.

Was wäre, wenn es einen besseren Weg gäbe? Was wäre, wenn Sie einfach eine lokale Datei öffnen, sie bearbeiten und dann die Änderungen speichern könnten, entweder in einer neuen Datei oder in der ursprünglichen Datei, die Sie zuvor geöffnet hatten? Es stellt sich heraus, dass es eine gibt. Die File System Access API können Sie Dateien öffnen und erstellen sowie ändern und speichern .

Wie kann ich eine Feature-Erkennung für eine API durchführen? 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, verschiedene Import- und Exportmodule bedingt laden. Wie das geht, sehen Sie im Folgenden.

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 wir uns mit den Details der File System Access API beschäftigen, möchte ich hier kurz das Progressive-Enhancement-Muster hervorheben. In Browsern, die die File System Access API derzeit nicht unterstützen, lade ich die alten Skripts. Unten sehen Sie die Netzwerk-Tabs von Firefox und Safari.

<ph type="x-smartling-placeholder">
</ph> Safari Web Inspector, der zeigt, wie ältere Dateien geladen werden <ph type="x-smartling-placeholder">
</ph> Tab „Network“ in Safari Web Inspector.
<ph type="x-smartling-placeholder">
</ph> Firefox-Entwicklertools, die zeigen, wie alte Dateien geladen werden <ph type="x-smartling-placeholder">
</ph> Netzwerk-Tab der Firefox-Entwicklertools

In Chrome, einem Browser, der die API unterstützt, werden jedoch nur die neuen Skripts geladen. Dies wird elegant möglich dank dynamisches import(), das alle modernen Browser Support. Wie ich bereits sagte, ist das Gras heutzutage ziemlich grün.

<ph type="x-smartling-placeholder">
</ph> Chrome-Entwicklertools, in denen die modernen Dateien angezeigt werden, die geladen werden. <ph type="x-smartling-placeholder">
</ph> Tab „Netzwerk“ in den Chrome-Entwicklertools

Die File System Access API

Nachdem wir dieses Problem behandelt haben, sehen wir uns nun die tatsächliche Implementierung basierend auf dem File System Access API an. Zum Importieren eines Bildes rufe ich window.chooseFileSystemEntries() auf und übergeben eine accepts-Eigenschaft, wo ich Bilddateien angeben möchte. Es werden sowohl Dateierweiterungen als auch MIME-Typen unterstützt. Dies führt zu einem Datei-Handle, aus dem ich die eigentliche Datei abrufen kann, indem ich getFile() aufrufe.

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 Images ist fast identisch, diesmal jedoch Ich muss den Typparameter 'save-file' an die Methode chooseFileSystemEntries() übergeben. Daraufhin wird ein Dialogfeld zum Speichern von Dateien angezeigt. Bei geöffneter Datei war dies nicht erforderlich, da 'open-file' die Standardeinstellung ist. Ich habe den Parameter accepts ähnlich wie zuvor festgelegt, aber diesmal nur auf PNG-Bilder. Ich erhalte wieder ein Datei-Handle, aber statt die Datei abzurufen, dieses Mal erstelle ich einen beschreibbaren Stream, indem ich createWritable() aufrufe. Als Nächstes schreibe ich den Blob, das Bild meiner Grußkarte, in die Datei. Schließlich schließe ich den beschreibbaren Stream.

Alles kann immer ausfallen: Auf dem Laufwerk ist möglicherweise nicht genügend Speicherplatz, Möglicherweise liegt ein Schreib- oder Lesefehler vor oder der Nutzer schließt den Dateidialog einfach ab. Deshalb schließe ich die Aufrufe immer in eine try...catch-Anweisung ein.

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);
  }
};

Bei der Verwendung von Progressive Enhancement mit der File System Access API Ich kann eine Datei wie zuvor öffnen. Die importierte Datei wird direkt auf dem Canvas gezeichnet. Ich kann meine Änderungen vornehmen und sie schließlich in einem echten Dialogfeld zum Speichern speichern. Hier kann ich den Namen und den Speicherort der Datei auswählen. Jetzt kann die Datei für unbegrenzte Zeit aufbewahrt werden.

<ph type="x-smartling-placeholder">
</ph> Fugu-Begrüßungs-App mit einem Dialogfeld zum Öffnen von Dateien. <ph type="x-smartling-placeholder">
</ph> Dialogfeld zum Öffnen von Dateien
<ph type="x-smartling-placeholder">
</ph> Die Fugu-Begrüßungs-App enthält jetzt ein importiertes Bild. <ph type="x-smartling-placeholder">
</ph> Das importierte Image.
<ph type="x-smartling-placeholder">
</ph> Fugu Greetings-App mit dem geänderten Bild. <ph type="x-smartling-placeholder">
</ph> Das geänderte Bild wird in einer neuen Datei gespeichert.

Web Share und Web Share Target APIs

Außer für die Ewigkeit möchte ich meine Grußkarte vielleicht auch mit anderen teilen. Das ist etwas, das die Web Share API und Web Share Target API. Mobilgeräte und seit Kurzem Desktop-Betriebssysteme haben integrierte Freigabefunktionen. Mechanismen. Unten sehen Sie zum Beispiel das Share Sheet von Safari für macOS, das von einem Artikel über meinen Blog Durch Klicken auf die Schaltfläche Artikel teilen kannst du einen Link zu dem Artikel mit Freunden teilen: über die macOS Messages App.

<ph type="x-smartling-placeholder">
</ph> Das Share Sheet von Safari unter macOS wird über die Schaltfläche „Teilen“ eines Artikels ausgelöst <ph type="x-smartling-placeholder">
</ph> Web Share API in der Safari-Desktopversion unter macOS.

Der Code dafür ist ziemlich einfach. Ich rufe navigator.share() an und ein optionales title-, text- und url-Element in einem Objekt übergeben. 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 vorbereiten, das aus einem Blob besteht, und dann title und text. Als Nächstes verwende ich die neue navigator.canShare()-Methode, die was der Name schon sagt: Sie teilt mir mit, ob das data-Objekt, das ich freigeben möchte, technisch vom Browser geteilt werden kann. Wenn navigator.canShare() mir mitteilt, dass die Daten geteilt werden können, bin ich bereit rufen Sie navigator.share() wie zuvor auf. Weil alles fehlschlagen 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 nutze ich Progressive Enhancement. Wenn sowohl 'share' als auch 'canShare' im Objekt navigator vorhanden sind, gehe ich nur vor und share.mjs über dynamisches import() laden. In Browsern wie Safari auf dem Mobilgerät, die nur eine der beiden Bedingungen erfüllen, werde ich nicht die Funktionalität kennen.

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

Wenn ich in Fugu Begrüßungen in einem unterstützten Browser wie Chrome unter Android auf die Schaltfläche Teilen tippe, wird das integrierte Tabellenblatt zum Teilen geöffnet. Ich kann zum Beispiel Google Mail auswählen und das Widget zur Erstellung von E-Mails erscheint Bild angehängt.

<ph type="x-smartling-placeholder">
</ph> Freigabetabelle auf Betriebssystemebene mit verschiedenen Apps zum Teilen des Images. <ph type="x-smartling-placeholder">
</ph> App auswählen, für die die Datei freigegeben werden soll
<ph type="x-smartling-placeholder">
</ph> Gmail-Widget zum Schreiben von E-Mails mit dem angehängten Bild. <ph type="x-smartling-placeholder">
</ph> Die Datei wird an eine neue E-Mail im Editor von Gmail angehängt.

Die Contact Picker API

Als Nächstes möchte ich über Kontakte sprechen, also über das Adressbuch eines Geräts. oder Kontaktmanager-App. Es ist nicht immer einfach, Grußkarten richtig zu schreiben. auf den Namen einer Person. Ich habe zum Beispiel einen Freund Sergey, der seinen Namen lieber in kyrillischen Buchstaben schreiben möchte. Ich bin mit einer deutschen QWERTZ-Tastatur verwendet und wissen nicht, wie man den Namen eingibt. Dieses Problem kann mit der Contact Picker API behoben werden. Da ich eine Person in der Kontakte App auf meinem Smartphone gespeichert habe, kann ich über die Contacts Picker API auf meine Kontakte im Web zugreifen.

Zuerst muss ich die Liste der Properties angeben, auf die ich zugreifen möchte. In diesem Fall möchte ich nur die Namen, aber für andere Anwendungsfälle sind Telefonnummern, E-Mail-Adressen, Avatare Symbole oder physische Adressen. Als Nächstes konfiguriere ich ein options-Objekt und setze multiple auf true, damit ich mehr auswählen kann als ein Eintrag. Schließlich kann ich navigator.contacts.select() aufrufen, wodurch die gewünschten Eigenschaften zurückgegeben werden. für die vom Nutzer ausgewählten Kontakte.

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 inzwischen haben Sie das Muster wahrscheinlich kennengelernt: Ich lade die Datei nur, wenn die API tatsächlich unterstützt wird.

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

Wenn ich in der Fugu-Begrüßung auf die Schaltfläche Kontakte tippe und meine zwei besten Freunde auswähle, Kерргей 電кайлокиfenster БриLIMIT und 劳伦斯·爱德华·"拉里"·佩奇, können Sie sehen, wie die nur die Namen der Kontakte, nicht jedoch E-Mail-Adressen oder andere Informationen wie Telefonnummern. Ihre Namen werden dann auf meine Grußkarte gezeichnet.

<ph type="x-smartling-placeholder">
</ph> Kontaktauswahl mit den Namen von zwei Kontakten im Adressbuch. <ph type="x-smartling-placeholder">
</ph> Zwei Namen mit der Kontaktauswahl aus dem Adressbuch auswählen
<ph type="x-smartling-placeholder">
</ph> Die Namen der beiden zuvor ausgewählten Kontakte wurden auf die Grußkarte gezeichnet. <ph type="x-smartling-placeholder">
</ph> Die beiden Namen werden dann auf die Grußkarte gezeichnet.

Die Asynchronous Clipboard API

Als Nächstes folgt das Kopieren und Einfügen. Eine unserer Lieblingsbeschäftigungen als Softwareentwickler ist Kopieren und Einfügen. Als Grußkartenautor möchte ich dasselbe manchmal tun. Ich möchte entweder ein Bild in eine Grußkarte einfügen, an der ich arbeite, oder kopiere meine Grußkarte, damit ich sie über woanders hin. Async Clipboard API unterstützt sowohl Text als auch Bilder. Ich erkläre Ihnen gern, wie ich die Unterstützung für Kopieren und Einfügen auf der Fugu-Website hinzugefügt habe. Begrüßungs-App.

Um etwas in die Zwischenablage des Systems zu kopieren, muss ich darin schreiben. Bei der Methode navigator.clipboard.write() wird ein Array von Elementen in der Zwischenablage als . Jedes Element in der Zwischenablage ist im Wesentlichen ein Objekt mit einem Blob als Wert und der Typ des Blobs. als Schlüssel ein.

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 in der Zwischenablage durchgehen, die ich durch Aufruf von navigator.clipboard.read() Das liegt daran, dass sich in der Zwischenablage möglicherweise mehrere Elemente verschiedene Darstellungen. Jedes Element in der Zwischenablage hat ein types-Feld mit den MIME-Typen der verfügbaren Ressourcen. Ich rufe die getType()-Methode des Zwischenablage-Elements auf und übermittle den MIME-Typ, den ich zuvor abgerufen habe.

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 unnötig. Ich mache das nur mit 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 macOS Preview App geöffnet und in die Zwischenablage kopieren. Wenn ich auf Einfügen klicke, werden Sie in der Fugu Begrüßungen App gefragt, ob ich der App erlauben möchte, Text und Bilder in der Zwischenablage abzurufen.

<ph type="x-smartling-placeholder">
</ph> Fugu Greetings-App mit einer Aufforderung zur Berechtigung für die Zwischenablage. <ph type="x-smartling-placeholder">
</ph> Die Berechtigungsaufforderung für die Zwischenablage.

Nachdem Sie die Genehmigung 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.

<ph type="x-smartling-placeholder">
</ph> Die macOS Preview App mit einem unbenannten, gerade eingefügten Bild. <ph type="x-smartling-placeholder">
</ph> Ein Bild, das in die macOS Preview App eingefügt wurde.

Die Badging API

Eine weitere nützliche API ist die Badging API. Als installierbare PWA hat Fugu Greetings natürlich ein App-Symbol die Nutzer auf dem App-Dock oder auf dem Startbildschirm platzieren können. Eine unterhaltsame und einfache Möglichkeit, die API zu demonstrieren, besteht darin, sie in Fugu-Grüßen zu verwenden. als Stiftstrichzähler. Ich habe einen Event-Listener hinzugefügt, der den Stiftzähler erhöht, wenn das pointerdown-Ereignis eintritt und legt dann das aktualisierte Symbol fest. Sobald der Canvas geleert wird, wird der Zähler zurückgesetzt und das Logo entfernt.

let strokes = 0;

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

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

Bei dieser Funktion handelt es sich um eine progressive Verbesserung, die Ladelogik ist also wie gewohnt.

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

In diesem Beispiel habe ich mit einem Stift die Zahlen von eins bis sieben gezeichnet. pro Nummer. Der Badge-Zähler auf dem Symbol steht jetzt bei sieben.

<ph type="x-smartling-placeholder">
</ph> Die Zahlen von eins bis sieben werden auf die Grußkarte gezeichnet, jede mit nur einem Stift. <ph type="x-smartling-placeholder">
</ph> Zeichnen Sie mit sieben Stiftstrichen die Zahlen von 1 bis 7.
<ph type="x-smartling-placeholder">
</ph> Badge-Symbol in der Fugu Greetings App mit der Zahl 7. <ph type="x-smartling-placeholder">
</ph> Der Stiftstrichzähler in Form des App-Symbol-Badges.

Die Periodic Background Sync API

Du möchtest jeden Tag mit etwas Neuem beginnen? Eine tolle Funktion der Fugu Begrüßungs-App ist, dass sie dich jeden Morgen inspiriert. mit einem neuen Hintergrundbild, um mit Ihrer Grußkarte zu beginnen. Die App verwendet die Periodic Background Sync API. um dies zu erreichen.

Der erste Schritt besteht darin, ein regelmäßiges Synchronisierungsereignis in der Service Worker-Registrierung zu registrieren. Wartet auf das Synchronisierungs-Tag 'image-of-the-day' Das Intervall beträgt mindestens einen Tag. sodass der Nutzer alle 24 Stunden ein neues Hintergrundbild erhalten kann.

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 zuvor registrierte Tag, Das Bild des Tages wird mit der Funktion getImageOfTheDay() abgerufen. Das Ergebnis wird an alle Kunden weitergegeben, sodass sie ihre Canvases aktualisieren und Caches.

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 hier handelt es sich um eine progressive Verbesserung, d. h., der Code wird nur geladen, Die API wird vom Browser unterstützt. Dies gilt sowohl für den Clientcode als auch für den Service Worker-Code. In nicht unterstützten Browsern wird keiner von beiden geladen. Im Service Worker anstelle eines dynamischen import()-Objekts (wird in einem Service Worker-Kontext nicht unterstützt) noch) Ich nutze den klassischen 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');
}

Wenn Sie in Fugu entsprechende Benachrichtigungen drücken, wird das Grußkartenbild des Tages angezeigt, wenn Sie auf die Schaltfläche Hintergrund klicken. die täglich über die Periodic Background Sync API aktualisiert wird.

<ph type="x-smartling-placeholder">
</ph> Die Fugu-Begrüßungs-App zeigt ein neues Grußkartenbild des Tages. <ph type="x-smartling-placeholder">
</ph> Wenn du auf die Schaltfläche Hintergrund tippst, wird das Bild des Tages angezeigt.

Notification Triggers API

Manchmal braucht man auch bei viel Inspiration einen Anstupser, um eine begonnene Begrüßung fertigzustellen . Diese Funktion wird durch die Notification Triggers API aktiviert. Als Nutzer kann ich eine Uhrzeit eingeben, zu der ich daran erinnert werden möchte, meine Grußkarte fertigzustellen. Zu diesem Zeitpunkt werde ich eine Benachrichtigung erhalten, dass meine Grußkarte bereitsteht.

Nach der Aufforderung zur Zielzeit Die Anwendung plant die Benachrichtigung mit einem showTrigger. Dies 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, ist dies eine progressive Verbesserung, sodass der Code nur bedingt geladen wird.

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

Wenn ich das Kästchen Erinnerung in Fugu Begrüßungen auswähle, werden Sie gefragt, wenn ich daran erinnert werden möchte, meine Grußkarte fertigzustellen.

<ph type="x-smartling-placeholder">
</ph> Fugu Begrüßungs-App mit einer Aufforderung, in der der Nutzer gefragt wird, wann er daran erinnert werden möchte, die Grußkarte fertigzustellen. <ph type="x-smartling-placeholder">
</ph> Sie planen eine lokale Benachrichtigung, die daran erinnert wird, eine Grußkarte fertigzustellen.

Wenn eine geplante Benachrichtigung in Fugu-Begrüßungen ausgelöst wird, wie jede andere Benachrichtigung angezeigt, aber wie bereits erwähnt, war keine Netzwerkverbindung erforderlich.

<ph type="x-smartling-placeholder">
</ph> macOS-Benachrichtigungscenter mit einer ausgelösten Fugu-Benachrichtigung. <ph type="x-smartling-placeholder">
</ph> Die ausgelöste Benachrichtigung wird im macOS-Benachrichtigungscenter angezeigt.

Die Wake Lock API

Außerdem möchte ich die Wake Lock API einbeziehen. Manchmal musst du nur lange genug auf den Bildschirm starren, bis du dich inspiriert hast. küsst dich. Das Schlimmste, was dann passieren kann, ist, dass der Bildschirm ausgeschaltet wird. Die Wake Lock API kann dies verhindern.

Der erste Schritt besteht darin, mit navigator.wakelock.request method() einen Wakelock zu erhalten. Ich übergebe den String 'screen', um einen Display-Wakelock 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 den Wakelock neu anfordern, sobald der Tab wieder sichtbar wird.

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, es handelt sich um eine progressive Verbesserung, daher muss ich sie nur laden, wenn der Browser unterstützt die API.

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, Display aktiviert ist.

<ph type="x-smartling-placeholder">
</ph> Wenn das Kästchen „Schlaflosigkeit“ aktiviert ist, bleibt der Bildschirm aktiv. <ph type="x-smartling-placeholder">
</ph> Durch das Kästchen Schlaflosigkeit bleibt die App aktiv.

Die Idle Detection API

Auch wenn Sie stundenlang auf den Bildschirm starren, Es ist einfach nutzlos und Ihnen fällt nicht auf, was Sie mit der Grußkarte anfangen sollen. Mit der Idle Detection API kann die App die Inaktivitätszeit von Nutzern erkennen. Wenn der Nutzer zu lange inaktiv ist, wird die App auf den Ausgangszustand zurückgesetzt und löscht den Canvas. Diese API befindet sich derzeit hinter der Berechtigung für Benachrichtigungen da viele Anwendungsfälle der Inaktivitätserkennung Benachrichtigungen und Benachrichtigungen nur an Geräte senden, die der Nutzer gerade aktiv verwendet.

Nachdem ich sichergestellt habe, dass die Berechtigung zum Senden von Benachrichtigungen gewährt wurde, instanziiere ich die den Inaktivitätsdetektor. Ich registriere einen Event-Listener, der inaktive Änderungen überwacht, einschließlich Nutzer- und Bildschirmstatus. Der Nutzer kann aktiv oder inaktiv sein, und das Display kann entsperrt oder gesperrt werden. Ist der Nutzer inaktiv, wird der Canvas gelöscht. Ich lege für den Inaktivitätsdetektor einen Schwellenwert 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,
});

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

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

In der Fugu Begrüßungen-App wird der Canvas gelöscht, wenn das Kästchen Sitzungsspezifisch überprüft und der Nutzer zu lange inaktiv ist.

<ph type="x-smartling-placeholder">
</ph> Fugu-Begrüßungs-App mit gelöschtem Canvas, nachdem der Nutzer zu lange inaktiv war. <ph type="x-smartling-placeholder">
</ph> Wenn das Kästchen Sitzungsspezifisch 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: Ich zwinge Nutzer nie dazu, die Downloadkosten zu bezahlen, für eine Funktion, die ihr Browser nicht unterstützt. Durch die Verwendung von Progressive Enhancement stelle ich sicher, dass nur der relevante Code geladen wird. Und da Anfragen bei HTTP/2 günstig sind, sollte dieses Muster für viele Anwendungen, Allerdings sollten Sie einen Bundler für sehr große Apps in Betracht ziehen.

<ph type="x-smartling-placeholder">
</ph> Im Bereich „Netzwerk“ in Chrome DevTools werden nur Anfragen für Dateien mit Code angezeigt, der vom aktuellen Browser unterstützt wird. <ph type="x-smartling-placeholder">
</ph> 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 ein wenig anders aussehen, da nicht alle Plattformen alle Funktionen unterstützen. aber die Hauptfunktion ist immer verfügbar und wird entsprechend den jeweiligen Browserfunktionen schrittweise verbessert. Diese Funktionen können sich auch in ein und demselben Browser ändern. je nachdem, ob die App als installierte App oder in einem Browsertab ausgeführt wird.

<ph type="x-smartling-placeholder">
</ph> Auf Android-Geräten ausgeführte Fugu-Grüße werden in Chrome mit vielen verfügbaren Funktionen angezeigt. <ph type="x-smartling-placeholder">
</ph> Fugu Greetings unter Android Chrome.
<ph type="x-smartling-placeholder">
</ph> Fugu-Grüße werden in der Desktopversion von Safari ausgeführt und es werden weniger Funktionen angezeigt. <ph type="x-smartling-placeholder">
</ph> Fugu Greetings wird in der Desktopversion von Safari ausgeführt.
<ph type="x-smartling-placeholder">
</ph> Fugu Begrüßungen werden in der Desktopversion von Chrome mit vielen verfügbaren Funktionen angezeigt. <ph type="x-smartling-placeholder">
</ph> Die Fugu-Grüße werden in der Desktopversion von Chrome ausgeführt.

Wenn Sie sich für die App Fugu Greetings interessieren, auf GitHub.

<ph type="x-smartling-placeholder">
</ph> Repository für Fugu-Grüße auf GitHub. <ph type="x-smartling-placeholder">
</ph> App Fugu Greetings auf GitHub.

Das Chromium-Team arbeitet intensiv daran, das Gras für moderne Fugu-APIs umweltfreundlicher zu gestalten. Indem ich bei der Entwicklung meiner App Progressive Enhancement anwende, Ich sorge dafür, dass alle eine gute und solide Grunderfahrung erhalten, aber Nutzer von Browsern, die mehr Webplattform-APIs unterstützen, profitieren von einer noch besseren Nutzererfahrung. Ich bin gespannt, wie Sie die progressive Verbesserung in Ihren Apps umsetzen.

Danksagungen

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