Dati offline

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:

  1. Utilizza la funzione openDB per creare un nuovo database IndexedDB chiamato cookbook. 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.
  2. Un oggetto di inizializzazione contenente un callback upgrade() viene passato a openDB(). 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 funzione upgrade() contiene un'istruzione switch senza istruzioni break 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:

Safari e Chrome che mostrano contenuti IndexedDB.

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:

  1. Avvia una transazione con mode impostato su readwrite.
  2. Recupera l'archivio di oggetti in cui aggiungere 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'archivio di oggetti. Il dizionario deve essere clonato mediante la clonazione strutturata. Se vuoi aggiornare un oggetto esistente, devi chiamare il metodo put().

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:

  1. Avvia una transazione e specifica l'archivio o gli archivi di oggetti e, facoltativamente, il tipo di transazione.
  2. Chiama objectStore() da quella transazione. Assicurati di specificare il nome dell'archivio oggetti.
  3. 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.

Chrome DevTools nella sezione Applicazione, Cancella spazio di archiviazione

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.

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

Supporto browser API

Archiviazione web

Supporto dei browser

  • 4
  • 12
  • 3,5
  • 4

Fonte

Accesso al file system

Supporto dei browser

  • 86
  • 86
  • 111
  • 15.2

Fonte

Gestione archiviazione

Supporto dei browser

  • 55
  • 79
  • 57
  • 15.2

Fonte

Risorse