Améliorer progressivement votre progressive web app

Développer pour les navigateurs modernes et améliorer progressivement ses performances, comme en 2003

En mars 2003, Nick Finck et Steve Champeon a stupéfié le monde de la conception Web avec le concept de amélioration progressive, une stratégie de conception web qui met l'accent sur le chargement du contenu principal de la page web en premier, puis ajoute progressivement des nuanciers et techniquement rigoureuses de présentation et de fonctionnalités en plus du contenu. Alors qu'en 2003, l'amélioration progressive consistait à utiliser, à l'époque, l'utilisation des fonctionnalités CSS, un code JavaScript discret et même simplement des graphiques vectoriels évolutifs. En 2020 et au-delà, l'amélioration progressive concerne l'utilisation des fonctionnalités récentes du navigateur.

<ph type="x-smartling-placeholder">
</ph> Conception Web inclusive pour l&#39;avenir avec amélioration progressive. Diapositive de titre de la présentation originale de Finck et Champeon. <ph type="x-smartling-placeholder">
</ph> Diapositive: Conception Web inclusive pour l'avenir avec amélioration progressive. (source).

JavaScript moderne

Concernant JavaScript, voici comment fonctionne la compatibilité des navigateurs avec le dernier code JavaScript principal d'ES 2015. les caractéristiques. La nouvelle norme inclut les promesses, les modules, les classes, les littéraux de modèle, les fonctions fléchées, let et const, les paramètres par défaut, les générateurs, l'attribution de déstructuration, le repos et les écarts, Map/Set, WeakMap/WeakSet, et bien d'autres. Tous les types d'appareils sont acceptés.

<ph type="x-smartling-placeholder">
</ph> Tableau de compatibilité de CanIUse pour les fonctionnalités ES6, indiquant la compatibilité avec les principaux navigateurs. <ph type="x-smartling-placeholder">
</ph> Tableau des navigateurs compatibles avec ECMAScript 2015 (ES6). (source).

Les fonctions asynchrones, une fonctionnalité d'ES 2017 et l'une de mes préférées, peuvent être utilisés dans tous les principaux navigateurs. Les mots clés async et await permettent un comportement asynchrone basé sur des promesses soit rédigé dans un style plus épuré, en évitant de configurer explicitement des chaînes de promesses.

<ph type="x-smartling-placeholder">
</ph> Tableau de compatibilité de CanIUse pour les fonctions asynchrones, indiquant la compatibilité avec les principaux navigateurs. <ph type="x-smartling-placeholder">
</ph> Tableau sur les navigateurs compatibles avec les fonctions asynchrones. (source).

