Améliorer progressivement votre progressive web app

Conçu pour les navigateurs modernes et s'améliorer progressivement, en 2003

En mars 2003, Nick Finck et Steve Champeon ont stupéfié le monde de la conception Web avec le concept d'amélioration progressive. Cette stratégie de conception Web met d'abord l'accent sur le chargement du contenu principal des pages Web, puis ajoute progressivement des couches de présentation et de fonctionnalités plus nuancées et techniquement rigoureuses en plus du contenu. En 2003, l'amélioration progressive consistait à utiliser, à l'époque, des fonctionnalités CSS modernes, du code JavaScript discret, et même simplement des graphiques vectoriels évolutifs. L'amélioration progressive en 2020 et au-delà concerne l'utilisation des fonctionnalités des navigateurs modernes.

Une conception Web inclusive pour l'avenir avec une amélioration progressive. Diapositive de titre de la présentation originale de Finck et Champeon.
Diapositive: Une conception Web inclusive pour l'avenir avec l'amélioration progressive. (Source)

JavaScript moderne

À ce propos, la compatibilité des navigateurs avec les dernières fonctionnalités JavaScript de base d'ES 2015 est excellente. Cette nouvelle norme inclut des promesses, des modules, des classes, des littéraux de modèle, des fonctions fléchées, let et const, des paramètres par défaut, des générateurs, l'affectation de déstructuration, le repos et la propagation, Map/Set, WeakMap/WeakSet et bien d'autres. Tous sont compatibles.

Tableau des fonctionnalités d'ES6 prises en charge par CanIUse avec les principaux navigateurs.
Table 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ées dans tous les principaux navigateurs. Les mots clés async et await permettent d'écrire un comportement asynchrone basé sur des promesses dans un style plus clair, ce qui évite de devoir configurer explicitement les chaînes de promesse.

Tableau sur les fonctions asynchrones compatibles avec CanIUse, indiquant la compatibilité avec les principaux navigateurs.
Tableau des fonctions asynchrones compatibles avec les navigateurs. (Source)

De plus, même des ajouts très récents de langages ES 2020, tels que le chaînage facultatif et la coalisation nullh, sont rapidement pris en charge. Vous trouverez un exemple de code ci-dessous. En ce qui concerne les principales fonctionnalités JavaScript, l'herbe est incroyablement plus écologique qu'aujourd'hui.

const adventurer = {
  name: 'Alice',
  cat: {
    name: 'Dinah',
  },
};
console.log(adventurer.dog?.name);
// Expected output: undefined
console.log(0 ?? 42);
// Expected output: 0
L'emblématique image de fond d'herbe verte de Windows XP.
L'herbe est verte pour les fonctionnalités JavaScript essentielles. (Capture d'écran du produit Microsoft, utilisée avec l'autorisation).

Application exemple: Fugu Greetings

Pour cet article, je travaille avec une PWA simple, appelée Fugu Greetings (GitHub). Le nom de cette application est une référence au projet Fugu 🐡, dont l'objectif est d'offrir au Web toute la puissance des applications Android, iOS et de bureau. Pour en savoir plus sur le projet, consultez sa page de destination.

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

Fugu salue la PWA avec un dessin ressemblant au logo de la communauté des PWA.
Application exemple Fugu Greetings

amélioration progressive

Maintenant que nous avons terminé, il est temps de parler de l'amélioration progressive. Le glossaire de la documentation Web MDN définit le concept comme suit:

L'amélioration progressive est une philosophie de conception qui fournit une base de base de contenu et de fonctionnalités essentiels au plus grand nombre d'utilisateurs possible, tout en offrant la meilleure expérience possible uniquement aux utilisateurs des navigateurs les plus récents, capables d'exécuter tout le code requis.

La détection de fonctionnalités est généralement utilisée pour déterminer si les navigateurs sont capables de gérer des fonctionnalités plus modernes, tandis que les polyfills sont souvent utilisés pour ajouter des fonctionnalités manquantes avec JavaScript.

[…]

L'amélioration progressive est une technique utile qui permet aux développeurs Web de se concentrer sur le développement des meilleurs sites Web possibles tout en les faisant fonctionner sur plusieurs user-agents inconnus. La 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 valides et peuvent souvent se compléter.

Contributeurs RMD

Commencer chaque carte de vœux à partir de zéro peut être très fastidieux. Alors pourquoi ne pas avoir une fonctionnalité qui permet aux utilisateurs d'importer une image et de commencer à partir de là ? Avec une approche traditionnelle, vous auriez utilisé un élément <input type=file> pour effectuer cette opération. Tout d'abord, vous devez créer l'élément, définir sa type sur 'file' et ajouter des types MIME à la propriété accept, puis "cliquer" de manière automatisée dessus pour écouter les modifications. Lorsque vous sélectionnez une image, elle est importée directement sur 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();
  });
};

