Para crear una experiencia sin conexión sólida, tu AWP 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, incluida la persistencia de datos, los límites y las herramientas disponibles.
Almacenamiento
El almacenamiento no solo se trata de archivos y recursos, sino que también puede incluir otros tipos de datos. En todos los navegadores que admiten 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 con almacenamiento local o de sesión. No está disponible en el contexto de un trabajador de servicio. 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 plataformas compatibles. La API de Cache Storage y IndexedDB proporcionan acceso asíncrono al almacenamiento persistente para los PWA, y se puede acceder a ellos desde el subproceso principal, los trabajadores web y los trabajadores del servicio. Ambos desempeñan un papel esencial para que los PWA funcionen de forma 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 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 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 mostrar una promesa. En el siguiente ejemplo, se usa la biblioteca idb de Jake Archibald, que es un pequeño wrapper de Promise para IndexedDB. No se requieren bibliotecas de ayuda para usar IndexedDB, pero si deseas usar la sintaxis de Promise, la biblioteca idb
es una opción.
En el siguiente ejemplo, se crea una base de datos para contener recetas de cocina.
Cómo crear y abrir una base de datos
Para abrir una base de datos, sigue estos pasos:
- Usa la función
openDB
para crear una nueva base de datos de IndexedDB llamadacookbook
. Debido a que las bases de datos de 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. - Se pasa un objeto de inicialización que contiene una devolución de llamada
upgrade()
aopenDB()
. 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 en el que pueden ocurrir acciones. Las acciones pueden incluir la creación de almacenes de objetos nuevos (las estructuras que usa IndexedDB para organizar los datos) o índices (en los que te gustaría realizar búsquedas). También es aquí donde debe ocurrir la migración de datos. Por lo general, la funciónupgrade()
contiene una sentenciaswitch
sin sentenciasbreak
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 se acaba de crear. Después de agregar recetas al almacén de objetos y abrir DevTools en navegadores basados en Chromium o el Inspector web en Safari, deberías ver lo siguiente:
Agrega datos
IndexedDB usa transacciones. Las transacciones agrupan acciones para que ocurran 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 la app en ejecución, para evitar la escritura simultánea en los mismos datos. Para agregar datos, sigue estos pasos:
- Inicia una transacción con el
mode
establecido enreadwrite
. - Obtén el almacén de objetos, en el que agregarás datos.
- Llama a
add()
con los datos que deseas guardar. 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étodoput()
.
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 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 cookies, la receta estará en la base de datos con otras recetas. indexedDB establece y aumenta automáticamente el ID. Si ejecutas este código dos veces, tendrás dos entradas de cookies idénticas.
Recuperando datos
A continuación, se muestra cómo obtener datos de IndexedDB:
- Inicia una transacción y especifica la tienda de objetos o las tiendas, y, de manera opcional, el tipo de transacción.
- Llama a
objectStore()
desde esa transacción. Asegúrate de especificar el nombre de la tienda de objetos. - 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 red de forma correcta.
La capacidad de almacenamiento se comparte entre todas las opciones de almacenamiento, incluido el almacenamiento en caché, IndexedDB, el almacenamiento web y hasta el archivo del trabajador de servicio y sus dependencias.
Sin embargo, la cantidad de almacenamiento disponible varía de un navegador a otro. Es probable que no se agote, ya que los sitios pueden almacenar megabytes y hasta gigabytes de datos en algunos navegadores. Por ejemplo, Chrome permite que el navegador use hasta el 80% del espacio total en el disco, y un origen individual puede usar hasta el 60% de todo el espacio en el disco. En el caso de los navegadores que admiten la API de Storage, puedes saber cuánto almacenamiento aún está disponible para tu app, 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
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.
Firefox y Safari no ofrecen una pantalla de resumen para ver toda la cuota y el uso de almacenamiento del origen actual.
Persistencia de datos
Puedes solicitarle al navegador almacenamiento persistente en plataformas compatibles para evitar la expulsión automática de datos después de un período de inactividad o cuando haya presión de almacenamiento. Si se otorga, el navegador nunca desalojará datos del almacenamiento. Esta protección incluye el registro del trabajador de servicio, las bases de datos de IndexedDB y los archivos en el almacenamiento en caché. Ten en cuenta que los usuarios siempre están 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
se accede 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 ya se otorgó el almacenamiento persistente en el origen actual. Firefox solicita permiso al usuario para usar el almacenamiento persistente. Los navegadores basados en Chromium otorgan o rechazan 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.
Compatibilidad del navegador con la API
Almacenamiento web
Acceso al sistema de archivos
Administrador de almacenamiento