Même les tout derniers ajouts de langues en ES 2020, comme chaînage facultatif et coalesçage nulliste ont rapidement contacté le service d'assistance. Vous trouverez un exemple de code ci-dessous. Concernant les fonctionnalités JavaScript essentielles, l'herbe est purement verte a lieu aujourd'hui.

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> Image de fond emblématique de l&#39;herbe verte de Windows XP.
Les principales fonctionnalités de JavaScript ne sont pas négligeables. (Capture d'écran du produit Microsoft, utilisée avec autorisation.)

Application exemple: Fugu Greetings

Pour cet article, je travaille avec une PWA simple, appelée Salutations Fugu (GitHub). Le nom de cette application est un bravo pour Project Fugu 🐡, une initiative visant à offrir tout le Web la puissance des applications Android/iOS/de bureau. Pour en savoir plus sur le projet, consultez son page de destination.

Fugu Greetings est une application de dessin qui vous permet de créer des cartes de vœux virtuelles et d'envoyer à vos proches. Il illustre parfaitement Concepts fondamentaux des PWA. Il est fiable et entièrement hors connexion. Ainsi, même si vous ne si vous disposez d'un réseau, vous pouvez toujours l'utiliser. Elle est également Installable à l'écran d'accueil d'un appareil et s'intègre parfaitement au système d'exploitation en tant qu'application autonome.

<ph type="x-smartling-placeholder">
</ph> PWA Fugu Greetings avec un dessin ressemblant au logo de la communauté PWA. <ph type="x-smartling-placeholder">
</ph> L'application exemple Fugu Greetings.

Amélioration progressive

Maintenant que tout cela est terminé, intéressons-nous à l'amélioration progressive. Le glossaire MDN Web Docs définit le concept comme suit:

L'amélioration progressive est une philosophie de conception qui fournit une base de le contenu et les fonctionnalités essentiels au plus grand nombre d'utilisateurs possible, tandis que fournir la meilleure expérience possible uniquement aux utilisateurs de la version la plus récente qui peuvent exécuter tout le code requis.

Détection de caractéristiques sert généralement à déterminer si les navigateurs acceptent des fonctionnalités plus modernes, tandis que les polyfills sont souvent utilisés pour ajouter des caractéristiques manquantes avec JavaScript.

[…]

L'amélioration progressive est une technique utile qui permet aux développeurs Web de se concentrer à développer les meilleurs sites Web possibles tout en faisant fonctionner ces sites Web sur plusieurs user-agents inconnus. Dégradation progressive est liée, mais n'est pas la même chose et est souvent considérée comme allant dans la direction opposée. à l'amélioration progressive. En réalité, les deux approches sont valables et peuvent souvent se compléter.

Contributeurs MMD

Commencer chaque carte de vœux à partir de zéro peut être très fastidieux. Alors pourquoi ne pas proposer une fonctionnalité qui permet aux utilisateurs d'importer une image et de partir de là ? Avec une approche traditionnelle, vous auriez utilisé un <input type=file> pour y parvenir. Tout d'abord, vous devez créer l'élément, définir sa propriété type sur 'file' et ajouter des types MIME à la propriété accept, puis "cliquez" de façon programmatique et écouter les changements. Lorsque vous sélectionnez une image, elle est importée directement dans le canevas.

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

Lorsqu'il existe une fonctionnalité d'importation, une fonctionnalité d'exportation devrait être disponible afin que les utilisateurs puissent enregistrer leurs cartes de vœux localement. La méthode traditionnelle d'enregistrement des fichiers consiste à créer un lien d'ancrage. avec un download et avec une URL blob comme href. Par programmation, vous pouvez aussi "cliquer" pour déclencher le téléchargement, et, pour éviter les fuites de mémoire, pensez à révoquer l'URL de l'objet blob.

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

Attendez une minute. Mentalement, vous n'avez pas « téléchargé » une carte de vœux, vous avez "enregistré" Plutôt que d'afficher un bouton "Enregistrer" qui vous permet de choisir où placer le fichier, Le navigateur a téléchargé directement la carte de vœux sans intervention de l'utilisateur. et l'a placé directement dans le dossier Téléchargements. Ce n'est pas génial.

Et s'il existait une meilleure solution ? Et si vous pouviez simplement ouvrir un fichier local, le modifier, puis enregistrer les modifications, dans un nouveau fichier ou au fichier d'origine que vous aviez initialement ouvert ? Il s’avère que c’est le cas. L'API File System Access vous permet d'ouvrir et de créer des fichiers et des répertoires, et les modifier et les enregistrer .

Comment détecter une API ? L'API File System Access expose une nouvelle méthode window.chooseFileSystemEntries(). Par conséquent, je dois charger de manière conditionnelle différents modules d'importation et d'exportation selon que cette méthode est disponible ou non. Vous trouverez ci-dessous la procédure à suivre.

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'),
    ]);
  }
};

Mais avant de plonger dans les détails de l'API File System Access, permettez-moi de souligner rapidement le modèle d'amélioration progressive. Dans les navigateurs qui ne sont actuellement pas compatibles avec l'API File System Access, je charge les anciens scripts. Vous trouverez ci-dessous les onglets réseau de Firefox et Safari.