En cas de fonctionnalité d'import, il devrait probablement y avoir une fonctionnalité d'import 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 attribut download et une URL d'objet blob comme href. Vous devez également "cliquer" de manière automatisée pour déclencher le téléchargement et, pour éviter les fuites de mémoire, n'oubliez pas de 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();
};

Mais attendez une minute. Mentuellement, vous n'avez pas "téléchargé" une carte de vœux, mais vous l'avez "enregistrée". Plutôt que d'afficher une boîte de dialogue d'enregistrement qui vous permet de choisir où placer le fichier, le navigateur a directement téléchargé la carte de vœux sans intervention de l'utilisateur et l'a placée directement dans votre dossier de 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 dans le fichier d'origine que vous aviez 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 répertoires, ainsi que de les modifier et de les enregistrer .

Comment détecter des fonctionnalités d'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 d'entrer dans les détails de l'API File System Access, permettez-moi de souligner rapidement le modèle d'amélioration progressive ici. Dans les navigateurs qui ne sont actuellement pas compatibles avec l'API File System Access, je charge les anciens scripts. Vous pouvez voir les onglets réseau de Firefox et Safari ci-dessous.

Safari Web Inspector montrant le chargement des anciens fichiers
Onglet "Réseau" de Safari Web Inspector
Outils pour les développeurs Firefox affichant les anciens fichiers en cours de chargement.
Onglet "Réseau" des outils pour les développeurs Firefox

Toutefois, dans Chrome, un navigateur compatible avec l'API, seuls les nouveaux scripts sont chargés. Tout cela est possible avec élégance grâce à la technologie dynamique import(), compatible avec tous les navigateurs récents. Comme je le disais tout à l'heure, l'herbe est plutôt verte de nos jours.

Outils pour les développeurs Chrome montrant les fichiers modernes en cours de chargement.
Onglet "Réseau" des outils pour les développeurs Chrome

API File System Access

Maintenant que j'ai résolu ce problème, il est temps d'examiner l'implémentation réelle basée sur l'API File System Access. Pour importer une image, j'appelle window.chooseFileSystemEntries() et je lui transmets une propriété accepts où je veux des fichiers image. Les extensions de fichier et les types MIME sont acceptés. Cela se traduit par 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(). À partir de là, j'obtiens une boîte de dialogue d'enregistrement de fichier. Lorsque le fichier est ouvert, cela n'était pas nécessaire, car 'open-file' est la valeur par défaut. J'ai défini le paramètre accepts de la même manière que précédemment, mais cette fois-ci limité aux images PNG. Là encore, je récupère un handle de fichier, mais au lieu d'obtenir le fichier, je crée cette fois un flux accessible en écriture en appelant createWritable(). Ensuite, j'écris le blob, qui est mon image de 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, une erreur d'écriture ou de lecture peut se produire, ou l'utilisateur peut simplement annuler la boîte de dialogue de création de fichier. C'est pourquoi j'encaps 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);
  }
};

Grâce à l'amélioration progressive avec l'API File System Access, je peux ouvrir un fichier comme auparavant. Le fichier importé est dessiné directement sur le canevas. Je peux faire mes modifications et enfin 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é.

Application Fugu Greetings avec une boîte de dialogue d&#39;ouverture de fichier.
Boîte de dialogue d'ouverture d'un fichier
L&#39;application Fugu Greetings dispose désormais d&#39;une image importée.
Image importée.
Application Fugu Greetings avec l&#39;image modifiée.
Enregistrement de l'image modifiée dans un nouveau fichier.

API Web Share et Web Share Target

