Datos sin conexión

Para crear una experiencia sólida sin conexión, la AWP necesita administración de almacenamiento. En el capítulo sobre 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 datos sin conexión, incluidos la persistencia de datos, los límites y las herramientas disponibles.

Almacenamiento

El almacenamiento no se trata solo de archivos y activos, sino que también puede incluir otros tipos de datos. En todos los navegadores compatibles con AWP, 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 clave-valor de cadenas de texto a través del almacenamiento local o 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.
  • Almacenamiento en caché: Como se explica en el Módulo de almacenamiento en caché.

Puedes administrar todo el almacenamiento del dispositivo con la API de Storage Manager en las plataformas compatibles. IndexedDB y la API de Cache Storage proporcionan acceso asíncrono al almacenamiento persistente para las AWP, y se puede acceder a él desde el subproceso principal, los trabajadores web y los service workers. Ambos desempeñan funciones esenciales para que las AWP 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, a los que accederías si los solicitas 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 se pueden buscar o combinar 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 realiza una devolución de llamada en lugar de mostrar una promesa. En el siguiente ejemplo, se usa la biblioteca idb de Jake Archibald, que es un wrapper de promesa pequeño para IndexedDB. No es necesario usar las bibliotecas auxiliares para usar IndexedDB, pero si quieres usar la sintaxis de la promesa, puedes usar la biblioteca idb.

En el siguiente ejemplo, se crea una base de datos que contiene recetas de cocina.

Crea y abre una base de datos

Para abrir una base de datos, haz lo siguiente:

  1. Usa la función openDB para crear una nueva base de datos IndexedDB llamada cookbook. Debido a que las bases de datos IndexedDB tienen control de 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 a openDB() un objeto de inicialización que contiene una devolución de llamada upgrade(). Se llama a la función de devolución de llamada cuando la base de datos se instala por primera vez o cuando se actualiza a una versión nueva. Esta función es el único lugar en el que 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 (que deseas buscar). Aquí es también donde debería ocurrir la migración de datos. Por lo general, la función upgrade() contiene una sentencia switch sin sentencias 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 del índice del almacén y otro índice llamado type, basado en la propiedad type.

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

Safari y Chrome muestran contenido de IndexedDB.

Agrega datos

IndexedDB usa transacciones. Las transacciones agrupan acciones, por lo que suceden como una unidad. Ayudan a garantizar que la base de datos siempre esté en un estado coherente. Si tienes varias copias de tu app en ejecución, también son fundamentales para evitar escrituras simultáneas en los mismos datos. Para agregar datos, haz lo siguiente:

  1. Inicia una transacción con mode configurado como readwrite.
  2. Obtén el almacén de objetos, en el que agregarás datos.
  3. Llama a add() con los datos que estás guardando. 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 clonación estructurada. Si quisieras actualizar un objeto existente, tendrías que llamar al método put() en su lugar.

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

Como se explica en la documentación de la biblioteca del 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, te recomendamos que esperes a que se realicen operaciones individuales para ver los errores que provocan el error de la transacción.

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

Recuperando datos

Así es como obtienes datos de IndexedDB:

  1. Inicia una transacción y especifica los almacenes de objetos y, opcionalmente, 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, la tienda 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 AWP es particularmente importante para almacenar y transmitir respuestas de la red de manera correcta.

La capacidad de almacenamiento se comparte entre todas las opciones de almacenamiento, incluidos Cache Storage, IndexedDB, Web Storage e incluso el archivo del service worker y sus dependencias. Sin embargo, la cantidad de almacenamiento disponible varía de un navegador a otro. Es probable que no se queden; los sitios podían almacenar megabytes e incluso gigabytes de datos en algunos navegadores. Chrome, por ejemplo, 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 en el disco. En el caso de los navegadores compatibles con la API de Storage, puedes saber cuánto almacenamiento aún hay disponible para tu app, así como su cuota y su uso. En el siguiente ejemplo, se usa la API de Storage para obtener una estimación de la cuota y el uso y, luego, se calcula el porcentaje utilizado y los bytes restantes. Ten en cuenta que navigator.storage muestra una instancia de StorageManager. Hay una interfaz Storage separada y es fácil confundirlos.

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 utiliza desglosado por lo que se usa. Para ello, abre la sección Almacenamiento en la pestaña Aplicación.

Herramientas para desarrolladores de Chrome en la aplicación, sección Borrar almacenamiento

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 solicitar al navegador almacenamiento continuo en plataformas compatibles para evitar la expulsión automática de datos después de inactividad o presión de almacenamiento. Si se otorga, el navegador nunca expulsará los datos del almacenamiento. Esta protección incluye el registro del service worker, las bases de datos IndexedDB y los archivos en el almacenamiento en caché. Ten en cuenta que los usuarios siempre están a cargo y que pueden borrar el almacenamiento en cualquier momento, incluso si el navegador otorgó el almacenamiento persistente.

Para solicitar almacenamiento persistente, llama a StorageManager.persist(). Como antes, la interfaz StorageManager es acceso 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 deniegan persistencia en función de 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 AWP en el sistema operativo, el navegador puede otorgar almacenamiento persistente.

Mozilla Firefox solicitando al usuario permiso de persistencia de almacenamiento.

Compatibilidad con el navegador de la API

Almacenamiento web

Navegadores compatibles

  • Chrome: 4.
  • Límite: 12.
  • Firefox: 3.5
  • Safari: 4.

Origen

Acceso al sistema de archivos

Navegadores compatibles

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

Origen

Administrador de almacenamiento

Navegadores compatibles

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

Origen

Recursos