<ph type="x-smartling-placeholder">
</ph> L&#39;inspecteur Web de Safari montrant le chargement des anciens fichiers <ph type="x-smartling-placeholder">
</ph> Onglet "Réseau" de l'inspecteur Web de Safari
<ph type="x-smartling-placeholder">
</ph> Outils pour les développeurs Firefox montrant le chargement des anciens fichiers <ph type="x-smartling-placeholder">
</ph> Onglet "Réseau" des outils pour les développeurs Firefox.

Toutefois, dans Chrome, un navigateur qui prend en charge l'API, seuls les nouveaux scripts sont chargés. Tout cela est possible avec élégance grâce à import() dynamique, compatible avec tous les navigateurs récents assistance. Comme je l'ai dit tout à l'heure, l'herbe est plutôt verte ces jours-ci.

<ph type="x-smartling-placeholder">
</ph> Outils pour les développeurs Chrome montrant les fichiers modernes en cours de chargement <ph type="x-smartling-placeholder">
</ph> Onglet "Réseau" des outils pour les développeurs Chrome.

API File System Access

Maintenant que nous avons résolu ce problème, il est temps d'examiner l'implémentation effective basée sur l'API File System Access. Pour importer une image, j'appelle window.chooseFileSystemEntries(). et lui transmettre une propriété accepts où je veux des fichiers image. Les extensions de fichier et les types MIME sont acceptés. Il en résulte un handle de fichier à partir duquel je peux obtenir le fichier réel en appelant getFile().

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

L'exportation d'une image est presque la même, mais cette fois Je dois transmettre un paramètre de type 'save-file' à la méthode chooseFileSystemEntries(). J'obtiens alors une boîte de dialogue pour enregistrer un fichier. Lorsque le fichier est ouvert, cela n'était pas nécessaire puisque 'open-file' est le paramètre par défaut. J'ai défini le paramètre accepts comme précédemment, mais cette fois-ci en me limitant aux images PNG. Encore une fois, je récupère un handle de fichier, mais au lieu d’obtenir le fichier, cette fois, je crée un flux accessible en écriture en appelant createWritable(). Ensuite, j'écris le blob, qui est l'image de ma carte de vœux, dans le fichier. Enfin, je ferme le flux accessible en écriture.

Tout peut toujours échouer: le disque peut manquer d'espace, il se peut qu'il y ait une erreur d'écriture ou de lecture, ou peut-être simplement que l'utilisateur annule la boîte de dialogue du fichier. C'est pourquoi j'encapsule toujours les appels dans une instruction try...catch.

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

En utilisant l'amélioration progressive avec l'API File System Access, je peux ouvrir un fichier comme avant. Le fichier importé est dessiné directement sur le canevas. Je peux apporter mes modifications, puis les enregistrer avec une boîte de dialogue d'enregistrement réelle. où je peux choisir le nom et l’emplacement de stockage du fichier. Le fichier est maintenant prêt à être conservé pour l'éternité.

<ph type="x-smartling-placeholder">
</ph> Application Fugu Greetings avec une boîte de dialogue d&#39;ouverture de fichier. <ph type="x-smartling-placeholder">
</ph> Boîte de dialogue d'ouverture de fichier.
<ph type="x-smartling-placeholder">
</ph> L&#39;application Fugu Greetings dispose désormais d&#39;une image importée. <ph type="x-smartling-placeholder">
</ph> Image importée.
<ph type="x-smartling-placeholder">
</ph> Application Fugu Greetings avec l&#39;image modifiée. <ph type="x-smartling-placeholder">
</ph> Enregistrement de l'image modifiée dans un nouveau fichier...

API Web Share et Web Share Target

