Per creare un'esperienza offline solida, la tua PWA deve gestire l'archiviazione. Nel capitolo sulla memorizzazione nella cache hai scoperto che l'archiviazione nella cache è una delle opzioni per salvare i dati su un dispositivo. In questo capitolo ti mostreremo come gestire i dati offline, inclusi la persistenza dei dati, i limiti e gli strumenti disponibili.
Archiviazione
L'archiviazione non riguarda solo file e asset, ma può includere altri tipi di dati. Su tutti i browser che supportano le PWA, per l'archiviazione sul dispositivo sono disponibili le seguenti API:
- IndexedDB: un'opzione di archiviazione di oggetti NoSQL per dati strutturati e BLOB (dati binari).
- WebStorage: un modo per memorizzare coppie di stringhe chiave/valore utilizzando la memoria locale o di sessione. Non è disponibile nel contesto di un service worker. Questa API è sincrona, pertanto non è consigliata per l'archiviazione di dati complessi.
- Spazio di archiviazione cache: come descritto nel modulo Caching.
Puoi gestire tutto lo spazio di archiviazione del dispositivo con l'API Storage Manager sulle piattaforme supportate. Le API Cache Storage e IndexedDB forniscono accesso asincrono all'archiviazione permanente per le PWA e sono accessibili dal thread principale, dai web worker e dai service worker. Entrambi svolgono un ruolo essenziale per garantire il funzionamento affidabile delle PWA quando la rete è instabile o inesistente. Ma quando dovresti utilizzarli?
Utilizza l'API Cache Storage per le risorse di rete, ovvero gli elementi a cui accedi richiedendoli tramite un URL, ad esempio HTML, CSS, JavaScript, immagini, video e audio.
Utilizza IndexedDB per archiviare dati strutturati. Ciò include i dati che devono essere ricercabili o combinabili in modo simile a NoSQL o altri dati, come quelli specifici dell'utente, che non corrispondono necessariamente a una richiesta di URL. Tieni presente che IndexedDB non è progettato per la ricerca full-text.
IndexedDB
Per utilizzare IndexedDB, apri prima un database. Viene creato un nuovo database se non ne esiste uno.
IndexedDB è un'API asincrona, ma accetta un callback anziché restituire una promessa. L'esempio seguente utilizza la libreria idb di Jake Archibald, un piccolo wrapper Promise per IndexedDB. Le librerie helper non sono necessarie per utilizzare IndexedDB, ma se vuoi utilizzare la sintassi Promise, la libreria idb
è un'opzione.
L'esempio seguente crea un database per contenere ricette di cucina.
Creare e aprire un database
Per aprire un database:
- Utilizza la funzione
openDB
per creare un nuovo database IndexedDB chiamatocookbook
. Poiché i database IndexedDB sono versionati, devi aumentare il numero di versione ogni volta che apporti modifiche alla struttura del database. Il secondo parametro è la versione del database. Nell'esempio è impostato su 1. - A
openDB()
viene passato un oggetto di inizializzazione contenente un callbackupgrade()
. La funzione di callback viene chiamata quando il database viene installato per la prima volta o quando viene eseguito l'upgrade a una nuova versione. Questa funzione è l'unico punto in cui possono verificarsi azioni. Le azioni potrebbero includere la creazione di nuovi archivi di oggetti (le strutture utilizzate da IndexedDB per organizzare i dati) o indici (in cui vuoi eseguire ricerche). È anche qui che deve avvenire la migrazione dei dati. In genere, la funzioneupgrade()
contiene un'istruzioneswitch
senza istruzionibreak
per consentire a ogni passaggio di avvenire in ordine, in base alla vecchia versione del database.
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'esempio crea un archivio di oggetti all'interno del database cookbook
chiamato recipes
, con la proprietà id
impostata come chiave di indice dell'archivio e crea un altro indice chiamato type
, basato sulla proprietà type
.
Diamo un'occhiata all'object store appena creato. Dopo aver aggiunto le ricette all'object store e aperto DevTools sui browser basati su Chromium o Web Inspector su Safari, dovresti visualizzare quanto segue:
Aggiunta di dati
IndexedDB utilizza le transazioni. Le transazioni raggruppano le azioni in modo che vengano eseguite come un'unica unità. Contribuiscono a garantire che il database sia sempre in uno stato coerente. Sono fondamentali anche per impedire la scrittura simultanea degli stessi dati se sono in esecuzione più copie dell'app. Per aggiungere i dati:
- Avvia una transazione con
mode
impostato sureadwrite
. - Recupera l'archivio di oggetti in cui aggiungerai i dati.
- Chiama
add()
con i dati che stai salvando. Il metodo riceve i dati in formato dizionario (come coppie chiave/valore) e li aggiunge all'archivio oggetti. Il dizionario deve essere clonabile utilizzando la clonazione strutturata. Se vuoi aggiornare un oggetto esistente, devi chiamare il metodoput()
.
Le transazioni hanno una promessa done
che viene risolta quando la transazione viene completata correttamente o rifiutata con un errore di transazione.
Come spiegato nella documentazione della libreria IDB, se stai scrivendo nel database, tx.done
è il segnale che tutto è stato registrato correttamente nel database. Tuttavia, è utile attendere le singole operazioni per poter visualizzare eventuali errori che causano l'esito negativo della transazione.
// 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;
}
Una volta aggiunti, i cookie si troveranno nel database insieme alle altre ricette. L'ID viene impostato e incrementato automaticamente da IndexedDB. Se esegui questo codice due volte, avrai due voci di cookie identiche.
Recupero dei dati in corso…
Ecco come ottenere i dati da IndexedDB:
- Avvia una transazione e specifica l'archivio o gli archivi di oggetti e, facoltativamente, il tipo di transazione.
- Chiama
objectStore()
da questa transazione. Assicurati di specificare il nome dell'object store. - Chiama
get()
con la chiave che vuoi ottenere. Per impostazione predefinita, lo store utilizza la chiave come 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]);
}
Gestione archiviazione
Sapere come gestire lo spazio di archiviazione della PWA è particolarmente importante per archiviare e riprodurre in streaming correttamente le risposte di rete.
La capacità di archiviazione è condivisa tra tutte le opzioni di archiviazione, tra cui Cache Storage, IndexedDB, Web Storage e persino il file service worker e le relative dipendenze.
Tuttavia, la quantità di spazio di archiviazione disponibile varia da browser a browser. È improbabile che tu esaurisca lo spazio di archiviazione. Alcuni browser possono archiviare megabyte e persino gigabyte di dati. Ad esempio, Chrome consente al browser di utilizzare fino all'80% dello spazio su disco totale e una singola origine può utilizzare fino al 60% dell'intero spazio su disco. Per i browser che supportano l'API Storage, puoi sapere quanto spazio di archiviazione è ancora disponibile per la tua app, la relativa quota e il relativo utilizzo.
L'esempio seguente utilizza l'API Storage per ottenere la quota e l'utilizzo stimati, quindi calcola la percentuale utilizzata e i byte rimanenti. Tieni presente che navigator.storage
restituisce un'istanza di StorageManager
. Esiste un'interfaccia Storage
separata ed è facile confondersi.
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.`);
}
In Chromium DevTools, puoi visualizzare la quota del tuo sito e la quantità di spazio di archiviazione utilizzato suddivisa per tipo di utilizzo aprendo la sezione Archiviazione nella scheda Applicazione.
Firefox e Safari non offrono una schermata di riepilogo per visualizzare tutte le quote e l'utilizzo dello spazio di archiviazione per l'origine corrente.
Persistenza dei dati
Puoi chiedere al browser di utilizzare l'archiviazione permanente su piattaforme compatibili per evitare l'eliminazione automatica dei dati dopo l'inattività o in caso di pressione sull'archiviazione. Se viene concesso, il browser non eliminerà mai i dati dallo spazio di archiviazione. Questa protezione include la registrazione del service worker, i database IndexedDB e i file nella memoria della cache. Tieni presente che gli utenti hanno sempre il controllo e possono eliminare lo spazio di archiviazione in qualsiasi momento, anche se il browser ha concesso l'archiviazione persistente.
Per richiedere l'archiviazione permanente, chiama StorageManager.persist()
. Come in precedenza, l'interfaccia StorageManager
è accessibile tramite la proprietà navigator.storage
.
async function persistData() {
if (navigator.storage && navigator.storage.persist) {
const result = await navigator.storage.persist();
console.log(`Data persisted: ${result}`);
}
Puoi anche verificare se l'archiviazione permanente è già concessa nell'origine corrente chiamando StorageManager.persisted()
. Firefox chiede all'utente l'autorizzazione a utilizzare lo spazio di archiviazione permanente. I browser basati su Chromium concedono o negano la persistenza in base a un'euristica per determinare l'importanza dei contenuti per l'utente. Un criterio per Google Chrome è, ad esempio, l'installazione di PWA. Se l'utente ha installato un'icona per la PWA nel sistema operativo, il browser potrebbe concedere l'archiviazione permanente.
Supporto del browser per le API
Web Storage
Accesso al file system
Gestione archiviazione