Données hors connexion

Pour bénéficier d'une expérience hors connexion fiable, votre PWA doit gérer l'espace de stockage. Dans le chapitre consacré à la mise en cache, vous avez appris que le stockage en cache est l'une des options 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 et les limites, ainsi que les outils disponibles.

Stockage

Le stockage ne se limite pas aux fichiers et aux éléments : il peut inclure d'autres types de données. Dans 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 d'un stockage local ou de session. Elle n'est pas disponible dans le contexte d'un service worker. Cette API étant synchrone, elle n'est pas recommandée pour le stockage de données complexes.
  • Stockage du cache: comme indiqué dans le module de mise en cache.

Vous pouvez gérer l'espace de stockage de tout l'appareil avec l'API Storage Manager sur les plates-formes compatibles. L'API Cache Storage et IndexedDB fournissent un accès asynchrone au stockage persistant pour les PWA. Ils sont accessibles depuis le thread principal, les nœuds de calcul Web et les service workers. Ces deux éléments jouent un rôle essentiel dans le fonctionnement fiable des PWA lorsque le réseau est irrégulier ou inexistant. Mais quand les utiliser ?

Utilisez l'API Cache Storage pour les ressources réseau. Il s'agit des éléments auxquels vous pouvez accéder en faisant une demande via une URL (HTML, CSS, JavaScript, images, vidéos ou fichiers audio, par exemple).

Utilisez IndexedDB pour stocker des données structurées. Cela inclut les données qui doivent être incluses dans l'index de recherche ou qui peuvent être combinées comme un 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 que IndexedDB n'est pas conçu pour la recherche en texte intégral.

IndexedDB

Pour utiliser IndexedDB, commencez par ouvrir une base de données. S'il n'en existe aucune, une nouvelle base de données est créée. IndexedDB est une API asynchrone, mais elle utilise un rappel au lieu de renvoyer une promesse. L'exemple suivant utilise la bibliothèque idb de Jake Archibal, qui est un petit wrapper Promise pour IndexedDB. Les bibliothèques d'aide ne sont pas obligatoires pour utiliser IndexedDB, mais si vous souhaitez utiliser la syntaxe Promise, vous pouvez utiliser la bibliothèque idb.

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 cet 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 est mise à niveau vers une nouvelle version. Cette fonction est le seul endroit où des actions peuvent se produire. Les actions peuvent inclure la création de magasins d'objets (structures utilisées par IndexedDB pour organiser les données) ou d'index (que vous souhaitez rechercher). 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 dérouler 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');
     }
   }
  });
}

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

Examinons le magasin d'objets qui vient d'être créé. Après avoir ajouté des recettes au magasin d'objets et ouvert les outils de développement dans les navigateurs basés sur Chromium ou l'inspecteur Web sur Safari, voici ce à quoi vous devez vous attendre:

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 comme une seule 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 une écriture simultanée sur les mêmes données. Pour ajouter des données:

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

Les transactions ont une promesse done qui se résout une fois la transaction terminée ou sont refusées 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 indique que tout a bien été validé dans la base de données. Toutefois, il est utile d'attendre les opérations individuelles afin de voir toutes 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 se trouve dans la base de données avec les autres recettes. L'ID est automatiquement défini et incrémenté par la base de données indexée. Si vous exécutez ce code deux fois, vous aurez deux entrées de cookie identiques.

Récupérer des données

Voici comment récupérer des données dans IndexedDB:

  1. Démarrez 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 index.
// 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]);
}

Le gestionnaire d'espace de stockage

Savoir gérer le 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 le stockage du cache, IndexedDB et Web Storage, et même le fichier du service worker et ses dépendances. Cependant, l'espace de stockage disponible varie d'un navigateur à l'autre. Vous ne risquez pas d'en manquer, car certains 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, tandis qu'une origine individuelle peut occuper jusqu'à 60% de l'espace disque total. Pour les navigateurs compatibles avec l'API Storage, vous pouvez connaître l'espace de stockage restant disponible pour votre application, son quota et son utilisation. L'exemple suivant utilise l'API Storage pour obtenir une estimation du quota et de l'utilisation, puis calcule le pourcentage utilisé et le nombre d'octets restants. Notez que navigator.storage renvoie une instance de StorageManager. Il existe une interface Storage distincte, très facile à prendre en compte.

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 pour les développeurs Chromium, vous pouvez consulter le quota de votre site et l'espace de stockage utilisé, répartis selon l'utilisation, en ouvrant la section Stockage de l'onglet Application.

Outils pour les développeurs Chrome dans les applications, section "Vider l'espace de stockage"

Firefox et Safari ne proposent pas d'écran récapitulatif permettant d'afficher l'intégralité du quota de stockage et de l'utilisation pour l'origine actuelle.

Persistance des données

Vous pouvez demander au navigateur un stockage persistant sur des plates-formes compatibles pour éviter l'éviction automatique des données après une période d'inactivité ou lorsque l'espace de stockage est saturé. Si cette option est activée, le navigateur n'évince jamais de données de l'espace de stockage. Cette protection inclut l'enregistrement du service worker, les bases de données IndexedDB et les fichiers stockés dans le cache. Notez que les utilisateurs sont toujours responsables et qu'ils peuvent supprimer l'espace de stockage à tout moment, même si le navigateur autorise le stockage persistant.

Pour demander un stockage persistant, appelez StorageManager.persist(). Comme précédemment, vous pouvez accéder à l'interface StorageManager 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 le 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 basés sur Chromium accordent ou refusent la persistance en fonction d'une heuristique afin de déterminer l'importance du contenu pour l'utilisateur. L'un des critères à respecter pour utiliser Google Chrome est, par exemple, l'installation d'une PWA. Si l'utilisateur a installé une icône pour la PWA dans le système d'exploitation, le navigateur peut accorder un espace de stockage persistant.

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

Compatibilité avec les navigateurs d'API

Stockage Web

Navigateurs pris en charge

  • 4
  • 12
  • 3,5
  • 4

Source

Accès au système de fichiers

Navigateurs pris en charge

  • 86
  • 86
  • 111
  • 15.2

Source

Gestionnaire d'espace de stockage

Navigateurs pris en charge

  • 55
  • 79
  • 57
  • 15.2

Source

Ressources