En plus du stockage pour l'éternité, je veux peut-être partager ma carte de vœux. C'est un aspect que l'API Web Share et API Web Share Target me permettent de le faire. Les systèmes d'exploitation pour mobile, et plus récemment, pour les ordinateurs de bureau, ont développé le partage intégré ces mécanismes. Par exemple, vous trouverez ci-dessous la feuille de partage de bureau de Safari sur macOS déclenchée à partir d'un article sur mon blog. Lorsque vous cliquez sur le bouton Partager l'article, vous pouvez partager un lien vers l'article avec un ami, par par exemple via l'application Messages de macOS.

<ph type="x-smartling-placeholder">
</ph> Feuille de partage de l&#39;application Safari pour ordinateur sous macOS déclenchée par le bouton &quot;Partager&quot; d&#39;un article <ph type="x-smartling-placeholder">
</ph> API Web Share dans Safari pour ordinateur sous macOS.

Pour ce faire, le code est assez simple. J'appelle navigator.share() et transmettez-lui des éléments facultatifs title, text et url dans un objet. Comment joindre une image ? Le niveau 1 de l'API Web Share n'est pas encore compatible. La bonne nouvelle, c'est que Web Share niveau 2 dispose de fonctionnalités de partage de fichiers supplémentaires.

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

Laissez-moi vous montrer comment faire fonctionner cela avec l'application de carte de vœux Fugu. Tout d'abord, je dois préparer un objet data avec un tableau files composé d'un blob, puis un title et un text. Ensuite, nous vous recommandons d'utiliser la nouvelle méthode navigator.canShare(), qui comme son nom l'indique: Il m'indique si le navigateur peut techniquement partager l'objet data que j'essaie de partager. Si navigator.canShare() m'indique que les données peuvent être partagées, je suis prêt à appelez navigator.share() comme précédemment. Comme tout peut échouer, j'utilise à nouveau un bloc try...catch.

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

Comme précédemment, j'utilise l'amélioration progressive. Si 'share' et 'canShare' existent tous les deux sur l'objet navigator, c'est seulement que j'avance et charger share.mjs via une import() dynamique. Dans les navigateurs comme Safari pour mobile qui ne remplissent que l'une des deux conditions, je ne charge pas la fonctionnalité.

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

Dans Fugu Greetings, si j'appuie sur le bouton Partager dans un navigateur compatible comme Chrome sur Android, la feuille de partage intégrée s'ouvre. Je peux, par exemple, choisir Gmail pour que le widget de rédaction d'e-mails s'affiche avec le image jointe.

<ph type="x-smartling-placeholder">
</ph> Feuille de partage au niveau de l&#39;OS montrant les différentes applications avec lesquelles partager l&#39;image. <ph type="x-smartling-placeholder">
</ph> sélectionner l'application avec laquelle partager le fichier ;
<ph type="x-smartling-placeholder">
</ph> Widget de rédaction d&#39;e-mails de Gmail avec l&#39;image en pièce jointe <ph type="x-smartling-placeholder">
</ph> Le fichier est joint à un nouvel e-mail dans l'outil de rédaction de Gmail.

API Contact Picker

Ensuite, je veux parler des contacts, c'est-à-dire du carnet d'adresses d'un appareil, ou de gestion de contacts. Lorsque vous écrivez une carte de vœux, il n'est pas toujours facile d'écrire correctement le nom de quelqu'un. Par exemple, j'ai un ami Sergey qui préfère que son nom soit orthographié en lettres cyrilliques. Je suis utilisant un clavier QWERTZ allemand et vous n'avez aucune idée de la façon de taper leur nom. L'API Contact Picker peut résoudre ce problème. Étant donné que mon ami est stocké dans l'application de contacts de mon téléphone, via l'API Contacts Picker, je peux accéder à mes contacts sur le Web.

