Dati offline

Per creare un'esperienza offline solida, la tua PWA ha bisogno di gestione dello spazio di archiviazione. Nel capitolo sulla memorizzazione nella cache hai appreso che lo spazio di archiviazione della cache è un'opzione per salvare i dati su un dispositivo. In questo capitolo ti mostreremo come gestire i dati offline, inclusa la persistenza dei dati, i limiti e gli strumenti disponibili.

Archiviazione

Lo spazio di archiviazione non riguarda solo file e asset, ma può includere altri tipi di dati. Su tutti i browser che supportano le PWA, sono disponibili le seguenti API per lo spazio di archiviazione sul dispositivo:

  • 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 in un contesto di service worker. Questa API è sincrona, pertanto non è consigliata per l'archiviazione di dati complessi.
  • Spazio di archiviazione cache: come descritto nel modulo sulla 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 accesso asincrono allo spazio di 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 utilizzare l'una o l'altra?

Utilizza l'API Cache Storage per le risorse di rete, ovvero le risorse a cui accedi richiedendole tramite un URL, come HTML, CSS, JavaScript, immagini, video e audio.

Utilizza IndexedDB per archiviare dati strutturati. Sono inclusi i dati che devono essere cercabili 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 a testo intero.

IndexedDB

Per utilizzare IndexedDB, apri prima un database. Se non esiste, viene creato un nuovo database. IndexedDB è un'API asincrona, ma accetta un callback anziché restituire una promessa. L'esempio seguente utilizza la libreria idb di Jake Archibald, che è un piccolo wrapper Promise per IndexedDB. Le librerie di assistenza non sono necessarie per utilizzare IndexedDB, ma se vuoi utilizzare la sintassi Promise, la libreria idb è un'opzione.

Il seguente esempio crea un database per contenere le ricette di cucina.

Creazione e apertura di un database

Per aprire un database:

  1. Utilizza la funzione openDB per creare un nuovo database IndexedDB denominato cookbook. 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.
  2. A openDB() viene passato un oggetto di inizializzazione contenente un callback upgrade(). 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 possono includere la creazione di nuovi oggetti (le strutture utilizzate da IndexedDB per organizzare i dati) o di indici (su cui vuoi eseguire ricerche). È qui che deve avvenire la migrazione dei dati. In genere, la funzione upgrade() contiene un'istruzione switch senza istruzioni break per consentire l'esecuzione di ogni passaggio 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 object store all'interno del database cookbook denominato recipes, con la proprietà id impostata come chiave dell'indice dell'archivio e crea un altro indice denominato type, in base alla proprietà type.

Diamo un'occhiata all'object store appena creato. Dopo aver aggiunto le ricette all'object store e aver aperto DevTools sui browser basati su Chromium o Web Inspector su Safari, dovresti visualizzare quanto segue:

Safari e Chrome che mostrano i contenuti di IndexedDB.

Aggiunta di dati

IndexedDB utilizza le transazioni. Le transazioni raggruppano le azioni, in modo che vengano eseguite come un'unità. Contribuiscono a garantire che il database sia sempre in uno stato coerente. Sono inoltre fondamentali, se hai più copie dell'app in esecuzione, per impedire la scrittura simultanea negli stessi dati. Per aggiungere dati:

  1. Avvia una transazione con mode impostato su readwrite.
  2. Ottieni l'object store in cui aggiungerai i dati.
  3. 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'object store. Il dizionario deve essere clonabile utilizzando la clonazione strutturata. Se vuoi aggiornare un oggetto esistente, devi chiamare il metodo put().

Le transazioni hanno una promessa done che si risolve quando la transazione viene completata correttamente o viene rifiutata con un errore di transazione.

Come spiegato nella documentazione della libreria IDB, se stai scrivendo nel database, tx.done indica che tutto è stato eseguito correttamente nel database. Tuttavia, è consigliabile attendere le singole operazioni per poter vedere eventuali errori che causano il fallimento 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, la ricetta verrà inserita nel database insieme alle altre ricette. L'ID viene impostato e incrementato automaticamente da indexedDB. Se esegui questo codice due volte, avrai due voci cookie identiche.

Recupero dei dati in corso…

Ecco come recuperare i dati da IndexedDB:

  1. Avvia una transazione e specifica lo o gli oggetti store e, facoltativamente, il tipo di transazione.
  2. Chiama objectStore() da quella transazione. Assicurati di specificare il nome del negozio dell'oggetto.
  3. Chiama get() con la chiave 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 tua PWA è particolarmente importante per archiviare e riprodurre in streaming le risposte di rete in modo corretto.

La capacità di archiviazione è condivisa tra tutte le opzioni di archiviazione, tra cui Cache Storage, IndexedDB, Web Storage e persino il file del servizio worker e le relative dipendenze. Tuttavia, la quantità di spazio di archiviazione disponibile varia da un browser all'altro. È improbabile che tu li esaurisca; i siti potrebbero memorizzare megabyte e persino gigabyte di dati su alcuni browser. 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 sua quota e il suo utilizzo. L'esempio seguente utilizza l'API Storage per stimare la quota e l'utilizzo, 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 confonderle.

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 Strumenti per gli sviluppatori di Chromium, puoi visualizzare la quota del tuo sito e la quantità di spazio di archiviazione utilizzata suddivisa per i relativi utilizzatori aprendo la sezione Spazio di archiviazione nella scheda Applicazione.

Chrome DevTools nella sezione App, Cancella archiviazione

Firefox e Safari non offrono una schermata di riepilogo per visualizzare tutta la quota e l'utilizzo dello spazio di archiviazione per l'origine corrente.

Persistenza dei dati

Puoi chiedere al browser di utilizzare lo spazio di archiviazione permanente su piattaforme compatibili per evitare l'espulsione automatica dei dati dopo un periodo di inattività o in caso di pressione sull'archiviazione. Se concessa, il browser non eliminerà mai i dati dall'archiviazione. Questa protezione include la registrazione dei worker di servizio, i database IndexedDB e i file nello spazio di archiviazione della cache. Tieni presente che gli utenti sono sempre al comando e possono eliminare lo spazio di archiviazione in qualsiasi momento, anche se il browser ha concesso spazio di archiviazione permanente.

Per richiedere lo spazio di 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 controllare se lo spazio di archiviazione permanente è già stato concesso nell'origine corrente chiamando StorageManager.persisted(). Firefox richiede all'utente l'autorizzazione per 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 spazio di archiviazione permanente.

Mozilla Firefox chiede all'utente l'autorizzazione di persistenza dello spazio di archiviazione.

Supporto del browser per le API

Archiviazione web

Browser Support

  • Chrome: 4.
  • Edge: 12.
  • Firefox: 3.5.
  • Safari: 4.

Source

Accesso al file system

Browser Support

  • Chrome: 86.
  • Edge: 86.
  • Firefox: 111.
  • Safari: 15.2.

Source

Gestione archiviazione

Browser Support

  • Chrome: 55.
  • Edge: 79.
  • Firefox: 57.
  • Safari: 15.2.

Source

Risorse