Mis à part le stockage pour l'éternité, je veux peut-être partager ma carte de vœux. C'est quelque chose que je peux réaliser avec l'API Web Share et l'API Web Share Target. Les systèmes d'exploitation mobiles et, plus récemment, pour ordinateur de bureau ont été dotés de mécanismes de partage intégrés. Par exemple, ci-dessous, la feuille de partage de Safari pour ordinateur de bureau sur macOS est déclenchée à partir d'un article sur mon blog. Lorsque vous cliquez sur le bouton Share Article (Partager l'article), vous pouvez partager un lien vers l'article avec un ami, par exemple via l'application macOS Messages.

Feuille de partage de Safari pour ordinateur sous macOS déclenchée par le bouton &quot;Partager&quot; d&#39;un article
API Web Share dans Safari pour ordinateur sous macOS.

Le code à cet effet est assez simple. J'appelle navigator.share() et je lui transmets des éléments title, text et url facultatifs dans un objet. Mais que se passe-t-il si je souhaite joindre une image ? Le niveau 1 de l'API Web Share n'est pas encore compatible. La bonne nouvelle, c'est que les fonctionnalités de partage de fichiers ont été ajoutées.

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 objet blob, puis d'un objet title et d'un élément text. Ensuite, j'utilise la nouvelle méthode navigator.canShare(), dont le nom l'indique, ce qui indique si l'objet data que j'essaie de partager peut techniquement être partagé par le navigateur. Si navigator.canShare() m'indique que les données peuvent être partagées, je suis prêt à appeler navigator.share() comme auparavant. 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 sur l'objet navigator, ce n'est qu'alors que je continue et que je charge share.mjs via une import() dynamique. Dans les navigateurs tels que Safari pour mobile qui ne remplissent qu'une de ces 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 Share (Partager) dans un navigateur compatible tel que Chrome sur Android, la feuille de partage intégrée s'ouvre. Je peux, par exemple, choisir Gmail. Le widget de rédaction des e-mails s'affiche avec l'image jointe.

Feuille de partage au niveau du système d&#39;exploitation affichant les différentes applications avec lesquelles partager l&#39;image.
Choisissez l'application avec laquelle partager le fichier.
Widget de rédaction des e-mails de Gmail avec l&#39;image en pièce jointe
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 l'application de gestion de contacts. Lorsque vous écrivez une carte de vœux, il n'est pas toujours facile d'écrire correctement le nom d'une personne. Prenons l'exemple d'un ami Sergey qui préfère que son nom s'écrit en caractères cyrilliques. J'utilise un clavier allemand QWERTZ et je ne sais pas comment taper son nom. L'API Contact Picker permet de résoudre ce problème. Comme mon ami est stocké dans l'application de contacts de mon téléphone, je peux accéder à mes contacts sur le Web via l'API Contacts Picker.

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 peux être intéressé par les numéros de téléphone, les adresses e-mail, les icônes d'avatar ou les adresses physiques. Ensuite, je configure un objet options et je définis multiple sur true, afin de pouvoir sélectionner 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);
  }
};

Vous avez sans doute déjà appris ce modèle : je ne charge le fichier que lorsque l'API est prise en charge.

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

Dans Fugu Salutation, lorsque j'appuie sur le bouton Contacts et que je sélectionne mes deux meilleurs amis, للاى ذذي يُرجى الذي تقديم في مؤسستك الإخباري. Leurs noms sont ensuite dessinés sur ma carte de vœux.

Sélecteur de contacts affichant les noms de deux contacts du carnet d&#39;adresses.
Sélectionnez deux noms à l'aide de l'outil de sélection de contacts dans le carnet d'adresses.
Noms des deux contacts précédemment sélectionnés, dessinés sur la carte de vœux.
Les deux noms sont alors dessinés sur la carte de vœux.

API Asynchrone Clipboard

Ensuite, copier et coller. L'une de nos opérations préférées en tant que développeurs de logiciels est le copier-coller. En tant qu'auteur de cartes de vœux, je peux parfois vouloir faire de même. Je peux soit coller une image dans une carte de vœux sur laquelle je travaille, ou copier ma carte de vœux afin de pouvoir continuer à la modifier depuis un autre endroit. L'API Async Clipboard prend en charge le texte et les images. Voyons comment j'ai ajouté la prise en charge du copier-coller à l'application Fugu Greetings.

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 comme paramètre. Chaque élément du presse-papiers est essentiellement un objet avec un blob comme valeur et le type de ce 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(). En effet, plusieurs éléments du presse-papiers peuvent se trouver dans le presse-papiers dans des représentations différentes. Chaque élément du presse-papiers comporte un champ types qui m'indique les types MIME des ressources disponibles. J'appelle la méthode getType() de l'élément du presse-papiers en transmettant le type MIME précédemment obtenu.

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 pour l'instant. Je ne fais cela que sur les navigateurs compatibles.

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 macOS Preview et la copie dans le presse-papiers. Lorsque je clique sur Coller, l'application Fugu Greetings me demande si je souhaite autoriser l'application à voir le texte et les images dans le presse-papiers.

Application Fugu Greetings affichant l&#39;invite d&#39;autorisation du presse-papiers.
Invite d'autorisation du presse-papiers.