Tout d'abord, je dois spécifier la liste des propriétés auxquelles je souhaite accéder. Dans ce cas, je ne veux que les noms, Mais pour d'autres cas d'utilisation, je pourrais m'intéresser aux numéros de téléphone, aux adresses e-mail, aux avatars des icônes ou des adresses physiques. Ensuite, je configure un objet options et je définis multiple sur true afin de pouvoir en sélectionner davantage. plusieurs entrées. Enfin, je peux appeler navigator.contacts.select(), qui renvoie les propriétés souhaitées pour les contacts sélectionnés par l'utilisateur.

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

À ce stade, vous avez probablement appris le modèle: Je ne charge le fichier que lorsque l'API est réellement prise en charge.

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

Dans Fugu Greeting, lorsque j'appuie sur le bouton Contacts et que je sélectionne mes deux meilleurs amis, Radio Po vous pouvez voir comment le sélecteur de contacts est limité à afficher uniquement leur nom, mais pas leurs adresses e-mail, ni d’autres informations comme leurs numéros de téléphone. Leurs noms sont ensuite dessinés sur ma carte de vœux.

<ph type="x-smartling-placeholder">
</ph> Outil de sélection de contacts affichant les noms de deux contacts dans le carnet d&#39;adresses. <ph type="x-smartling-placeholder">
</ph> Sélection de deux noms à l'aide du sélecteur de contacts dans le carnet d'adresses.
<ph type="x-smartling-placeholder">
</ph> Les noms des deux contacts précédemment sélectionnés qui sont dessinés sur la carte de vœux. <ph type="x-smartling-placeholder">
</ph> Les deux noms sont ensuite dessinés sur la carte de vœux.

API Asynchronous Clipboard

L'étape suivante consiste à copier et coller. L'une de nos opérations préférées en tant que développeurs logiciels est le copier-coller. En tant qu'auteur de cartes de vœux, je peux parfois vouloir faire de même. Je veux peut-être coller une image dans une carte de vœux sur laquelle je travaille, ou copier ma carte de vœux pour que je puisse continuer à la modifier ailleurs. l'API Async Clipboard ; prend en charge à la fois du texte et des images. Laissez-moi vous expliquer comment j'ai ajouté la prise en charge du copier-coller dans Fugu Application d'accueil.

Pour copier un élément dans le presse-papiers du système, je dois y écrire. La méthode navigator.clipboard.write() utilise un tableau d'éléments du presse-papiers en tant que . Chaque élément du presse-papiers est essentiellement un objet avec un blob comme valeur et le type de l'objet blob. comme clé.

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

Pour coller, je dois effectuer une boucle sur les éléments du presse-papiers que j'obtiens en appelant navigator.clipboard.read() La raison est que plusieurs éléments du presse-papiers peuvent se trouver dans le presse-papiers dans différentes représentations. Chaque élément du presse-papiers comporte un champ types qui indique les types MIME des types de fichiers ressources. J'appelle la méthode getType() de l'élément du presse-papiers, en transmettant Type MIME obtenu précédemment.

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

Et c'est presque inutile de le dire à présent. Je le fais uniquement sur les navigateurs pris en charge.

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

Comment cela fonctionne-t-il concrètement ? J'ai une image ouverte dans l'application Preview de macOS et le copier dans le presse-papiers. Lorsque je clique sur Coller, l'application Fugu Greetings me demande alors si je veux autoriser l'application à voir le texte et les images du presse-papiers.

<ph type="x-smartling-placeholder">
</ph> Application Fugu Greetings affichant l&#39;invite d&#39;autorisation du presse-papiers. <ph type="x-smartling-placeholder">
</ph> Invite d'autorisation du presse-papiers.

Enfin, après avoir accepté l'autorisation, l'image est collée dans l'application. L'inverse fonctionne également. Je copie une carte de vœux dans le presse-papiers. Lorsque j'ouvre l'aperçu et que je clique sur Fichier, puis sur Nouveau depuis le presse-papiers, la carte de vœux est collée dans une nouvelle image sans titre.

<ph type="x-smartling-placeholder">
</ph> Application Preview de macOS avec une image sans titre, juste collée. <ph type="x-smartling-placeholder">
</ph> Une image collée dans l'application Preview de macOS
.

