Per creare una solida esperienza offline, la PWA richiede la gestione dello spazio di archiviazione. Nel capitolo sulla memorizzazione nella cache hai appreso che lo spazio di archiviazione nella cache è un'opzione per salvare i dati su un dispositivo. In questo capitolo, ti mostreremo come gestire i dati offline, tra cui persistenza dei dati, limiti e strumenti disponibili.
Archiviazione
Lo spazio di archiviazione non riguarda solo file e asset, ma può includere altri tipi di dati. In tutti i browser che supportano le PWA, sono disponibili le seguenti API per l'archiviazione on-device:
- IndexedDB: un'opzione di archiviazione di oggetti NoSQL per dati strutturati e BLOB (dati binari).
- WebStorage: un modo per archiviare coppie di stringhe chiave/valore utilizzando la memoria locale o di sessione. Non è disponibile all'interno di un contesto di service worker. Questa API è sincrona, pertanto non è consigliata per l'archiviazione di dati complessi.
- Spazio di archiviazione della cache: come descritto nel modulo Memorizzazione nella cache.
Puoi gestire tutto lo spazio di archiviazione del dispositivo con l'API Storage Manager sulle piattaforme supportate. L'API Cache Storage e IndexedDB forniscono l'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 nel far funzionare le PWA in modo affidabile quando la rete è instabile o inesistente. Quando, però, dovresti utilizzarli?
Utilizza l'API Cache Storage per le risorse di rete e gli elementi ai quali potresti accedere richiedendoli tramite URL, ad esempio HTML, CSS, JavaScript, immagini, video e audio.
Utilizza IndexedDB per archiviare i dati strutturati. Sono inclusi i dati che devono essere disponibili per la ricerca o combinabili in modo simile a NoSQL o altri dati come quelli specifici dell'utente che non corrispondono necessariamente a una richiesta URL. Tieni presente che IndexedDB non è progettato per la ricerca di testo completo.
IndexedDB
Per utilizzare IndexedDB, apri prima un database. Se non esiste un database, viene creato un nuovo database.
IndexedDB è un'API asincrona, ma richiede un callback invece di restituire un Promise. L'esempio seguente utilizza la libreria idb di Jake Archibald, che è un piccolo wrapper Promise per IndexedDB. Le librerie helper non sono necessarie per utilizzare IndexedDB, ma se vuoi usare la sintassi Promise, la libreria idb
è un'opzione.
L'esempio seguente crea un database per contenere le ricette di cucina.
Creazione e apertura di un database
Per aprire un database:
- Utilizza la funzione
openDB
per creare un nuovo database IndexedDB chiamatocookbook
. Poiché i database IndexedDB sono sottoposti al controllo delle versioni, devi aumentare il numero di versione ogni volta che apporti modifiche alla struttura del database. Il secondo parametro è la versione del database. Nell'esempio, il valore è impostato su 1. - Un oggetto di inizializzazione contenente un callback
upgrade()
viene passato aopenDB()
. La funzione di callback viene richiamata quando il database viene installato per la prima volta o quando viene eseguito l'upgrade a una nuova versione. Questa funzione è l'unico in cui possono verificarsi le 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). È qui che avviene anche la migrazione dei dati. In genere, la funzioneupgrade()
contiene un'istruzioneswitch
senza istruzionibreak
per consentire che ogni passaggio venga eseguito in ordine, in base alla versione precedente 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
denominato recipes
, con la proprietà id
impostata come chiave di indice dell'archivio e crea un altro indice chiamato type
, in base alla proprietà type
.
Diamo un'occhiata all'archivio di oggetti appena creato. Dopo aver aggiunto ricette all'archivio di oggetti e aver aperto DevTools sui browser basati su Chromium o l'inspector web su Safari, dovresti vedere questo:
Aggiunta di dati
IndexedDB utilizza le transazioni. Le transazioni raggruppano le azioni, che vengono eseguite come unità. Aiutano ad assicurare che il database sia sempre in uno stato coerente. Sono inoltre fondamentali, se hai più copie della tua app in esecuzione, per impedire la scrittura simultanea sugli stessi dati. Per aggiungere dati:
- Avvia una transazione con
mode
impostato sureadwrite
. - Recupera l'archivio di oggetti in cui aggiungere i dati.
- Chiama
add()
con i dati che stai salvando. Il metodo riceve i dati sotto forma di dizionario (come coppie chiave/valore) e li aggiunge all'archivio di oggetti. Il dizionario deve essere clonato mediante la clonazione strutturata. Se vuoi aggiornare un oggetto esistente, devi chiamare il metodoput()
.
Le transazioni hanno una promessa done
che si risolve quando vengono completate correttamente o vengono rifiutate con un errore di transazione.
Come spiegato nella documentazione relativa alla libreria IDB, se stai scrivendo nel database, tx.done
è l'indicazione che è stato eseguito il commit di tutti gli elementi nel database. Tuttavia, è utile attendere le singole operazioni in modo da poter visualizzare eventuali errori che causano la mancata riuscita 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;
}
Dopo aver aggiunto i cookie, la ricetta sarà presente nel database insieme ad altre ricette. L'ID viene impostato automaticamente e incrementato da indexDB. Se esegui questo codice due volte, avrai due voci di cookie identiche.
Recupero dati
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 quella transazione. Assicurati di specificare il nome dell'archivio oggetti. - Chiama
get()
con il token che vuoi ricevere. Per impostazione predefinita, il negozio utilizza la propria 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 trasmettere in modo corretto 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 dei service worker e le sue dipendenze.
Tuttavia, la quantità di spazio di archiviazione disponibile varia in base al browser. È poco probabile che lo esaurisca; in alcuni browser i siti potrebbero archiviare megabyte e persino gigabyte di dati. Chrome, ad esempio, 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 suo utilizzo.
L'esempio seguente utilizza l'API Storage per ottenere una stima della quota e dell'utilizzo, quindi calcola la percentuale utilizzata e i byte rimanenti. Tieni presente che navigator.storage
restituisce un'istanza di StorageManager
. C'è 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 vedere la quota del tuo sito e la quantità di spazio di archiviazione utilizzato, suddivisi in base al dispositivo che lo utilizza, aprendo la sezione Spazio di archiviazione nella scheda Applicazione.
Firefox e Safari non offrono una schermata di riepilogo in cui viene visualizzato tutto lo spazio di archiviazione e l'utilizzo relativi all'origine corrente.
Persistenza dei dati
Puoi chiedere al browser l'archiviazione permanente su piattaforme compatibili per evitare la rimozione automatica dei dati dopo inattività o pressione dell'archiviazione. Se questa autorizzazione viene concessa, il browser non rimuoverà mai i dati dallo spazio di archiviazione. Questa protezione include la registrazione dei service worker, i database IndexedDB e i file nello spazio di archiviazione nella 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 permanente.
Per richiedere l'archiviazione permanente, chiama il StorageManager.persist()
. Come in precedenza, l'interfaccia di 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à stata concessa nell'origine attuale chiamando StorageManager.persisted()
. Firefox richiede all'utente l'autorizzazione per utilizzare l'archiviazione permanente. I browser basati su Chromium forniscono o negano la persistenza in base a un'euristica per determinare l'importanza dei contenuti per l'utente. Ad esempio, un criterio per Google Chrome è 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 browser API
Archiviazione web
Accesso al file system
Gestione archiviazione