Améliorer progressivement votre progressive web app

Créer pour les navigateurs modernes et améliorer progressivement comme 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, 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 couches de présentation et de fonctionnalités plus nuancées et techniquement rigoureuses au-dessus du contenu. En 2003, l'amélioration progressive consistait à utiliser, à l'époque, des fonctionnalités CSS modernes, du JavaScript discret et même simplement des graphiques vectoriels adaptables. En 2020 et au-delà, l'amélioration progressive consiste à utiliser les fonctionnalités modernes des navigateurs.

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

JavaScript moderne

En parlant de JavaScript, la compatibilité des navigateurs avec les dernières fonctionnalités JavaScript de base ES 2015 est excellente. La nouvelle norme inclut les promesses, les modules, les classes, les littéraux de modèle, les fonctions flèche, let et const, les paramètres par défaut, les générateurs, l'affectation destructurante, le reste et la propagation, Map/Set, WeakMap/WeakSet, et bien plus encore. Tous sont acceptés.

Tableau de compatibilité CanIUse pour les fonctionnalités ES6, indiquant la compatibilité avec tous les principaux navigateurs.
Tableau de compatibilité des navigateurs avec ECMAScript 2015 (ES6). (Source)

Les fonctions asynchrones, une fonctionnalité 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 de manière plus claire, sans avoir à configurer explicitement des chaînes de promesses.

Tableau de compatibilité CanIUse pour les fonctions asynchrones, indiquant la compatibilité avec tous les principaux navigateurs.
Tableau de compatibilité des navigateurs avec les fonctions asynchrones. (Source)

Même les ajouts de langage ES 2020 très récents, comme la chaîne de liaison facultative et la coalescion des valeurs nulles, ont été pris en charge très rapidement. Vous trouverez un exemple de code ci-dessous. En ce qui concerne les fonctionnalités JavaScript de base, l'herbe est plus verte que jamais.

const adventurer = {
  name
: 'Alice',
  cat
: {
    name
: 'Dinah',
 
},
};
console
.log(adventurer.dog?.name);
// Expected output: undefined
console
.log(0 ?? 42);
// Expected output: 0
Image de fond emblématique de Windows XP représentant de l'herbe verte.
Les fonctionnalités de base de JavaScript sont bien vertes. (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 Fugu Greetings (GitHub). Le nom de cette application est un clin d'œil au projet Fugu 🐡, qui vise à donner au Web toutes les fonctionnalités des applications Android/iOS/ordinateur. 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 virtuelles et de les envoyer à vos proches. Il illustre les concepts fondamentaux des PWA. Il est fiable et entièrement compatible avec le mode hors connexion. Vous pouvez donc l'utiliser même si vous n'avez pas de réseau. Elle peut également être installée sur l'écran d'accueil d'un appareil et s'intègre parfaitement au système d'exploitation en tant qu'application autonome.

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

Amélioration progressive

Maintenant que vous savez tout, il est temps de parler de l'amélioration progressive. Le glossaire MDN Web Docs définit ce concept comme suit:

L'amélioration progressive est une philosophie de conception qui fournit une référence de contenu et de fonctionnalités essentielles à autant d'utilisateurs que possible, tout en offrant la meilleure expérience possible uniquement aux utilisateurs des navigateurs les plus modernes pouvant exécuter tout le code requis.

La détection de fonctionnalités est généralement utilisée pour déterminer si les navigateurs peuvent 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 agents utilisateur inconnus. La dégradation élégante 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 MDN

Créer chaque carte de vœus à partir de zéro peut être très fastidieux. Pourquoi ne pas proposer une fonctionnalité permettant aux utilisateurs d'importer une image et de partir de là ? Avec une approche traditionnelle, vous auriez utilisé un élément <input type=file> pour y parvenir. Tout d'abord, vous devez créer l'élément, définir son type sur 'file' et ajouter des types MIME à la propriété accept, puis "cliquer" dessus de manière programmatique et é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();
 
});
};

Lorsqu'une fonctionnalité d'importation est disponible, une fonctionnalité d'exportation devrait également l'être pour que les utilisateurs puissent enregistrer leurs cartes de vœux localement. La méthode traditionnelle d'enregistrement de fichiers consiste à créer un lien d'ancrage avec un attribut download et une URL de blob comme href. Vous devez également "cliquer" dessus de manière programmatique pour déclencher le téléchargement. 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. Mentalement, vous n'avez pas "téléchargé" une carte de vœux, vous l'avez "enregistrée". Plutôt que de vous afficher une boîte de dialogue "Enregistrer" qui vous permet de choisir l'emplacement du fichier, le navigateur a directement téléchargé la carte de vœux sans interaction de l'utilisateur et l'a directement placée dans votre dossier de téléchargements. Ce n'est pas idéal.

