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.
Stockage
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:
- Utilisez la fonction
openDB
pour créer une base de données IndexedDB appeléecookbook
. É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. - 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 fonctionupgrade()
contient une instructionswitch
sans instructionsbreak
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:
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:
- Démarrez une transaction avec
mode
défini surreadwrite
. - Obtenez le data store dans lequel vous allez ajouter des données.
- 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éthodeput()
.
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:
- Lancez une transaction et spécifiez le ou les magasins d'objets, et éventuellement le type de transaction.
- Appelez
objectStore()
à partir de cette transaction. Veillez à spécifier le nom du magasin d'objets. - 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).
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.
Compatibilité avec les navigateurs pour les API
Stockage Web
Accès au système de fichiers
Gestionnaire d'espace de stockage