Datos sin conexión

Para crear una experiencia sin conexión sólida, tu APW necesita administración de almacenamiento. En el capítulo sobre el almacenamiento en caché, aprendiste que el almacenamiento en caché es una opción para guardar datos en un dispositivo. En este capítulo, te mostraremos cómo administrar los datos sin conexión, incluidos la persistencia de los datos, los límites y las herramientas disponibles.

Almacenamiento

El almacenamiento no se trata solo de archivos y recursos, sino que puede incluir otros tipos de datos. En todos los navegadores que admiten PWA, las siguientes APIs están disponibles para el almacenamiento en el dispositivo:

  • IndexedDB: Es una opción de almacenamiento de objetos NoSQL para datos estructurados y BLOB (datos binarios).
  • WebStorage: Es una forma de almacenar pares de cadenas clave-valor con almacenamiento local o almacenamiento de sesión. No está disponible en el contexto de un service worker. Esta API es síncrona, por lo que no se recomienda para el almacenamiento de datos complejos.
  • Cache Storage: Como se explicó en el módulo sobre almacenamiento en caché.

Puedes administrar todo el almacenamiento del dispositivo con la API de Storage Manager en las plataformas compatibles. Las APIs de Cache Storage y IndexedDB proporcionan acceso asíncrono al almacenamiento persistente para las PWA y se puede acceder a ellas desde el subproceso principal, los trabajadores web y los service workers. Ambos desempeñan un papel esencial para que las APW funcionen de manera confiable cuando la red es inestable o no existe. Pero, ¿cuándo deberías usar cada una?

Usa la API de Cache Storage para los recursos de red, es decir, los elementos a los que accederías solicitándolos a través de una URL, como HTML, CSS, JavaScript, imágenes, videos y audio.

Usa IndexedDB para almacenar datos estructurados. Esto incluye los datos que deben poder buscarse o combinarse de manera similar a NoSQL, o bien otros datos, como los datos específicos del usuario, que no necesariamente coinciden con una solicitud de URL. Ten en cuenta que IndexedDB no está diseñado para la búsqueda en el texto completo.

IndexedDB

Para usar IndexedDB, primero abre una base de datos. Esto crea una base de datos nueva si no existe una. IndexedDB es una API asíncrona, pero toma una devolución de llamada en lugar de devolver una promesa. En el siguiente ejemplo, se usa la biblioteca idb de Jake Archibald, que es un wrapper pequeño de Promise para IndexedDB. No es necesario usar bibliotecas de ayuda para usar IndexedDB, pero, si quieres usar la sintaxis de Promise, la biblioteca idb es una opción.

En el siguiente ejemplo, se crea una base de datos para almacenar recetas de cocina.

Cómo crear y abrir una base de datos

Para abrir una base de datos, sigue estos pasos:

  1. Usa la función openDB para crear una nueva base de datos IndexedDB llamada cookbook. Dado que las bases de datos de IndexedDB tienen versiones, debes aumentar el número de versión cada vez que realices cambios en la estructura de la base de datos. El segundo parámetro es la versión de la base de datos. En el ejemplo, se establece en 1.
  2. Se pasa un objeto de inicialización que contiene una devolución de llamada upgrade() a openDB(). Se llama a la función de devolución de llamada cuando se instala la base de datos por primera vez o cuando se actualiza a una versión nueva. Esta función es el único lugar donde pueden ocurrir acciones. Las acciones pueden incluir la creación de nuevos almacenes de objetos (las estructuras que IndexedDB usa para organizar los datos) o índices (en los que te gustaría realizar búsquedas). Aquí también se debe realizar la migración de datos. Por lo general, la función upgrade() contiene una instrucción switch sin instrucciones break para permitir que cada paso se realice en orden, según la versión anterior de la base de datos.
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');
     }
   }
  });
}

En el ejemplo, se crea un almacén de objetos dentro de la base de datos cookbook llamado recipes, con la propiedad id establecida como la clave de índice del almacén, y se crea otro índice llamado type, basado en la propiedad type.

Veamos el almacén de objetos que acabamos de crear. Después de agregar recetas al almacén de objetos y abrir Herramientas para desarrolladores en navegadores basados en Chromium o el Inspector web en Safari, deberías ver lo siguiente:

Safari y Chrome muestran el contenido de IndexedDB.

Agrega datos

IndexedDB usa transacciones. Las transacciones agrupan acciones para que se realicen como una unidad. Ayudan a garantizar que la base de datos siempre se encuentre en un estado coherente. También son fundamentales, si tienes varias copias de tu app en ejecución, para evitar la escritura simultánea en los mismos datos. Para agregar datos, sigue estos pasos:

  1. Inicia una transacción con el parámetro mode establecido en readwrite.
  2. Obtén el almacén de objetos, en el que agregarás datos.
  3. Llama a add() con los datos que guardas. El método recibe datos en forma de diccionario (como pares clave-valor) y los agrega al almacén de objetos. El diccionario se debe poder clonar con la clonación estructurada. Si quisieras actualizar un objeto existente, llamarías al método put().