API Badging

L'API Badging est également utile. Fugu Greetings est bien sûr associée à une icône d'application, car il s'agit d'une PWA installable. que les utilisateurs peuvent placer sur la barre d'applications ou l'écran d'accueil. Un moyen simple et amusant de présenter l'API consiste à l'utiliser (ou de l'utiliser de manière abusive) dans Fugu Greetings. en tant que compteur de coups de stylo. J'ai ajouté un écouteur d'événements qui incrémente le compteur de traits de crayon à chaque fois que l'événement pointerdown se produit. puis définit le nouveau badge d'icône. Dès que la toile est effacée, le compteur est réinitialisé et le badge est supprimé.

let strokes = 0;

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

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

Comme il s'agit d'une amélioration progressive, la logique de chargement reste la même.

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

Dans cet exemple, j'ai dessiné les chiffres de un à sept en un trait de crayon. par numéro. Le nombre de badges sur l'icône est désormais de sept.

<ph type="x-smartling-placeholder">
</ph> Les chiffres de un à sept sont dessinés sur la carte de vœux, chacun avec un seul coup de stylo. <ph type="x-smartling-placeholder">
</ph> Dessiner les nombres de 1 à 7 à l'aide de sept traits de stylo.
<ph type="x-smartling-placeholder">
</ph> Icône de badge sur l&#39;application Fugu Greetings affichant le chiffre 7. <ph type="x-smartling-placeholder">
</ph> Compteur de coups de crayon, sous la forme d'un badge d'icône d'application

API Periodic Background Sync

Vous voulez prendre un nouveau départ chaque journée avec quelque chose de nouveau ? L'application Fugu Greetings offre une fonctionnalité intéressante : elle peut vous inspirer chaque matin. avec une nouvelle image de fond pour commencer votre carte de vœux. L'application utilise l'API Periodic Background Sync. pour y parvenir.

La première étape consiste à enregistrer un événement de synchronisation périodique dans l'enregistrement du service worker. Il écoute un tag de synchronisation appelé 'image-of-the-day'. et a un intervalle minimal d'un jour, afin que l'utilisateur puisse obtenir une nouvelle image de fond toutes les 24 heures.

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

La deuxième étape consiste à écouter l'événement periodicsync dans le service worker. Si le tag d'événement est 'image-of-the-day' (c'est-à-dire celui qui a été enregistré auparavant), l'image du jour est récupérée via la fonction getImageOfTheDay(), Le résultat est propagé à tous les clients, pour qu'ils puissent mettre à jour leurs canevas et 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,
          });
        });
      })()
    );
  }
});

Encore une fois, il s'agit d'une amélioration progressive. Le code n'est chargé que lorsque le est compatible avec le navigateur. Cela s'applique à la fois au code client et au code du service worker. Sur les navigateurs non compatibles, aucun des deux n'est chargé. Notez que dans le service worker, au lieu d'un import() dynamique (non disponible dans le contexte d'un service worker, encore), J'utilise la version classique 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');
}

Dans Fugu Greetings, appuyer sur le bouton Fond d'écran permet d'afficher l'image de la carte de vœux du jour. mis à jour quotidiennement via l'API Periodic Background Sync.

<ph type="x-smartling-placeholder">
</ph> Application Fugu Greetings avec une nouvelle image de carte de vœux du jour. <ph type="x-smartling-placeholder">
</ph> Appuyez sur le bouton Fond d'écran pour afficher l'image du jour.

API Notification Triggers

Parfois, même avec beaucoup d'inspiration, vous avez besoin d'un coup de pouce pour terminer une salutation qui a commencé . Cette fonctionnalité est activée par l'API Notification Triggers. En tant qu'utilisateur, je peux saisir un moment auquel je souhaite être invité à terminer ma carte de vœux. À ce moment-là, je recevrai une notification indiquant que ma carte de vœux sera disponible.

