Données hors connexion

Pour créer une expérience hors connexion solide, votre PWA a besoin de gestion de l'espace de stockage. Dans le chapitre sur la mise en cache, vous avez appris que le stockage en cache est une option permettant d'enregistrer des données sur un appareil. Dans ce chapitre, nous allons vous expliquer comment gérer les données hors connexion, y compris la persistance des données, les limites et les outils disponibles.

Le stockage ne se limite pas aux fichiers et aux composants, mais peut inclure d'autres types de données. Sur tous les navigateurs compatibles avec les PWA, les API suivantes sont disponibles pour le stockage sur l'appareil:

  • IndexedDB: option de stockage d'objets NoSQL pour les données structurées et les blobs (données binaires).
  • WebStorage: permet de stocker des paires de chaînes clé-valeur à l'aide du stockage local ou de session. Il n'est pas disponible dans un contexte de service worker. Cette API étant synchrone, elle n'est pas recommandée pour le stockage de données complexes.
  • Stockage dans le cache: comme indiqué dans le module sur le stockage dans le cache.

Vous pouvez gérer tout l'espace de stockage de l'appareil avec l'API Storage Manager sur les plates-formes compatibles. L'API Cache Storage et IndexedDB offrent un accès asynchrone au stockage persistant pour les PWA. Elles sont accessibles depuis le thread principal, les Web Workers et les service workers. Les deux jouent un rôle essentiel pour que les PWA fonctionnent de manière fiable lorsque le réseau est instable ou inexistant. Mais dans quel cas devez-vous utiliser chacune d'elles ?

Utilisez l'API Cache Storage pour les ressources réseau, c'est-à-dire les éléments auxquels vous accédez en les demandant via une URL, comme le code HTML, CSS, JavaScript, les images, les vidéos et l'audio.

Utilisez IndexedDB pour stocker des données structurées. Cela inclut les données qui doivent être indexables ou combinables de manière NoSQL, ou d'autres données telles que les données spécifiques à l'utilisateur qui ne correspondent pas nécessairement à une requête d'URL. Notez qu'IndexedDB n'est pas conçu pour la recherche de texte intégral.

IndexedDB

Pour utiliser IndexedDB, ouvrez d'abord une base de données. Une base de données est créée si aucune n'existe. IndexedDB est une API asynchrone, mais elle accepte un rappel au lieu de renvoyer une promesse. L'exemple suivant utilise la bibliothèque idb de Jake Archibald, qui est un petit wrapper Promise pour IndexedDB. Les bibliothèques d'assistance ne sont pas obligatoires pour utiliser IndexedDB, mais si vous souhaitez utiliser la syntaxe Promise, la bibliothèque idb est une option.

L'exemple suivant crée une base de données pour stocker des recettes de cuisine.

Créer et ouvrir une base de données

Pour ouvrir une base de données:

  1. Utilisez la fonction openDB pour créer une base de données IndexedDB appelée cookbook. Étant donné que les bases de données IndexedDB sont gérées par version, vous devez augmenter le numéro de version chaque fois que vous modifiez la structure de la base de données. Le deuxième paramètre correspond à la version de la base de données. Dans l'exemple, il est défini sur 1.
  2. Un objet d'initialisation contenant un rappel upgrade() est transmis à openDB(). La fonction de rappel est appelée lorsque la base de données est installée pour la première fois ou lorsqu'elle passe à une nouvelle version. Cette fonction est le seul endroit où des actions peuvent se produire. Les actions peuvent inclure la création de datastores d'objets (structures utilisées par IndexedDB pour organiser les données) ou d'index (sur lesquels vous souhaitez effectuer des recherches). C'est également là que la migration des données doit avoir lieu. En règle générale, la fonction upgrade() contient une instruction switch sans instructions break pour permettre à chaque étape de se produire dans l'ordre, en fonction de l'ancienne version de la base de données.
import { openDB } from 'idb';