Las transacciones tienen una promesa done que se resuelve cuando la transacción se completa correctamente o se rechaza con un error de transacción.

Como se explica en la documentación de la biblioteca de IDB, si escribes en la base de datos, tx.done es el indicador de que todo se confirmó correctamente en la base de datos. Sin embargo, es beneficioso esperar las operaciones individuales para que puedas ver los errores que hacen que la transacción falle.

// 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 vez que agregues las cookies, la receta estará en la base de datos con otras recetas. IndexedDB establece y aumenta el ID automáticamente. Si ejecutas este código dos veces, tendrás dos entradas de cookies idénticas.

Recuperando datos

A continuación, se explica cómo obtener datos de IndexedDB:

  1. Inicia una transacción y especifica el almacén de objetos o los almacenes de objetos, y, de manera opcional, el tipo de transacción.
  2. Llama a objectStore() desde esa transacción. Asegúrate de especificar el nombre del almacén de objetos.
  3. Llama a get() con la clave que deseas obtener. De forma predeterminada, el almacén usa su clave como índice.
// 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]);
}

El administrador de almacenamiento

Saber cómo administrar el almacenamiento de tu PWA es especialmente importante para almacenar y transmitir las respuestas de la red de forma correcta.

La capacidad de almacenamiento se comparte entre todas las opciones de almacenamiento, incluido Cache Storage, IndexedDB, Web Storage y hasta el archivo de Service Worker y sus dependencias. Sin embargo, la cantidad de almacenamiento disponible varía de un navegador a otro. Es poco probable que te quedes sin espacio, ya que los sitios pueden almacenar megabytes e incluso gigabytes de datos en algunos navegadores. Por ejemplo, Chrome permite que el navegador use hasta el 80% del espacio total del disco, y un origen individual puede usar hasta el 60% de todo el espacio del disco. En los navegadores que admiten la API de Storage, puedes saber cuánto almacenamiento está disponible para tu app, su cuota y su uso. En el siguiente ejemplo, se usa la API de Storage para obtener la cuota y el uso estimados, y, luego, se calcula el porcentaje de bytes usados y restantes. Ten en cuenta que navigator.storage devuelve una instancia de StorageManager. Hay una interfaz Storage independiente, y es fácil confundirlas.

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.`);
}

En las Herramientas para desarrolladores de Chromium, puedes ver la cuota de tu sitio y cuánto almacenamiento se usa desglosado por lo que lo usa. Para ello, abre la sección Almacenamiento en la pestaña Aplicación.

Herramientas para desarrolladores de Chrome en la sección Application, Clear Storage

Firefox y Safari no ofrecen una pantalla de resumen para ver toda la cuota de almacenamiento y el uso del origen actual.

Persistencia de datos

Puedes solicitarle al navegador almacenamiento persistente en plataformas compatibles para evitar el desalojo automático de datos después de un período de inactividad o cuando haya presión de almacenamiento. Si se otorga, el navegador nunca expulsará datos del almacenamiento. Esta protección incluye el registro del service worker, las bases de datos de IndexedDB y los archivos en el almacenamiento de caché. Ten en cuenta que los usuarios siempre tienen el control y pueden borrar el almacenamiento en cualquier momento, incluso si el navegador otorgó almacenamiento persistente.

Para solicitar almacenamiento persistente, llama a StorageManager.persist(). Al igual que antes, se accede a la interfaz StorageManager a través de la propiedad navigator.storage.

async function persistData() {
  if (navigator.storage && navigator.storage.persist) {
    const result = await navigator.storage.persist();
    console.log(`Data persisted: ${result}`);
}

También puedes llamar a StorageManager.persisted() para verificar si el almacenamiento persistente ya se otorgó en el origen actual. Firefox solicita permiso al usuario para usar el almacenamiento persistente. Los navegadores basados en Chromium otorgan o niegan la persistencia según una heurística para determinar la importancia del contenido para el usuario. Un criterio para Google Chrome es, por ejemplo, la instalación de AWP. Si el usuario instaló un ícono para la PWA en el sistema operativo, es posible que el navegador otorgue almacenamiento persistente.

Mozilla Firefox le solicita al usuario permiso de persistencia de almacenamiento.

Compatibilidad con el Explorador de APIs

Almacenamiento web

Browser Support

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

Source

Acceso al sistema de archivos

Browser Support

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

Source

Administrador de almacenamiento

Browser Support

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

Source

Recursos