Après avoir demandé l'heure cible, l'application planifie la notification avec un showTrigger. Il peut s'agir d'un TimestampTrigger avec la date cible précédemment sélectionnée. La notification de rappel sera déclenchée localement. Aucun côté réseau ou serveur n'est nécessaire.

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

Comme pour tout ce que j'ai montré jusqu'à présent, c'est une amélioration progressive, Le code n'est donc chargé que de manière conditionnelle.

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

Lorsque je coche la case Rappel dans Fugu Greetings, une invite me demande moi quand je veux qu'on me rappelle de terminer ma carte de vœux.

<ph type="x-smartling-placeholder">
</ph> Application Fugu Greetings avec une invite demandant à l&#39;utilisateur à quel moment il souhaite recevoir un rappel pour terminer sa carte de vœux <ph type="x-smartling-placeholder">
</ph> Programmation d'une notification locale pour recevoir un rappel de finalisation d'une carte de vœux.

Lorsqu'une notification programmée se déclenche dans Fugu Greetings, elle s’affiche comme n’importe quelle autre notification, mais comme je l’ai écrit auparavant, il n’avait pas besoin d’une connexion réseau.

<ph type="x-smartling-placeholder">
</ph> Centre de notifications macOS affichant une notification déclenchée par Fugu Greetings. <ph type="x-smartling-placeholder">
</ph> La notification déclenchée apparaît dans le centre de notifications macOS.

API Wake Lock

Je souhaite également inclure l'API Wake Lock. Parfois, il suffit de regarder l'écran suffisamment longtemps pour trouver l'inspiration t'embrasse. Dans le pire des cas, c’est alors que l’écran s’éteint. L'API Wake Lock peut empêcher cette situation.

La première étape consiste à obtenir un wakelock avec le navigator.wakelock.request method(). Je lui transmets la chaîne 'screen' pour obtenir un wakelock de l'écran. J'ajoute ensuite un écouteur d'événements pour être informé du lancement du wakelock. Cela peut se produire, par exemple, lorsque la visibilité des onglets change. Si cela se produit, lorsque l'onglet redevient visible, activer de nouveau le wakelock.

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

Oui, il s'agit d'une amélioration progressive. Je n'ai donc besoin de la charger que lorsque le navigateur est compatible avec l'API.

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

Dans Fugu Greetings, il existe une case à cocher Insomnia qui, lorsqu'elle est cochée, permet de conserver écran activé.

<ph type="x-smartling-placeholder">
</ph> Si la case &quot;Insomnie&quot; est cochée, l&#39;écran reste allumé. <ph type="x-smartling-placeholder">
</ph> La case à cocher Insomnie maintient l'application active.

API Idle Detection

Parfois, même si vous regardez l’écran pendant des heures, elle est tout simplement inutile et vous ne pouvez pas trouver la moindre idée de ce qu'il faut faire avec votre carte de vœux. L'API Idle Detection permet à l'application de détecter le temps d'inactivité des utilisateurs. Si l'utilisateur reste inactif trop longtemps, l'application est réinitialisée à son état initial. et efface le canevas. Cette API est actuellement contrôlée par l'équipe autorisation de notification, puisque de nombreux cas d'utilisation en production de la détection d'inactivité sont liés aux notifications, (par exemple, pour n'envoyer une notification qu'à un appareil en cours d'utilisation).

Après m'assurer que l'autorisation de notification est accordée, j'instancie ensuite détecteur d'inactivité. J'enregistre un écouteur d'événements qui écoute les modifications apportées à l'inactivité, qui inclut l'utilisateur l'état de l'écran. L'utilisateur peut être actif ou inactif, et l'écran peut être déverrouillé ou verrouillé. Si l'utilisateur est inactif, le canevas est effacé. Je définis un seuil de 60 secondes pour le détecteur d'inactivité.

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