Et s'il existait une meilleure façon de procéder ? Que se passerait-il 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 initialement ouvert ? Il existe une solution. L'API File System Access vous permet d'ouvrir et de créer des fichiers et des répertoires, ainsi que de les modifier et de les enregistrer .

Comment détecter les 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. Je vous explique comment procéder ci-dessous.

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, laissez-moi rapidement mettre en avant le modèle d'amélioration progressive. Dans les navigateurs qui ne sont pas compatibles avec l'API File System Access, je charge les anciens scripts. Vous pouvez voir les onglets réseau de Firefox et de Safari ci-dessous.

L&#39;inspecteur Web Safari affichant les anciens fichiers en cours de chargement.
Tabulation "Réseau" de l'outil Web Inspector de Safari.
Outils pour les développeurs Firefox affichant les anciens fichiers en cours de chargement.
Onglet "Network" (Réseau) des outils de développement Firefox.

Toutefois, dans Chrome, un navigateur compatible avec l'API, seuls les nouveaux scripts sont chargés. Cela est possible grâce à la import() dynamique, que tous les navigateurs modernes acceptent. Comme je l'ai dit plus tôt, l'herbe est bien verte ces jours-ci.

Outils pour les développeurs Chrome montrant le chargement des fichiers modernes.
Outils pour les développeurs Chrome : onglet "Réseau".

API File System Access

Maintenant que j'ai abordé ce point, 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 lui transmets une propriété accepts dans laquelle j'indique que je souhaite des fichiers image. Les extensions de fichier et les types MIME sont acceptés. Cela génère un gestionnaire de fichiers, à 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(). Une boîte de dialogue d'enregistrement de fichier s'affiche. Lorsque le fichier était ouvert, cela n'était pas nécessaire, car 'open-file' est l'option par défaut. J'ai défini le paramètre accepts de la même manière que précédemment, mais cette fois, je l'ai limité aux images PNG. Je reçois à nouveau un descripteur de fichier, mais au lieu d'obtenir le fichier, je crée cette fois un flux 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 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 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);
 
}
};

Grâce à 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 effectuer mes modifications et les enregistrer avec une véritable boîte de dialogue d'enregistrement dans laquelle 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 de fichier.
Application Fugu Greetings avec une image importée.
Illustration 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

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

Volet de partage de Safari pour ordinateur sur macOS déclenché à partir du bouton &quot;Partager&quot; d&#39;un article
Partage Web API sur Safari pour ordinateur sous macOS.

Le code permettant de le faire est assez simple. J'appelle navigator.share() et lui transmets un title, text et url facultatifs dans un objet. Mais que faire si je souhaite joindre une image ? Le niveau 1 de l'API Web Share n'est pas encore compatible avec cette fonctionnalité. La bonne nouvelle est que le niveau 2 de partage Web a ajouté des fonctionnalités de partage de fichiers.

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

Je vais vous montrer comment procéder avec l'application de cartes 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, comme bonne pratique, j'utilise la nouvelle méthode navigator.canShare(), qui fait ce que son nom suggère : elle m'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 avant. 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 à la fois sur l'objet navigator, je continue et charge share.mjs via import() dynamique. Dans les navigateurs tels que Safari pour mobile qui ne remplissent qu'une seule 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 tel que Chrome sur Android, la feuille de partage intégrée s'ouvre. Je peux, par exemple, choisir Gmail, et le widget de création d'e-mails s'affiche avec l'image jointe.

Sharesheet au niveau de l&#39;OS affichant les différentes applications avec lesquelles partager l&#39;image.
Choisissez une application avec laquelle partager le fichier.
Widget de rédaction d&#39;e-mails de Gmail avec l&#39;image jointe.
Le fichier est joint à un nouvel e-mail dans l'éditeur de Gmail.

API Contact Picker

Je vais maintenant vous parler des contacts, c'est-à-dire de l'application de gestion des contacts ou du carnet d'adresses d'un appareil. Lorsque vous rédigez une carte de vœux, il n'est pas toujours facile d'écrire correctement le nom de quelqu'un. Par exemple, un ami à moi, Sergey, préfère que son nom soit écrit en cyrillique. J'utilise un clavier QWERTZ allemand et je ne sais pas comment écrire son nom. C'est un problème que l'API Contact Picker peut résoudre. Étant donné que mon ami est enregistré dans l'application Contacts de mon téléphone, je peux accéder à mes contacts depuis 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 des numéros de téléphone, des adresses e-mail, des icônes d'avatar ou des 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 probablement compris le schéma : je ne charge le fichier que lorsque l'API est réellement compatible.

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

