Una guía extensa para aplicaciones web progresivas
Publicado: 2022-03-10Era el cumpleaños de mi papá y quería pedir un pastel de chocolate y una camisa para él. Me dirigí a Google para buscar pasteles de chocolate e hice clic en el primer enlace de los resultados de búsqueda. Hubo una pantalla en blanco durante unos segundos; No entendí lo que estaba pasando. Después de unos segundos de mirar pacientemente, la pantalla de mi móvil se llenó de deliciosos pasteles. Tan pronto como hice clic en uno de ellos para verificar sus detalles, apareció una ventana emergente fea y gorda que me pedía que instalara una aplicación de Android para poder obtener una experiencia suave como la seda mientras ordenaba un pastel.
Eso fue decepcionante. Mi conciencia no me permitía hacer clic en el botón "Instalar". Todo lo que quería hacer era pedir un pastel pequeño y seguir mi camino.
Hice clic en el ícono de la cruz a la derecha de la ventana emergente para salir lo antes posible. Pero luego, la ventana emergente de instalación se sentó en la parte inferior de la pantalla, ocupando una cuarta parte del espacio. Y con la interfaz de usuario escamosa, desplazarse hacia abajo fue un desafío. De alguna manera me las arreglé para pedir un pastel holandés.
Después de esta terrible experiencia, mi próximo reto fue encargar una camiseta para mi papá. Como antes, busco camisas en Google. Hice clic en el primer enlace y, en un abrir y cerrar de ojos, todo el contenido estaba justo frente a mí. El desplazamiento fue suave. Sin banner de instalación. Me sentí como si estuviera navegando en una aplicación nativa. Hubo un momento en que mi terrible conexión a Internet se interrumpió, pero aún podía ver el contenido en lugar de un juego de dinosaurios. Incluso con mi Internet deficiente, logré pedir una camisa y jeans para mi papá. Lo más sorprendente de todo es que estaba recibiendo notificaciones sobre mi pedido.
Yo llamaría a esto una experiencia suave como la seda. Esta gente estaba haciendo algo bien. Cada sitio web debería hacerlo por sus usuarios. Se llama una aplicación web progresiva.
Como afirma Alex Russell en una de sus publicaciones de blog:
“Ocurre en la web de vez en cuando que tecnologías poderosas llegan a existir sin el beneficio de los departamentos de marketing o empaques ingeniosos. Permanecen y crecen en las periferias, convirtiéndose en anticuados para un pequeño grupo mientras permanecen casi invisibles para todos los demás. Hasta que alguien los nombra”.
Una experiencia suave como la seda en la web, a veces conocida como una aplicación web progresiva
Las aplicaciones web progresivas (PWA) son más una metodología que implica una combinación de tecnologías para crear aplicaciones web potentes. Con una experiencia de usuario mejorada, las personas pasarán más tiempo en los sitios web y verán más anuncios. Tienden a comprar más y, con las actualizaciones de notificaciones, es más probable que visiten con más frecuencia. The Financial Times abandonó sus aplicaciones nativas en 2011 y creó una aplicación web utilizando las mejores tecnologías disponibles en ese momento. Ahora, el producto se ha convertido en una PWA completa.
Pero, ¿por qué, después de todo este tiempo, crearía una aplicación web cuando una aplicación nativa hace el trabajo lo suficientemente bien?
Veamos algunas de las métricas compartidas en Google IO 17.
Cinco mil millones de dispositivos están conectados a la web, lo que convierte a la web en la plataforma más grande en la historia de la informática. En la web móvil, 11,4 millones de visitantes únicos mensuales van a las 1000 propiedades web principales y 4 millones van a las mil aplicaciones principales. La web móvil reúne alrededor de cuatro veces más usuarios que las aplicaciones nativas. Pero este número cae bruscamente cuando se trata de participación.
Un usuario pasa un promedio de 188,6 minutos en aplicaciones nativas y solo 9,3 minutos en la web móvil. Las aplicaciones nativas aprovechan el poder de los sistemas operativos para enviar notificaciones automáticas para brindar a los usuarios actualizaciones importantes. Ofrecen una mejor experiencia de usuario y se inician más rápido que los sitios web en un navegador. En lugar de escribir una URL en el navegador web, los usuarios solo tienen que tocar el ícono de una aplicación en la pantalla de inicio.
Es poco probable que la mayoría de los visitantes de la web regresen, por lo que a los desarrolladores se les ocurrió la solución de mostrarles pancartas para instalar aplicaciones nativas, en un intento de mantenerlos profundamente comprometidos. Pero entonces, los usuarios tendrían que pasar por el tedioso procedimiento de instalar el binario de una aplicación nativa. Obligar a los usuarios a instalar una aplicación es molesto y reduce aún más la posibilidad de que la instalen en primer lugar. La oportunidad para la web es clara.
Lectura recomendada : Nativo y PWA: ¡Opciones, no desafíos!
Si las aplicaciones web vienen con una rica experiencia de usuario, notificaciones automáticas, soporte sin conexión y carga instantánea, pueden conquistar el mundo. Esto es lo que hace una aplicación web progresiva.
Una PWA ofrece una rica experiencia de usuario porque tiene varios puntos fuertes:
Rápido
La interfaz de usuario no es escamosa. El desplazamiento es fluido. Y la aplicación responde rápidamente a la interacción del usuario.De confianza
Un sitio web normal obliga a los usuarios a esperar, sin hacer nada, mientras está ocupado yendo al servidor. Mientras tanto, una PWA carga datos instantáneamente desde el caché. Una PWA funciona a la perfección, incluso en una conexión 2G. Cada solicitud de red para obtener un activo o un dato pasa por un trabajador de servicio (más sobre esto más adelante), que primero verifica si la respuesta para una solicitud en particular ya está en el caché. Cuando los usuarios obtienen contenido real casi al instante, incluso con una conexión deficiente, confían más en la aplicación y la ven como más confiable.Atractivo
Un PWA puede ganarse un lugar en la pantalla de inicio del usuario. Ofrece una experiencia similar a la de una aplicación nativa al proporcionar un área de trabajo de pantalla completa. Hace uso de notificaciones automáticas para mantener a los usuarios interesados.
Ahora que sabemos lo que aportan las PWA, entremos en los detalles de lo que le da a las PWA una ventaja sobre las aplicaciones nativas. Las PWA se crean con tecnologías como trabajadores de servicios, manifiestos de aplicaciones web, notificaciones automáticas y estructura de datos locales/indexedDB para el almacenamiento en caché. Veamos cada uno en detalle.
Trabajadores de servicios
Un service worker es un archivo JavaScript que se ejecuta en segundo plano sin interferir con las interacciones del usuario. Todas las solicitudes GET al servidor pasan por un trabajador de servicio. Actúa como un proxy del lado del cliente. Al interceptar las solicitudes de la red, toma el control total sobre la respuesta que se envía al cliente. Un PWA se carga instantáneamente porque los trabajadores del servicio eliminan la dependencia de la red al responder con datos del caché.
Un trabajador de servicio solo puede interceptar una solicitud de red que esté dentro de su alcance. Por ejemplo, un trabajador de servicio con ámbito de raíz puede interceptar todas las solicitudes de recuperación que provienen de una página web. Un trabajador de servicio opera como un sistema controlado por eventos. Entra en un estado latente cuando no se necesita, conservando así la memoria. Para usar un trabajador de servicio en una aplicación web, primero debemos registrarlo en la página con JavaScript.
(function main () { /* navigator is a WEB API that allows scripts to register themselves and carry out their activities. */ if ('serviceWorker' in navigator) { console.log('Service Worker is supported in your browser') /* register method takes in the path of service worker file and returns a promises, which returns the registration object */ navigator.serviceWorker.register('./service-worker.js').then (registration => { console.log('Service Worker is registered!') }) } else { console.log('Service Worker is not supported in your browser') } })()
Primero verificamos si el navegador es compatible con los trabajadores del servicio. Para registrar un trabajador de servicio en una aplicación web, proporcionamos su URL como parámetro a la función de register
, disponible en navigator.serviceWorker
( navigator
es una API web que permite que los scripts se registren y realicen sus actividades). Un trabajador de servicio se registra solo una vez. El registro no ocurre en cada carga de página. El navegador descarga el archivo del trabajador del servicio ( ./service-worker.js
) solo si hay una diferencia de bytes entre el trabajador del servicio activado existente y el más nuevo o si su URL ha cambiado.
El trabajador de servicio anterior interceptará todas las solicitudes provenientes de la raíz ( /
). Para limitar el alcance de un trabajador de servicio, pasaríamos un parámetro opcional con una de las claves como alcance.
if ('serviceWorker' in navigator) { /* register method takes in an optional second parameter as an object. To restrict the scope of a service worker, the scope should be provided. scope: '/books' will intercept requests with '/books' in the url. */ navigator.serviceWorker.register('./service-worker.js', { scope: '/books' }).then(registration => { console.log('Service Worker for scope /books is registered', registration) }) }
El trabajador de servicio anterior interceptará las solicitudes que tengan /books
en la URL. Por ejemplo, no interceptará solicitudes con /products
, pero podría muy bien interceptar solicitudes con /books/products
.
Como se mencionó, un trabajador de servicio opera como un sistema impulsado por eventos. Escucha eventos (instalar, activar, buscar, empujar) y, en consecuencia, llama al controlador de eventos respectivo. Algunos de estos eventos son parte del ciclo de vida de un trabajador de servicio, que pasa por estos eventos en secuencia para activarse.
Instalación
Una vez que un trabajador de servicio se ha registrado correctamente, se activa un evento de instalación. Este es un buen lugar para realizar el trabajo de inicialización, como configurar el caché o crear almacenes de objetos en IndexedDB. (IndexedDB tendrá más sentido para usted una vez que entremos en detalles. Por ahora, solo podemos decir que es una estructura de par clave-valor).
self.addEventListener('install', (event) => { let CACHE_NAME = 'xyz-cache' let urlsToCache = [ '/', '/styles/main.css', '/scripts/bundle.js' ] event.waitUntil( /* open method available on caches, takes in the name of cache as the first parameter. It returns a promise that resolves to the instance of cache All the URLS above can be added to cache using the addAll method. */ caches.open(CACHE_NAME) .then (cache => cache.addAll(urlsToCache)) ) })
Aquí, estamos almacenando en caché algunos de los archivos para que la próxima carga sea instantánea. self
se refiere a la instancia del trabajador de servicio. event.waitUntil
hace que el trabajador del servicio espere hasta que todo el código que contiene haya terminado de ejecutarse.
Activación
Una vez que se ha instalado un trabajador de servicio, aún no puede escuchar las solicitudes de recuperación. Más bien, se dispara un evento de activate
. Si ningún trabajador de servicio activo está operando en el sitio web en el mismo ámbito, el trabajador de servicio instalado se activa de inmediato. Sin embargo, si un sitio web ya tiene un trabajador de servicio activo, la activación de un nuevo trabajador de servicio se retrasa hasta que se cierren todas las pestañas que operan en el trabajador de servicio anterior. Esto tiene sentido porque el trabajador del servicio anterior podría estar usando la instancia del caché que ahora se modifica en el más nuevo. Por lo tanto, el paso de activación es un buen lugar para deshacerse de los cachés antiguos.
self.addEventListener('activate', (event) => { let cacheWhitelist = ['products-v2'] // products-v2 is the name of the new cache event.waitUntil( caches.keys().then (cacheNames => { return Promise.all( cacheNames.map( cacheName => { /* Deleting all the caches except the ones that are in cacheWhitelist array */ if (cacheWhitelist.indexOf(cacheName) === -1) { return caches.delete(cacheName) } }) ) }) ) })
En el código anterior, estamos eliminando el caché antiguo. Si el nombre de un caché no coincide con la cacheWhitelist
, entonces se elimina. Para omitir la fase de espera y activar inmediatamente el service worker, usamos skip.waiting()
.
self.addEventListener('activate', (event) => { self.skipWaiting() // The usual stuff })
Una vez que se activa el trabajador de servicio, puede escuchar solicitudes de recuperación y eventos de inserción.
Obtener controlador de eventos
Cada vez que una página web activa una solicitud de recuperación de un recurso a través de la red, se llama al evento de recuperación del trabajador del servicio. El controlador de eventos de búsqueda primero busca el recurso solicitado en la memoria caché. Si está presente en el caché, devuelve la respuesta con el recurso almacenado en caché. De lo contrario, inicia una solicitud de obtención al servidor y, cuando el servidor devuelve la respuesta con el recurso solicitado, lo coloca en la memoria caché para solicitudes posteriores.
/* Fetch event handler for responding to GET requests with the cached assets */ self.addEventListener('fetch', (event) => { event.respondWith( caches.open('products-v2') .then (cache => { /* Checking if the request is already present in the cache. If it is present, sending it directly to the client */ return cache.match(event.request).then (response => { if (response) { console.log('Cache hit! Fetching response from cache', event.request.url) return response } /* If the request is not present in the cache, we fetch it from the server and then put it in cache for subsequent requests. */ fetch(event.request).then (response => { cache.put(event.request, response.clone()) return response }) }) }) ) })
event.respondWith
permite que el trabajador del servicio envíe una respuesta personalizada al cliente.
Desconectado primero es ahora una cosa. Para cualquier solicitud que no sea crítica, debemos servir la respuesta desde el caché, en lugar de hacer un viaje al servidor. Si algún activo no está presente en el caché, lo obtenemos del servidor y luego lo almacenamos en caché para solicitudes posteriores.
Los trabajadores del servicio solo trabajan en sitios web HTTPS porque tienen el poder de manipular la respuesta de cualquier solicitud de búsqueda. Alguien con intenciones maliciosas podría alterar la respuesta de una solicitud en un sitio web HTTP. Por lo tanto, es obligatorio alojar una PWA en HTTPS. Los trabajadores del servicio no interrumpen el funcionamiento normal del DOM. No pueden comunicarse directamente con la página web. Para enviar cualquier mensaje a una página web, hace uso de los mensajes de publicación.
Notificaciones push web
Supongamos que está ocupado jugando un juego en su dispositivo móvil y aparece una notificación que le informa de un 30% de descuento en su marca favorita. Sin más preámbulos, haces clic en la notificación y compras sin aliento. Obtener actualizaciones en vivo sobre, por ejemplo, un partido de cricket o fútbol o recibir correos electrónicos y recordatorios importantes como notificaciones es un gran problema cuando se trata de involucrar a los usuarios con un producto. Esta función solo estaba disponible en aplicaciones nativas hasta que apareció PWA. Una PWA utiliza notificaciones web push para competir con esta poderosa función que las aplicaciones nativas proporcionan de manera inmediata. Un usuario aún recibiría una notificación push web incluso si el PWA no está abierto en ninguna de las pestañas del navegador e incluso si el navegador no está abierto.
Una aplicación web tiene que pedir permiso al usuario para enviarle notificaciones automáticas.

Una vez que el usuario confirma haciendo clic en el botón "Permitir", el navegador genera un token de suscripción único. Este token es único para este dispositivo. El formato del token de suscripción generado por Chrome es el siguiente:
{ "endpoint": "https://fcm.googleapis.com/fcm/send/c7Veb8VpyM0:APA91bGnMFx8GIxf__UVy6vJ-n9i728CUJSR1UHBPAKOCE_SrwgyP2N8jL4MBXf8NxIqW6NCCBg01u8c5fcY0kIZvxpDjSBA75sVz64OocQ-DisAWoW7PpTge3SwvQAx5zl_45aAXuvS", "expirationTime": null, "keys": { "p256dh": "BJsj63kz8RPZe8Lv1uu-6VSzT12RjxtWyWCzfa18RZ0-8sc5j80pmSF1YXAj0HnnrkyIimRgLo8ohhkzNA7lX4w", "auth": "TJXqKozSJxcWvtQasEUZpQ" } }
El endpoint
contenido en el token anterior será único para cada suscripción. En un sitio web promedio, miles de usuarios aceptarían recibir notificaciones automáticas y, para cada uno de ellos, este endpoint
sería único. Entonces, con la ayuda de este endpoint
, la aplicación puede dirigirse a estos usuarios en el futuro enviándoles notificaciones automáticas. expirationTime
es la cantidad de tiempo que la suscripción es válida para un dispositivo en particular. Si expirationTime
es de 20 días, significa que la suscripción de inserción del usuario caducará después de 20 días y el usuario no podrá recibir notificaciones de inserción en la suscripción anterior. En este caso, el navegador generará un nuevo token de suscripción para ese dispositivo. Las claves auth
y p256dh
se utilizan para el cifrado.
Ahora bien, para enviar notificaciones automáticas a estos miles de usuarios en el futuro, primero debemos guardar sus respectivos tokens de suscripción. El trabajo del servidor de aplicaciones (el servidor back-end, tal vez un script de Node.js) es enviar notificaciones automáticas a estos usuarios. Esto puede sonar tan simple como hacer una solicitud POST
a la URL del punto final con los datos de notificación en la carga útil de la solicitud. Sin embargo, debe tenerse en cuenta que si un usuario no está en línea cuando el servidor activa una notificación automática destinada a él, aún debe recibir esa notificación una vez que vuelva a estar en línea. El servidor tendría que encargarse de tales escenarios, además de enviar miles de solicitudes a los usuarios. Un servidor que realiza un seguimiento de la conexión del usuario suena complicado. Entonces, algo en el medio sería responsable de enrutar las notificaciones push web desde el servidor al cliente. Esto se denomina servicio de inserción y cada navegador tiene su propia implementación de un servicio de inserción. El navegador debe proporcionar la siguiente información al servicio push para poder enviar cualquier notificación:
- el tiempo de vivir
Este es el tiempo que debe permanecer en cola un mensaje, en caso de que no se entregue al usuario. Una vez transcurrido este tiempo, el mensaje se eliminará de la cola. - Urgencia del mensaje.
Esto es para que el servicio push conserve la batería del usuario enviando solo mensajes de alta prioridad.
El servicio push enruta los mensajes al cliente. Debido a que el cliente debe recibir el envío, incluso si su aplicación web respectiva no está abierta en el navegador, los eventos de envío deben ser escuchados por algo que monitorea continuamente en segundo plano. Lo has adivinado: ese es el trabajo del trabajador de servicio. El trabajador del servicio escucha los eventos de inserción y hace el trabajo de mostrar notificaciones al usuario.
Entonces, ahora sabemos que el navegador, el servicio push, el trabajador del servicio y el servidor de aplicaciones funcionan en armonía para enviar notificaciones push al usuario. Veamos los detalles de implementación.
Cliente de inserción web
Pedir permiso al usuario es una cosa de una sola vez. Si un usuario ya ha otorgado permiso para recibir notificaciones automáticas, no deberíamos volver a preguntar. El valor del permiso se guarda en Notification.permission
.

/* Notification.permission can have one of these three values: default, granted or denied. */ if (Notification.permission === 'default') { /* The Notification.requestPermission() method shows a notification permission prompt to the user. It returns a promise that resolves to the value of permission*/ Notification.requestPermission().then (result => { if (result === 'denied') { console.log('Permission denied') return } if (result === 'granted') { console.log('Permission granted') /* This means the user has clicked the Allow button. We're to get the subscription token generated by the browser and store it in our database. The subscription token can be fetched using the getSubscription method available on pushManager of the serviceWorkerRegistration object. If subscription is not available, we subscribe using the subscribe method available on pushManager. The subscribe method takes in an object. */ serviceWorkerRegistration.pushManager.getSubscription() .then (subscription => { if (!subscription) { const applicationServerKey = ' ' serviceWorkerRegistration.pushManager.subscribe({ userVisibleOnly: true, // All push notifications from server should be displayed to the user applicationServerKey // VAPID Public key }) } else { saveSubscriptionInDB(subscription, userId) // A method to save subscription token in the database } }) } }) }
/* Notification.permission can have one of these three values: default, granted or denied. */ if (Notification.permission === 'default') { /* The Notification.requestPermission() method shows a notification permission prompt to the user. It returns a promise that resolves to the value of permission*/ Notification.requestPermission().then (result => { if (result === 'denied') { console.log('Permission denied') return } if (result === 'granted') { console.log('Permission granted') /* This means the user has clicked the Allow button. We're to get the subscription token generated by the browser and store it in our database. The subscription token can be fetched using the getSubscription method available on pushManager of the serviceWorkerRegistration object. If subscription is not available, we subscribe using the subscribe method available on pushManager. The subscribe method takes in an object. */ serviceWorkerRegistration.pushManager.getSubscription() .then (subscription => { if (!subscription) { const applicationServerKey = ' ' serviceWorkerRegistration.pushManager.subscribe({ userVisibleOnly: true, // All push notifications from server should be displayed to the user applicationServerKey // VAPID Public key }) } else { saveSubscriptionInDB(subscription, userId) // A method to save subscription token in the database } }) } }) }
En el método de subscribe
anterior, pasamos userVisibleOnly
y applicationServerKey
para generar un token de suscripción. La propiedad userVisibleOnly
siempre debe ser verdadera porque le dice al navegador que cualquier notificación push enviada por el servidor se mostrará al cliente. Para comprender el propósito de applicationServerKey
, consideremos un escenario.
Si alguna persona obtiene sus miles de tokens de suscripción, muy bien podría enviar notificaciones a los puntos finales contenidos en estas suscripciones. No hay forma de que el punto final se vincule a su identidad única. Para proporcionar una identidad única a los tokens de suscripción generados en su aplicación web, utilizamos el protocolo VAPID. Con VAPID, el servidor de aplicaciones se identifica voluntariamente con el servicio push mientras envía notificaciones push. Generamos dos claves así:
const webpush = require('web-push') const vapidKeys = webpush.generateVAPIDKeys()
web-push es un módulo npm. vapidKeys
tendrá una clave pública y una clave privada. La clave del servidor de aplicaciones utilizada anteriormente es la clave pública.
Servidor web push
El trabajo del servidor push web (servidor de aplicaciones) es sencillo. Envía una carga útil de notificación a los tokens de suscripción.
const options = { TTL: 24*60*60, //TTL is the time to live, the time that the notification will be queued in the push service vapidDetails: { subject: '[email protected]', publicKey: ' ', privateKey: ' ' } } const data = { title: 'Update', body: 'Notification sent by the server' } webpush.sendNotification(subscription, data, options)
const options = { TTL: 24*60*60, //TTL is the time to live, the time that the notification will be queued in the push service vapidDetails: { subject: '[email protected]', publicKey: ' ', privateKey: ' ' } } const data = { title: 'Update', body: 'Notification sent by the server' } webpush.sendNotification(subscription, data, options)
const options = { TTL: 24*60*60, //TTL is the time to live, the time that the notification will be queued in the push service vapidDetails: { subject: '[email protected]', publicKey: ' ', privateKey: ' ' } } const data = { title: 'Update', body: 'Notification sent by the server' } webpush.sendNotification(subscription, data, options)
Utiliza el método sendNotification
de la biblioteca web push.
Trabajadores de servicios
El trabajador del servicio muestra la notificación al usuario como tal:
self.addEventListener('push', (event) => { let options = { body: event.data.body, icon: 'images/example.png', } event.waitUntil( /* The showNotification method is available on the registration object of the service worker. The first parameter to showNotification method is the title of notification, and the second parameter is an object */ self.registration.showNotification(event.data.title, options) ) })
Hasta ahora, hemos visto cómo un trabajador del servicio utiliza el caché para almacenar solicitudes y hace que una PWA sea rápida y confiable, y hemos visto cómo las notificaciones push web mantienen a los usuarios interesados.
Para almacenar una gran cantidad de datos en el lado del cliente para soporte fuera de línea, necesitamos una estructura de datos gigante. Echemos un vistazo a la PWA del Financial Times. Tienes que ser testigo del poder de esta estructura de datos por ti mismo. Cargue la URL en su navegador y luego apague su conexión a Internet. Vuelva a cargar la página. ¡Gah! ¿Sigue funcionando? Está. (Como dije, fuera de línea es el nuevo negro). Los datos no provienen de los cables. Se está sirviendo desde la casa. Dirígete a la pestaña "Aplicaciones" de las Herramientas para desarrolladores de Chrome. En "Almacenamiento", encontrará "IndexedDB".

Echa un vistazo a la tienda de objetos "Artículos" y expande cualquiera de los artículos para ver la magia por ti mismo. The Financial Times ha almacenado estos datos para soporte fuera de línea. Esta estructura de datos que nos permite almacenar una gran cantidad de datos se llama IndexedDB. IndexedDB es una base de datos orientada a objetos basada en JavaScript para almacenar datos estructurados. Podemos crear diferentes almacenes de objetos en esta base de datos para varios propósitos. Por ejemplo, como podemos ver en la imagen de arriba, "Recursos", "Imágenes de artículos" y "Artículos" se denominan almacenes de objetos. Cada registro en un almacén de objetos se identifica de forma única con una clave. IndexedDB incluso se puede usar para almacenar archivos y blobs.
Tratemos de entender IndexedDB creando una base de datos para almacenar libros.
let openIdbRequest = window.indexedDB.open('booksdb', 1)
Si la base de datos booksdb
aún no existe, el código anterior creará una base de datos booksdb
. El segundo parámetro del método abierto es la versión de la base de datos. La especificación de una versión se encarga de los cambios relacionados con el esquema que pueden ocurrir en el futuro. Por ejemplo, booksdb
ahora tiene solo una tabla, pero cuando la aplicación crezca, tenemos la intención de agregarle dos tablas más. Para asegurarnos de que nuestra base de datos esté sincronizada con el esquema actualizado, especificaremos una versión superior a la anterior.
Llamar al método open
no abre la base de datos de inmediato. Es una solicitud asíncrona que devuelve un objeto IDBOpenDBRequest
. Este objeto tiene propiedades de éxito y error; tendremos que escribir controladores apropiados para estas propiedades para administrar el estado de nuestra conexión.
let dbInstance openIdbRequest.onsuccess = (event) => { dbInstance = event.target.result console.log('booksdb is opened successfully') } openIdbRequest.onerror = (event) => { console.log('There was an error in opening booksdb database') } openIdbRequest.onupgradeneeded = (event) => { let db = event.target.result let objectstore = db.createObjectStore('books', { keyPath: 'id' }) }
Para administrar la creación o modificación de almacenes de objetos (los almacenes de objetos son análogos a las tablas basadas en SQL; tienen una estructura de clave-valor), se llama al método onupgradeneeded
en el objeto openIdbRequest
. El método onupgradeneeded
se invocará cada vez que cambie la versión. En el fragmento de código anterior, estamos creando una tienda de objetos de libros con una clave única como ID.
Digamos que, después de implementar este fragmento de código, tenemos que crear un almacén de objetos más, llamado users
. Entonces, ahora la versión de nuestra base de datos será 2
.
let openIdbRequest = window.indexedDB.open('booksdb', 2) // New Version - 2 /* Success and error event handlers remain the same. The onupgradeneeded method gets called when the version of the database changes. */ openIdbRequest.onupgradeneeded = (event) => { let db = event.target.result if (!db.objectStoreNames.contains('books')) { let objectstore = db.createObjectStore('books', { keyPath: 'id' }) } let oldVersion = event.oldVersion let newVersion = event.newVersion /* The users tables should be added for version 2. If the existing version is 1, it will be upgraded to 2, and the users object store will be created. */ if (oldVersion === 1) { db.createObjectStore('users', { keyPath: 'id' }) } }
Hemos almacenado en caché dbInstance
en el controlador de eventos de éxito de la solicitud abierta. Para recuperar o agregar datos en IndexedDB, utilizaremos dbInstance
. Agreguemos algunos registros de libros en nuestra tienda de objetos de libros.
let transaction = dbInstance.transaction('books') let objectstore = transaction.objectstore('books') let bookRecord = { id: '1', name: 'The Alchemist', author: 'Paulo Coelho' } let addBookRequest = objectstore.add(bookRecord) addBookRequest.onsuccess = (event) => { console.log('Book record added successfully') } addBookRequest.onerror = (event) => { console.log('There was an error in adding book record') }
Hacemos uso de transactions
, especialmente al escribir registros en almacenes de objetos. Una transacción es simplemente un envoltorio de una operación para garantizar la integridad de los datos. Si alguna de las acciones de una transacción falla, no se realiza ninguna acción en la base de datos.
Modifiquemos un registro contable con el método put
:
let modifyBookRequest = objectstore.put(bookRecord) // put method takes in an object as the parameter modifyBookRequest.onsuccess = (event) => { console.log('Book record updated successfully') }
Recuperemos un registro de libro con el método get
:
let transaction = dbInstance.transaction('books') let objectstore = transaction.objectstore('books') /* get method takes in the id of the record */ let getBookRequest = objectstore.get(1) getBookRequest.onsuccess = (event) => { /* event.target.result contains the matched record */ console.log('Book record', event.target.result) } getBookRequest.onerror = (event) => { console.log('Error while retrieving the book record.') }
Agregar icono en la pantalla de inicio
Ahora que apenas hay distinción entre una PWA y una aplicación nativa, tiene sentido ofrecer una posición privilegiada a la PWA. Si su sitio web cumple con los criterios básicos de un PWA (alojado en HTTPS, se integra con los trabajadores del servicio y tiene un manifest.json
) y después de que el usuario haya pasado algún tiempo en la página web, el navegador invocará un aviso en la parte inferior, preguntando el usuario para agregar la aplicación a su pantalla de inicio, como se muestra a continuación:

Cuando un usuario hace clic en "Agregar FT a la pantalla de inicio", la PWA puede poner su pie en la pantalla de inicio, así como en el cajón de la aplicación. Cuando un usuario busca cualquier aplicación en su teléfono, se enumerarán las PWA que coincidan con la consulta de búsqueda. También se verán en la configuración del sistema, lo que facilita que los usuarios los administren. En este sentido, una PWA se comporta como una aplicación nativa.
Los PWA utilizan manifest.json
para proporcionar esta función. Veamos un archivo manifest.json
simple.
{ "name": "Demo PWA", "short_name": "Demo", "start_url": "/?standalone", "background_color": "#9F0C3F", "theme_color": "#fff1e0", "display": "standalone", "icons": [{ "src": "/lib/img/icons/xxhdpi.png?v2", "sizes": "192x192" }] }
El short_name
aparece en la pantalla de inicio del usuario y en la configuración del sistema. El name
aparece en el indicador de Chrome y en la pantalla de inicio. La pantalla de inicio es lo que el usuario ve cuando la aplicación se está preparando para iniciarse. start_url
es la pantalla principal de su aplicación. Es lo que obtienen los usuarios cuando tocan un ícono en la pantalla de inicio. El background_color
se usa en la pantalla de presentación. El theme_color
establece el color de la barra de herramientas. El valor standalone
para el modo de display
dice que la aplicación debe operarse en modo de pantalla completa (ocultando la barra de herramientas del navegador). Cuando un usuario instala una PWA, su tamaño es simplemente en kilobytes, en lugar de los megabytes de las aplicaciones nativas.
Los trabajadores de servicios, las notificaciones push web, IndexedDB y la posición de la pantalla de inicio compensan el soporte, la confiabilidad y el compromiso fuera de línea. Cabe señalar que un trabajador de servicio no cobra vida y comienza a hacer su trabajo en la primera carga. La primera carga seguirá siendo lenta hasta que todos los activos estáticos y otros recursos se hayan almacenado en caché. Podemos implementar algunas estrategias para optimizar la primera carga.
Agrupación de activos
Todos los recursos, incluido el HTML, las hojas de estilo, las imágenes y JavaScript, deben obtenerse del servidor. Cuantos más archivos, más solicitudes HTTPS se necesitan para obtenerlos. Podemos usar paquetes como WebPack para agrupar nuestros activos estáticos, lo que reduce la cantidad de solicitudes HTTP al servidor. WebPack hace un gran trabajo al optimizar aún más el paquete mediante el uso de técnicas como la división de código (es decir, agrupar solo los archivos que se requieren para la carga de la página actual, en lugar de agruparlos todos juntos) y sacudir el árbol (es decir, eliminar dependencias duplicadas o dependencias que se importan pero no se utilizan en el código).
Reducción de viajes de ida y vuelta
Una de las principales razones de la lentitud en la web es la latencia de la red. El tiempo que tarda un byte en viajar de A a B varía según la conexión de red. Por ejemplo, un viaje de ida y vuelta en particular a través de Wi-Fi toma 50 milisegundos y 500 milisegundos en una conexión 3G, pero 2500 milisegundos en una conexión 2G. Estas solicitudes se envían mediante el protocolo HTTP, lo que significa que mientras se utiliza una conexión particular para una solicitud, no se puede utilizar para ninguna otra solicitud hasta que se atienda la respuesta de la solicitud anterior. Un sitio web puede realizar seis solicitudes HTTP asíncronas a la vez porque hay seis conexiones disponibles para un sitio web para realizar solicitudes HTTP. Un sitio web promedio realiza aproximadamente 100 solicitudes; por lo tanto, con un máximo de seis conexiones disponibles, un usuario podría terminar gastando alrededor de 833 milisegundos en un solo viaje de ida y vuelta. (El cálculo es 833 milisegundos - 100 ⁄ 6 = 1666 . Tenemos que dividir 1666 entre 2 porque estamos calculando el tiempo dedicado a un viaje de ida y vuelta). Con HTTP2 en su lugar, el tiempo de respuesta se reduce drásticamente. HTTP2 no bloquea el cabezal de conexión, por lo que se pueden enviar varias solicitudes simultáneamente.
La mayoría de las respuestas HTTP contienen encabezados etag
y last-modified
. El encabezado last-modified
es la fecha en que se modificó por última vez el archivo, y un etag
es un valor único basado en el contenido del archivo. Solo se cambiará cuando se cambie el contenido de un archivo. Ambos encabezados se pueden usar para evitar descargar el archivo nuevamente si una versión en caché ya está disponible localmente. Si el navegador tiene una versión de este archivo disponible localmente, puede agregar cualquiera de estos dos encabezados en la solicitud como tal:

El servidor puede verificar si el contenido del archivo ha cambiado. If the contents of the file have not changed, then it responds with a status code of 304 ( not modified ).

This indicates to the browser to use the locally available cached version of the file. By doing all of this, we've prevented the file from being downloaded.
Faster responses are in now place, but our job is not done yet. We still have to parse the HTML, load the style sheets and make the web page interactive. It makes sense to show some empty boxes with a loader to the user, instead of a blank screen. While the HTML document is getting parsed, when it comes across <script src='asset.js'></script>
, it will make a synchronous HTTP request to the server to fetch asset.js
, and the whole parsing process will be paused until the response comes back. Imagine having a dozen of synchronous static asset references. These could very well be managed just by making use of the async
keyword in script references, like <script src='asset.js' async></script>
. With the introduction of the async
keyword here, the browser will make an asynchronous request to fetch asset.js
without hindering the parsing of the HTML. If a script file is required at a later stage, we can defer the downloading of that file until the entire HTML has been parsed. A script file can be deferred by using the defer
keyword, like <script src='asset.js' defer></script>
.
Conclusión
We've learned a lot of many new things that make for a cool web application. Here's a summary of all of the things we've explored in this article:
- Service workers make good use of the cache to speed up the loading of assets.
- Web push notifications work under the hood.
- We use IndexedDB to store a massive amount of data.
- Some of the optimizations for instant first load, like using HTTP2 and adding headers like
Etag
,last-modified
andIf-None-Match
, prevent the downloading of valid cached assets.
That's all, folks!