Pour créer une expérience hors connexion solide, votre PWA doit gérer le stockage. Dans le chapitre sur la mise en cache, vous avez appris que le stockage du cache est une option permettant d'enregistrer des données sur un appareil. Dans ce chapitre, nous allons vous montrer 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 concerne pas uniquement les fichiers et les 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 : méthode permettant de stocker des paires de chaînes clé/valeur à l'aide du stockage local ou de session. Elle 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.
- Cache Storage : comme indiqué dans le module sur la mise en cache.
Vous pouvez gérer tout l'espace de stockage des appareils avec l'API Storage Manager sur les plates-formes compatibles. Les API Cache Storage et IndexedDB fournissent 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 quand faut-il utiliser chacun d'eux ?
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, tels que 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 consultables ou combinables de manière NoSQL, ou d'autres données telles que les données spécifiques aux utilisateurs qui ne correspondent pas nécessairement à une requête d'URL. Notez qu'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. Une base de données est créée si elle n'existe pas.
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 Archibald, qui est un petit wrapper Promise pour IndexedDB. Les bibliothèques d'assistance ne sont pas nécessaires 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 versionnées, vous devez incrémenter 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, la valeur est définie 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 nouveaux magasins 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 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');
}
}
});
}
L'exemple crée un magasin d'objets dans la base de données cookbook
appelée recipes
, avec l'ensemble de propriétés id
défini comme clé d'index du magasin, et crée un autre index appelé type
, 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 sur les navigateurs basés sur Chromium ou l'inspecteur Web sur Safari, voici ce que vous devriez voir :
Ajouter des données
IndexedDB utilise des transactions. Les transactions regroupent les actions pour 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. Elles sont également essentielles si vous exécutez plusieurs copies de votre application, car elles empêchent l'écriture simultanée dans les mêmes données. Pour ajouter des données :
- Démarrez une transaction avec
mode
défini surreadwrite
. - Obtenez le magasin d'objets dans lequel vous ajouterez des données.
- Appelez
add()
avec les données que vous enregistrez. La méthode reçoit les données sous forme de dictionnaire (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, vous devez appeler la méthodeput()
.
Les transactions ont une promesse done
qui est résolue lorsque la transaction se termine correctement 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
indique que tout a été correctement validé dans la base de données. Toutefois, il est préférable d'attendre les opérations individuelles pour 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 que vous avez ajouté les cookies, la recette se trouve 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 cookie identiques.
Récupération des données…
Voici comment obtenir des données à partir d'IndexedDB :
- Démarrez une transaction et spécifiez le ou les magasins d'objets, ainsi que le type de transaction (facultatif).
- Appelez
objectStore()
à partir de cette transaction. Assurez-vous de 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 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]);
}
Gestionnaire d'espace de stockage
Il est particulièrement important de savoir comment gérer le stockage de votre PWA pour stocker et diffuser correctement les réponses du 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 Service Worker et ses dépendances.
Toutefois, la quantité de stockage disponible varie d'un navigateur à l'autre. Il est peu probable que vous manquiez d'espace. En effet, les sites peuvent stocker des mégaoctets, voire des gigaoctets de données sur certains navigateurs. Par exemple, Chrome 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 l'espace de stockage encore 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 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é, en ouvrant la section Stockage de l'onglet Application.
Firefox et Safari ne proposent pas d'écran récapitulatif permettant d'afficher l'ensemble 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 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 le 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 du cache. Notez que les utilisateurs sont toujours responsables et qu'ils peuvent supprimer le stockage à tout moment, même si le navigateur a accordé un stockage persistant.
Pour demander un stockage persistant, appelez StorageManager.persist()
. Comme auparavant, 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 le stockage persistant est déjà accordé dans l'origine actuelle en appelant StorageManager.persisted()
. Firefox demande l'autorisation à l'utilisateur d'utiliser le stockage persistant. Les navigateurs basés sur Chromium accordent 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 un critère 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.
Navigateurs compatibles avec l'API
Stockage Web
Accès au système de fichiers
Gestionnaire d'espace de stockage