Dans Fugu Greeting, lorsque j'appuie sur le bouton Contacts et sélectionne mes deux meilleurs amis, Сергей Михайлович Брин et 劳伦斯·爱德华·"拉里"·佩奇, vous pouvez voir que le sélecteur de contacts ne montre que leurs noms, mais pas leurs adresses e-mail ni d'autres informations telles que leurs numéros de téléphone. Leurs noms sont ensuite dessinés sur ma carte de vœux.

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

API Async Clipboard

Passons maintenant à la copie et au collage. En tant que développeurs logiciels, l'une de nos opérations préférées est le copier-coller. En tant qu'auteur de cartes de vœux, je peux parfois vouloir faire de même. Je peux coller une image dans une carte de vœux sur laquelle je travaille ou copier ma carte de vœux pour pouvoir continuer à la modifier ailleurs. L'API Async Clipboard est compatible avec le texte et les images. Je vais vous expliquer comment j'ai ajouté la fonctionnalité de copier-coller à l'application Fugu Greetings.

Pour copier quelque chose dans le presse-papiers du système, je dois y écrire. La méthode navigator.clipboard.write() utilise un tableau d'éléments de presse-papiers comme paramètre. Chaque élément du presse-papiers est essentiellement un objet avec un blob comme valeur et le type du 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 sous différentes représentations. Chaque élément du presse-papiers comporte un champ types qui 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 que j'ai 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);
 
}
};

Il est presque inutile de le préciser. Je ne fais cela que dans les navigateurs compatibles.

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

Comment cela fonctionne-t-il concrètement ? J'ai ouvert une image dans l'application Aperçu de macOS et je l'ai copiée 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 du presse-papiers.

Application Fugu Greetings affichant l&#39;invite d&#39;autorisation d&#39;accès au presse-papiers.
Invite d'autorisation d'accès au presse-papiers.

Enfin, après avoir accepté l'autorisation, l'image est collée dans l'application. L'inverse fonctionne également. Je vais copier une carte de vœux dans le presse-papiers. Lorsque j'ouvre Aperçu, clique sur Fichier, puis sur Nouveau à partir du presse-papiers, la carte de vœus est collée dans une nouvelle image sans titre.

Application Aperçu macOS avec une image sans titre, juste collée.
Une image collée dans l'application Preview de macOS.

API Badging

L'API Badging est une autre API utile. En tant que PWA installable, Fugu Greetings dispose bien sûr d'une icône d'application que les utilisateurs peuvent placer sur le dock d'applications ou sur l'écran d'accueil. Un moyen amusant et simple de présenter l'API consiste à l'utiliser (à mauvais escient) dans Fugu Greetings comme compteur de traits de crayon. J'ai ajouté un écouteur d'événements qui incrémente le compteur des traits de crayon chaque fois que l'événement pointerdown se produit, puis définit le badge de l'icône mise à 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 est donc la même que d'habitude.

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

Dans cet exemple, j'ai dessiné les nombres de un à sept, en utilisant un trait de crayon par chiffre. Le compteur du badge 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.
Dessin des nombres de 1 à 7 à l'aide de sept traits de stylo.
Icône de badge dans l&#39;application Fugu Greetings affichant le chiffre 7.
Compteur des traits de stylo sous la forme du badge de l'icône de l'application.

API Periodic Background Sync

Vous souhaitez commencer chaque journée avec une nouveauté ? L'application Fugu Greetings propose une fonctionnalité intéressante : chaque matin, elle vous inspire avec une nouvelle image de fond pour commencer votre carte de vœux. Pour ce faire, l'application utilise l'API Periodic Background Sync.

La première étape consiste à register un événement de synchronisation périodique dans l'enregistrement du service worker. Il écoute une balise de synchronisation appelée 'image-of-the-day' et a un intervalle minimal d'un jour, de sorte 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 la balise d'événement est 'image-of-the-day', c'est-à-dire celle qui a été enregistrée 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,
         
});
       
});
     
})()
   
);
 
}
});

Encore une fois, il s'agit d'une amélioration progressive. Le code n'est donc chargé que lorsque l'API 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 d'eux n'est chargé. Notez que dans le service worker, au lieu d'un import() dynamique (qui n'est pas encore compatible avec un contexte de service worker), j'utilise le 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 Greetings, appuyez sur le bouton Fond d'écran pour afficher l'image de la carte de vœus du jour, qui est mise à jour tous les jours via l'API Periodic Background Sync.

Application Fugu Greetings avec une nouvelle image de carte de vœus du jour.
En appuyant sur le bouton Fond d'écran, vous pouvez afficher l'image du jour.

API Notification Triggers