Comme toujours, je ne charge ce code que lorsque le navigateur le prend en charge.

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

Dans l'application Fugu Greetings, le canevas est effacé lorsque la case Ephemeral (Éphémère) est cochée. cochée et l'utilisateur est inactif pendant trop longtemps.

<ph type="x-smartling-placeholder">
</ph> Application Fugu Greetings avec un canevas effacé après une inactivité trop longue de l&#39;utilisateur <ph type="x-smartling-placeholder">
</ph> Lorsque la case Éphémère est cochée et que l'utilisateur est inactif depuis trop longtemps, le canevas est effacé.

Conclusion

Quel bolide ! Un grand nombre d'API dans une seule application exemple. Et n'oubliez pas que je n'oblige jamais l'utilisateur à payer le coût du téléchargement. d'une fonctionnalité non prise en charge par leur navigateur. Grâce à l'amélioration progressive, je m'assure que seul le code approprié est chargé. Étant donné qu'avec le protocole HTTP/2, les requêtes sont économiques, ce modèle devrait fonctionner applications, même si vous pouvez envisager d'utiliser un bundler pour les applications très volumineuses.

<ph type="x-smartling-placeholder">
</ph> Panneau Chrome DevTools Network affichant uniquement les requêtes de fichiers comportant du code compatible avec le navigateur actuel. <ph type="x-smartling-placeholder">
</ph> Onglet Chrome DevTools Network affichant uniquement les demandes de fichiers comportant du code compatible avec le navigateur actuel.

L'application peut s'afficher un peu différemment sur chaque navigateur, car toutes les plateformes ne prennent pas en charge toutes les fonctionnalités, mais la fonctionnalité de base est toujours présente et s'améliore progressivement en fonction des capacités du navigateur. Notez que ces fonctionnalités peuvent changer même dans un seul et même navigateur, selon que l'application s'exécute en tant qu'application installée ou dans un onglet du navigateur.

<ph type="x-smartling-placeholder">
</ph> Fugu Greetings s&#39;exécute dans Chrome pour Android et présente de nombreuses fonctionnalités disponibles. <ph type="x-smartling-placeholder">
</ph> Fugu Greetings exécuté dans Chrome pour Android
<ph type="x-smartling-placeholder">
</ph> Fugu Greetings s&#39;exécute sur Safari pour ordinateur et présente moins de fonctionnalités disponibles. <ph type="x-smartling-placeholder">
</ph> Fugu Greetings exécuté dans Safari pour ordinateur.
<ph type="x-smartling-placeholder">
</ph> Fugu Greetings s&#39;exécute dans Chrome pour ordinateur de bureau et présente de nombreuses fonctionnalités disponibles. <ph type="x-smartling-placeholder">
</ph> Fugu Greetings est exécuté dans Chrome pour ordinateur de bureau.

Si l'application Fugu Greetings vous intéresse, recherchez-le et dupliquez-le sur GitHub.

<ph type="x-smartling-placeholder">
</ph> Dépôt Fugu Greetings sur GitHub. <ph type="x-smartling-placeholder">
</ph> Application Fugu Greetings sur GitHub.

L'équipe Chromium met tout en œuvre pour rendre l'herbe plus verte en ce qui concerne les API Fugu avancées. En appliquant une amélioration progressive au développement de mon application, Je m'assure que tout le monde bénéficie d'une bonne et solide expérience de base, mais que les personnes utilisant des navigateurs qui prennent en charge davantage d'API de plateforme Web bénéficient d'une expérience encore meilleure. J'ai hâte de découvrir ce que vous allez faire avec l'amélioration progressive de vos applications.

Remerciements

Je suis reconnaissant à Christian Liebel et Hemanth HM, tous deux ayant contribué à l'émission "Fugu Greetings". Cet article a été lu par Joe Medley et Kayce Basques. Jake Archibal m'a aidé à comprendre la situation avec une import() dynamique dans un contexte de service worker.