async function createDB() {
  // Using https://github.com/jakearchibald/idb
  const db = await openDB('cookbook', 1, {
    upgrade(db, oldVersion, newVersion, transaction) {
      // Switch over the oldVersion, *without breaks*, to allow the database to be incrementally upgraded.
    switch(oldVersion) {
     case 0:
       // Placeholder to execute when database is created (oldVersion is 0)
     case 1:
       // Create a store of objects
       const store = db.createObjectStore('recipes', {
         // The `id` property of the object will be the key, and be incremented automatically
           autoIncrement: true,
           keyPath: 'id'
       });
       // Create an index called `name` based on the `type` property of objects in the store
       store.createIndex('type', 'type');
     }
   }
  });
}

L'exemple crée un data store d'objets dans la base de données cookbook appelé recipes, avec la propriété id définie comme clé d'index du data store, et crée un autre index appelé type, basé sur la propriété type.

Examinons le store d'objets qui vient d'être créé. Après avoir ajouté des recettes au magasin d'objets et ouvert DevTools dans les navigateurs Chromium ou l'outil d'inspection Web dans Safari, vous devriez voir ce qui suit:

Safari et Chrome affichant le contenu IndexedDB

Ajouter des données

IndexedDB utilise des transactions. Les transactions regroupent les actions afin qu'elles se produisent en tant qu'unité. Ils permettent de s'assurer que la base de données est toujours dans un état cohérent. Ils sont également essentiels si plusieurs copies de votre application sont en cours d'exécution pour éviter l'écriture simultanée sur les mêmes données. Pour ajouter des données:

  1. Démarrez une transaction avec mode défini sur readwrite.
  2. Obtenez le data store dans lequel vous allez ajouter des données.
  3. Appelez add() avec les données que vous enregistrez. La méthode reçoit les données sous forme de dictionnaire (sous la forme de paires clé/valeur) et les ajoute au magasin d'objets. Le dictionnaire doit pouvoir être cloné à l'aide du clonage structuré. Si vous souhaitez mettre à jour un objet existant, appelez plutôt la méthode put().

Les transactions disposent d'une promesse done qui se résout lorsque la transaction aboutit, ou qui est refusée avec une erreur de transaction.

Comme l'explique la documentation de la bibliothèque IDB, si vous écrivez dans la base de données, tx.done est le signal indiquant que tout a été correctement validé dans la base de données. Toutefois, il est utile d'attendre les opérations individuelles afin de pouvoir identifier les erreurs qui entraînent l'échec de la transaction.

// Using https://github.com/jakearchibald/idb
async function addData() {
  const cookies = {
      name: "Chocolate chips cookies",
      type: "dessert",
        cook_time_minutes: 25
  };
  const tx = await db.transaction('recipes', 'readwrite');
  const store = tx.objectStore('recipes');
  store.add(cookies);
  await tx.done;
}

Une fois les cookies ajoutés, la recette apparaîtra dans la base de données avec les autres recettes. L'ID est automatiquement défini et incrémenté par indexedDB. Si vous exécutez ce code deux fois, vous obtiendrez deux entrées de cookies identiques.

Récupération des données…

Voici comment obtenir des données à partir d'IndexedDB:

  1. Lancez une transaction et spécifiez le ou les magasins d'objets, et éventuellement le type de transaction.
  2. Appelez objectStore() à partir de cette transaction. Veillez à spécifier le nom du magasin d'objets.
  3. Appelez get() avec la clé que vous souhaitez obtenir. Par défaut, le magasin utilise sa clé comme indice.
// Using https://github.com/jakearchibald/idb
async function getData() {
  const tx = await db.transaction('recipes', 'readonly')
  const store = tx.objectStore('recipes');
// Because in our case the `id` is the key, we would
// have to know in advance the value of the id to
// retrieve the record
  const value = await store.get([id]);
}

Gestionnaire de stockage

Savoir gérer l'espace de stockage de votre PWA est particulièrement important pour stocker et diffuser correctement les réponses réseau.