Même si vous avez beaucoup d'inspiration, vous avez parfois besoin d'un petit coup de pouce pour terminer une carte d'accueil commencée. Cette fonctionnalité est activée par l'API Notification Triggers. En tant qu'utilisateur, je peux indiquer l'heure à laquelle je souhaite être invité à terminer ma carte de vœux. À ce moment-là, je recevrai une notification m'indiquant que ma carte de vœux est 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 est 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, il s'agit d'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 à quel moment je souhaite être rappelé pour terminer ma carte de vœux.

Application Fugu Greetings avec une invite demandant à l&#39;utilisateur quand il souhaite être rappelé pour terminer sa carte de vœux.
Planification d'une notification locale pour vous rappeler de terminer une carte de vœux.

Lorsqu'une notification planifiée se déclenche dans Fugu Greetings, elle s'affiche comme n'importe quelle autre notification, mais comme je l'ai indiqué précédemment, elle ne nécessite 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 fixer l'écran suffisamment longtemps jusqu'à ce que l'inspiration vous embrasse. Le pire qui puisse se produire est que l'écran s'éteigne. L'API Wake Lock peut empêcher cela.

La première étape consiste à obtenir un wake lock avec navigator.wakelock.request method(). Je lui transmets la chaîne 'screen' pour obtenir un verrouillage de réveil de l'écran. J'ajoute ensuite un écouteur d'événements pour être informé lorsque le verrouillage de réveil est libéré. Cela peut se produire, par exemple, lorsque la visibilité de l'onglet change. Si cela se produit, je peux, lorsque l'onglet redevient visible, récupérer le verrouillage de réveil.

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 ne dois donc le 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, une case à cocher Insomnia (Insomnie) permet de maintenir l'écran allumé.

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

API Idle Detection

Parfois, même si vous fixez l'écran pendant des heures, cela ne sert à rien et vous ne trouvez pas la moindre idée pour 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 pendant trop longtemps, l'application revient à son état initial et efface le canevas. Cette API est actuellement limité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 n'envoyer une notification qu'à un appareil que l'utilisateur utilise activement).

Après m'être assuré que l'autorisation de notification est accordée, j'instancie le détecteur d'inactivité. J'enregistre un écouteur d'événements qui écoute les modifications d'inactivité, 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 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 à cocher Éphémère est cochée et que l'utilisateur est inactif pendant trop longtemps.

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

Conclusion

Fiou, quel trajet. Tant d'API dans un seul exemple d'application. Et n'oubliez pas que je ne fais jamais payer à l'utilisateur le coût de téléchargement d'une fonctionnalité que son navigateur ne prend pas en charge. En utilisant l'amélioration progressive, je m'assure que seul le code pertinent est chargé. Étant donné que les requêtes sont peu coûteuses avec HTTP/2, ce modèle devrait fonctionner pour de nombreuses applications, bien que vous puissiez envisager d'utiliser un outil de regroupement pour les applications très volumineuses.

Panneau &quot;Réseau&quot; de Chrome DevTools affichant uniquement les requêtes de fichiers avec du code compatible avec le navigateur actuel.
Onglet "Network" (Réseau) des outils pour les développeurs Chrome affichant uniquement les requêtes de fichiers avec du code compatible avec le navigateur actuel.

L'application peut sembler légèrement différente dans chaque navigateur, car toutes les plates-formes ne sont pas compatibles avec toutes les fonctionnalités. Toutefois, les fonctionnalités de base sont toujours présentes et sont progressivement améliorées en fonction des fonctionnalités du navigateur en question. 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 de navigateur.

Fugu Greetings exécuté sur Android Chrome, avec de nombreuses fonctionnalités disponibles
Fugu Greetings exécuté sur Android Chrome.
Fugu Greetings exécuté sur Safari pour ordinateur, avec moins de fonctionnalités disponibles
Fugu Greetings exécuté dans Safari pour ordinateur.
Fugu Greetings exécuté dans Chrome pour ordinateur, avec de nombreuses fonctionnalités disponibles.
Fugu Greetings exécuté sur Chrome pour ordinateur.

Si l'application Fugu Greetings vous intéresse, recherchez-la et créez-en une branche sur GitHub.

Dépôt Fugu Greetings sur GitHub
Application Fugu Greetings sur GitHub.

L'équipe Chromium s'efforce de rendre les API Fugu avancées plus attrayantes. En appliquant l'amélioration progressive au développement de mon application, je m'assure que tout le monde bénéficie d'une expérience de référence solide, mais que les utilisateurs de 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 faire de l'amélioration progressive dans vos applications.

Remerciements

Je remercie Christian Liebel et Hemanth HM, qui ont tous deux contribué à Fugu Greetings. Cet article a été relu par Joe Medley et Kayce Basques. Jake Archibald m'a aidé à comprendre la situation avec import() dynamique dans un contexte de service worker.