Enfin, une fois l'autorisation acceptée, 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 ensuite l'aperçu et que je clique sur File (Fichier), puis sur New from Clipboard (Nouveau dans le presse-papiers), la carte de vœux est collée dans une nouvelle image sans titre.

l&#39;application macOS Preview avec une image sans titre, qui vient d&#39;être collée.
Une image collée dans l'application macOS Preview.

API Badging

L'API Badging est une autre API utile. Fugu Greetings est une PWA installable disposant bien d'une icône d'application que les utilisateurs peuvent placer sur la barre d'applications ou sur l'écran d'accueil. Un moyen simple et amusant de démontrer l'API consiste à l'utiliser (ab) dans Fugu Greetings en tant que compteur de traits de stylo. J'ai ajouté un écouteur d'événements qui incrémente le compteur de traits de stylo chaque fois que l'événement pointerdown se produit, puis définit le badge d'icône mis à jour. À chaque fois que le canevas est effacé, 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);
});

Cette fonctionnalité est une amélioration progressive. La logique de chargement reste donc la même.

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

Dans cet exemple, j'ai dessiné les nombres de un à sept, en utilisant un trait de stylo par numéro. Le compteur de badges sur l'icône est maintenant à sept.

Les chiffres de un à sept dessinés sur la carte de vœux, chacun avec un seul trait de stylo.
Dessinez les chiffres de 1 à 7 en utilisant sept traits de stylo.
Icône de badge sur l&#39;application Fugu Greetings indiquant le chiffre 7.
Compteur de mouvements du stylo sous la forme du badge de l'icône d'application.

API périodiques Background Sync

Vous voulez repartir de zéro chaque jour ? L'application Fugu Greetings présente une fonctionnalité intéressante : elle peut vous inspirer chaque matin avec une nouvelle image de fond pour commencer votre carte de vœux. Pour ce faire, l'application utilise l'APIPeriodic Background Sync.

La première étape consiste à register un événement de synchronisation périodique dans l'enregistrement du service worker. Elle écoute une balise de synchronisation appelée 'image-of-the-day' et dispose d'un intervalle minimal d'une journée. L'utilisateur peut donc 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é précédemment, l'image du jour est récupérée via la fonction getImageOfTheDay(), et le résultat est propagé à tous les clients afin qu'ils puissent mettre à jour leurs canevas et leurs 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,
          });
        });
      })()
    );
  }
});

