Datos sin conexión

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

Almacenamiento

El almacenamiento no solo se trata de archivos y activos, sino que 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 de cadenas clave-valor mediante 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. La API de Cache Storage y IndexedDB proporcionan acceso asíncrono al almacenamiento persistente para AWP y se puede acceder a él desde el subproceso principal, los trabajadores web y los service worker. Ambas funciones son esenciales para que las AWP funcionen de forma confiable cuando la red es inestable o no existe. ¿Cuándo deberías usar cada uno?

Usa la API de Cache Storage para recursos de red y 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 datos que se pueden buscar o combinar de manera similar a un NoSQL, o bien otros datos, como datos específicos del usuario que no coinciden necesariamente 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 requiere una devolución de llamada en lugar de mostrar una promesa. En el siguiente ejemplo, se usa la biblioteca de IDs de Jake Archibald, que es un pequeño wrapper de Promise para IndexedDB. No se requieren bibliotecas auxiliares para usar IndexedDB, pero si deseas usar la sintaxis de promesa, la biblioteca idb es una opción.

En el siguiente ejemplo, se crea una base de datos para guardar 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 de IndexedDB llamada cookbook. Dado que las bases de datos IndexedDB tienen control de versiones, deberás 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 la base de datos se instala por primera vez o cuando se actualiza a una nueva versión. 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 deseas realizar búsquedas). Aquí es también donde debe 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 ocurra 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 configurada como la clave de índice del almacén, y se crea otro índice llamado type, basado en la propiedad type.

Echemos un vistazo al almacén de objetos que acabas 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, esto es lo que deberías ver:

Safari y Chrome muestran contenido de IndexedDB.

Agrega datos

IndexedDB usa transacciones. Las transacciones agrupan las acciones, de modo que ocurren como una unidad. Ayudan a garantizar que la base de datos siempre esté en un estado coherente. También son fundamentales si tienes varias copias de tu app en ejecución, para evitar escribir en los mismos datos de forma simultánea. Para agregar datos, haz lo siguiente:

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

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, te recomendamos que esperes 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 hayas agregado las galletas, la receta se incluirá en la base de datos con otras recetas. El ID se configura y aumenta por indexDB automáticamente. Si ejecutas este código dos veces, tendrás dos entradas de cookie idénticas.

Cómo recuperar datos

A continuación, te mostramos cómo obtener datos de IndexedDB:

  1. Inicia una transacción y especifica el depósito o los almacenes de objetos y, de forma 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 AWP es particularmente importante para almacenar y transmitir 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, 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 poco probable que se agote; los sitios pueden 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. En el caso de los navegadores que admiten la API de Storage, puedes saber cuánto almacenamiento queda 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 usado y los bytes restantes. Ten en cuenta que navigator.storage muestra una instancia de StorageManager. Hay una interfaz de 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 usa, desglosado según lo que lo 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 Liberar 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 persistente 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á datos del almacenamiento. Esta protección incluye el registro del service worker, las bases de datos de IndexedDB y los archivos almacenados en caché. Ten en cuenta que los usuarios están siempre a cargo 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, 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 verificar si el almacenamiento persistente ya se otorgó en el origen actual llamando a StorageManager.persisted(). Firefox solicita permiso al usuario para utilizar el almacenamiento persistente. Los navegadores basados en Chromium otorgan o deniegan la 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, es posible que el navegador otorgue almacenamiento persistente.

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

Compatibilidad con el navegador de la API

Almacenamiento web

Navegadores compatibles

  • 4
  • 12
  • 3.5
  • 4

Origen

Acceso al sistema de archivos

Navegadores compatibles

  • 86
  • 86
  • 111
  • 15.2

Origen

Administrador de almacenamiento

Navegadores compatibles

  • 55
  • 79
  • 57
  • 15.2

Origen

Recursos