La capacité de stockage est partagée entre toutes les options de stockage, y compris Cache Storage, IndexedDB, Web Storage, et même le fichier du service worker et ses dépendances. Toutefois, l'espace de stockage disponible varie d'un navigateur à l'autre. Vous ne risquez pas de manquer d'espace : les sites peuvent stocker des mégaoctets, voire des gigaoctets de données sur certains navigateurs. Chrome, par exemple, permet au navigateur d'utiliser jusqu'à 80% de l'espace disque total, et une origine individuelle peut utiliser jusqu'à 60% de l'espace disque total. Pour les navigateurs compatibles avec l'API Storage, vous pouvez connaître la quantité d'espace de stockage encore disponible pour votre application, son quota et son utilisation. L'exemple suivant utilise l'API Storage pour estimer le quota et l'utilisation, puis calcule le pourcentage utilisé et les octets restants. Notez que navigator.storage renvoie une instance de StorageManager. Il existe une interface Storage distincte, et il est facile de les confondre.

if (navigator.storage && navigator.storage.estimate) {
  const quota = await navigator.storage.estimate();
  // quota.usage -> Number of bytes used.
  // quota.quota -> Maximum number of bytes available.
  const percentageUsed = (quota.usage / quota.quota) * 100;
  console.log(`You've used ${percentageUsed}% of the available storage.`);
  const remaining = quota.quota - quota.usage;
  console.log(`You can write up to ${remaining} more bytes.`);
}

Dans les outils de développement Chromium, vous pouvez consulter le quota de votre site et la quantité d'espace de stockage utilisée, ventilée par élément qui l'utilise, en ouvrant la section Storage (Stockage) dans l'onglet Application (Application).

Outils pour les développeurs Chrome, section "Application", "Effacer le stockage"

Firefox et Safari ne proposent pas d'écran récapitulatif pour afficher tous les quotas et l'utilisation de l'espace de stockage pour l'origine actuelle.

Persistance des données

Vous pouvez demander au navigateur un espace de stockage persistant sur les plates-formes compatibles pour éviter l'éviction automatique des données après une période d'inactivité ou en cas de pression sur l'espace de stockage. Si l'autorisation est accordée, le navigateur ne supprimera jamais les données du stockage. Cette protection inclut l'enregistrement du service worker, les bases de données IndexedDB et les fichiers dans le stockage en cache. Notez que les utilisateurs sont toujours en charge et peuvent supprimer le stockage à tout moment, même si le navigateur a accordé un stockage persistant.

Pour demander un espace de stockage persistant, appelez StorageManager.persist(). Comme précédemment, l'interface StorageManager est accessible via la propriété navigator.storage.

async function persistData() {
  if (navigator.storage && navigator.storage.persist) {
    const result = await navigator.storage.persist();
    console.log(`Data persisted: ${result}`);
}

Vous pouvez également vérifier si l'espace de stockage persistant est déjà accordé dans l'origine actuelle en appelant StorageManager.persisted(). Firefox demande à l'utilisateur l'autorisation d'utiliser le stockage persistant. Les navigateurs Chromium autorisent ou refusent la persistance en fonction d'une heuristique qui détermine l'importance du contenu pour l'utilisateur. Par exemple, l'installation de PWA est l'un des critères pour Google Chrome. Si l'utilisateur a installé une icône pour la PWA dans le système d'exploitation, le navigateur peut accorder un stockage persistant.

Mozilla Firefox demande à l'utilisateur l'autorisation de persistance de stockage.

Compatibilité avec les navigateurs pour les API

Stockage Web

Browser Support

  • Chrome: 4.
  • Edge: 12.
  • Firefox: 3.5.
  • Safari: 4.

Source

Accès au système de fichiers

Browser Support

  • Chrome: 86.
  • Edge: 86.
  • Firefox: 111.
  • Safari: 15.2.

Source

Gestionnaire d'espace de stockage

Browser Support

  • Chrome: 55.
  • Edge: 79.
  • Firefox: 57.
  • Safari: 15.2.

Source

Ressources