Là encore, il s'agit d'une amélioration véritablement progressive. Le code n'est donc chargé que lorsque l'API est prise en charge par le navigateur. Cela s'applique à la fois au code client et au code du service worker. Sur les navigateurs non compatibles, aucun d'eux n'est chargé. Notez que, dans le service worker, au lieu d'un élément import() dynamique (qui n'est pas encore pris en charge dans un contexte de service worker), j'utilise la méthode importScripts() classique.

// 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 Salutations, appuyer sur le bouton Fond d'écran affiche l'image de la carte de vœux du jour, qui est mise à jour tous les jours via l'API Periodic Background Sync.

Application Fugu Salutations avec une nouvelle image de carte du jour.
Appuyez sur le bouton Fond d'écran pour afficher l'image de la journée.

API Notification Triggers

Parfois, même avec beaucoup d'inspiration, vous avez besoin d'un coup de pouce pour terminer une carte de vœux commencée. Cette fonctionnalité est activée par l'API Notification Triggers. En tant qu'utilisateur, je peux saisir une heure à laquelle je veux être rappelé pour terminer ma carte de vœux. Le moment venu, je recevrai une notification indiquant que ma carte de vœux attend.

Après avoir demandé l'heure cible, l'application planifie la notification avec un showTrigger. Il peut s'agir d'une TimestampTrigger avec la date cible précédemment sélectionnée. La notification de rappel est déclenchée localement. Vous n'avez pas besoin d'accéder au réseau ni au serveur.

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, il s'agit d'une amélioration progressive, de sorte que le code n'est chargé que de manière conditionnelle.

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

Lorsque je coche la case Reminder (Rappel) dans Fugu Greetings, une invite me demande quand je souhaite recevoir un rappel pour terminer ma carte de vœux.

Application Fugu Salutations avec une invite demandant à l&#39;utilisateur quand il souhaite recevoir un rappel pour terminer sa carte de vœux.
Planification d'une notification locale vous rappelant de terminer 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 déjà écrit, elle ne nécessitait pas de connexion réseau.

Centre de notifications macOS affichant une notification déclenchée par Fugu Greetings.
La notification déclenchée s'affiche dans le centre de notifications de macOS.

API Wake Lock

Je souhaite également inclure l'API Wake Lock. Parfois, il suffit de regarder suffisamment longtemps l'écran jusqu'à ce que l'inspiration vous embrasse. Dans ce cas, le pire des scénarios est que l'écran s'éteigne. L'API Wake Lock peut empêcher cela.

La première étape consiste à obtenir un wakelock avec 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é de la libération du wakelock. Cela peut se produire, par exemple, lorsque la visibilité des onglets change. Dans ce cas, je pourrai récupérer le wakelock lorsque l'onglet redevient visible.

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 prend en charge l'API.

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

Dans Fugu Greetings, une case à cocher Insomnia (Insomnie) permet de maintenir l'écran activé.

Si vous cochez la case &quot;Insomnie&quot;, l&#39;écran reste allumé.
La case à cocher Insomnie maintient l'application active.

API Idle Detection

Parfois, même si vous fixez l'écran pendant des heures, c'est tout simplement inutile et vous ne pouvez pas avoir 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é de l'utilisateur. Si l'utilisateur est inactif trop longtemps, l'application revient à son état initial et efface le canevas. Cette API est actuellement contrôlée par l'autorisation de notification, car de nombreux cas d'utilisation en production de la détection d'inactivité sont liés aux notifications, par exemple pour envoyer une notification uniquement à un appareil que l'utilisateur utilise activement.

Après avoir vérifié que l'autorisation de notifications est accordée, j'instancie le détecteur d'inactivité. J'enregistre un écouteur d'événements qui écoute les modifications inactives, y compris l'utilisateur et 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 s'efface. J'attribue au détecteur inactif un seuil de 60 secondes.

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 disparaît lorsque la case Ephemeral (Éphémère) est cochée et que l'utilisateur reste inactif trop longtemps.

Application Fugu Greetings avec un canevas effacé après une inactivité trop longue de l&#39;utilisateur
Lorsque la case Éphémère est cochée et que l'utilisateur est inactif trop longtemps, le canevas est effacé.

Conclusion

Quelle aventure ! Il existe tellement d'API dans une seule application exemple. N'oubliez pas que je ne demande jamais à l'utilisateur de payer le coût du téléchargement d'une fonctionnalité non compatible avec son navigateur. Grâce à l'amélioration progressive, je m'assure que seul le code pertinent est chargé. Et comme avec HTTP/2, les requêtes sont peu coûteuses, ce modèle devrait bien fonctionner pour de nombreuses applications, même si vous pouvez envisager d'utiliser un bundler pour les applications très volumineuses.

Panneau Chrome DevTools Network affichant uniquement les requêtes portant sur des fichiers dont le code est compatible avec le navigateur actuel.
Onglet "Chrome DevTools Network" (Réseau des outils pour les développeurs Chrome) qui n'affiche que les requêtes portant sur des fichiers dont le code est compatible avec le navigateur actuel.

L'application peut être légèrement différente d'un navigateur à l'autre, car toutes les plates-formes ne sont pas compatibles avec toutes les fonctionnalités, mais la fonctionnalité de base est toujours présente, progressivement améliorée en fonction des capacités du navigateur. Notez que ces fonctionnalités peuvent changer même dans un même navigateur, selon que l'application s'exécute en tant qu'application installée ou dans un onglet du navigateur.

Fugu Greetings s&#39;exécutant sur Chrome pour Android, présentant de nombreuses fonctionnalités disponibles.
Fugu Greetings s'exécute sur Chrome pour Android.
Fugu Greetings s&#39;exécute sur la version pour ordinateur de Safari, avec moins de fonctionnalités disponibles.
Fugu Greetings s'exécute sur le navigateur Safari pour ordinateur.
Fugu Salutations s&#39;exécutant sur Chrome pour ordinateur, affichant de nombreuses fonctionnalités disponibles.
Fugu Greetings s'exécute sur le navigateur Chrome pour ordinateur.

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

Dépôt Fugu Greetings sur GitHub.
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 améliorant progressivement le développement de mon application, je m'assure que tout le monde bénéficie d'une bonne expérience de base solide, mais que les personnes utilisant des navigateurs compatibles avec davantage d'API de plates-formes Web bénéficient d'une expérience encore meilleure. J'ai hâte de voir ce que vous allez accomplir avec l'amélioration progressive de vos applications.

Remerciements

Nous remercions Christian Liebel et Hemanth HM, qui ont tous deux contribué à Fugu Salutations. Cet article a été lu par Joe Medley et Kayce Basques. Jake Archibal m'a aidé à découvrir la situation des import() dynamiques dans un contexte de service worker.