Aprovechando la API de Notificación de Badges en PWAs: Iconos con Información al Instante
Descubre cómo la API de Notificación de Badges puede transformar la interacción del usuario en tus PWAs. Este tutorial te guiará paso a paso para implementar insignias numéricas y de punto en el icono de tu aplicación, mejorando el re-engagement y la visibilidad de información importante de un vistazo. Ideal para notificaciones silenciosas y actualizaciones de estado.
🚀 Introducción a la API de Notificación de Badges en PWAs
Las Progressive Web Apps (PWAs) continúan cerrando la brecha entre las aplicaciones web y las nativas, ofreciendo experiencias de usuario enriquecidas y capacidades avanzadas. Una de estas capacidades, crucial para mantener a los usuarios informados y comprometidos, es la API de Notificación de Badges (Badging API).
Tradicionalmente, las aplicaciones nativas han utilizado insignias (badges) en sus iconos para indicar nuevas notificaciones, mensajes no leídos o estados importantes. Piensa en el pequeño círculo rojo con un número que aparece en el icono de tu aplicación de correo o mensajería. Con la Badging API, las PWAs pueden lograr esta misma funcionalidad, proporcionando una señal visual directa al usuario sobre información relevante, incluso cuando la aplicación no está en primer plano.
Esta API permite a los desarrolladores establecer un "badge" en el icono de la aplicación en la pantalla de inicio o en la barra de tareas del sistema operativo, ya sea un número que representa un contador (por ejemplo, mensajes no leídos) o un punto genérico para indicar la existencia de alguna novedad. Esto es particularmente útil para notificaciones "silenciosas" que no necesitan una interrupción completa con una notificación push, pero que aún requieren visibilidad.
En este tutorial, exploraremos a fondo la Badging API: qué es, cómo funciona, sus ventajas y, lo más importante, cómo implementarla paso a paso en tu PWA para mejorar significativamente la experiencia de tus usuarios.
🎯 ¿Qué es la API de Notificación de Badges?
La Badging API es una interfaz web que permite a las Progressive Web Apps modificar el icono de la aplicación en la plataforma del usuario (escritorio o móvil) para mostrar un indicador visual. Este indicador puede ser:
- Un número: Generalmente un contador que indica la cantidad de elementos nuevos o pendientes (ej. 5 mensajes nuevos).
- Un punto: Un simple indicador visual que señala que hay algo nuevo o que requiere atención, sin especificar una cantidad.
Esta funcionalidad se expone a través de navigator.setAppBadge() y navigator.clearAppBadge(). Opera en el contexto de tu PWA y es controlada por el Service Worker, lo que le permite funcionar incluso cuando la aplicación no está activa o el navegador está cerrado (siempre que el Service Worker esté activo).
✅ Beneficios de Usar Badges
La implementación de insignias en tu PWA ofrece varias ventajas clave:
- Mayor visibilidad: Los usuarios pueden ver la información importante de un vistazo sin abrir la aplicación.
- Re-engagement mejorado: Las insignias actúan como un recordatorio visual sutil para que los usuarios vuelvan a interactuar con la aplicación.
- Notificaciones no intrusivas: A diferencia de las Notificaciones Push, las insignias son menos disruptivas, ideales para actualizaciones que no requieren atención inmediata pero son útiles.
- Consistencia con aplicaciones nativas: Ofrece una experiencia más similar a la de las aplicaciones nativas, haciendo que tu PWA se sienta más integrada en el sistema operativo.
- Contexto de un vistazo: Proporciona un contexto rápido sobre el estado de la aplicación (ej. número de elementos en el carrito, notificaciones pendientes).
⚠️ Consideraciones de Compatibilidad y Permisos
Antes de sumergirnos en la implementación, es crucial entender la compatibilidad y los permisos. Actualmente, la Badging API es ampliamente soportada por navegadores basados en Chromium (Chrome, Edge, Opera, Brave) en Android, Windows, macOS y Linux. Otros navegadores pueden tener soporte limitado o nulo. Siempre es buena práctica realizar una detección de características.
Permisos: A diferencia de las Notificaciones Push, la Badging API no requiere un permiso explícito del usuario para ser utilizada. Sin embargo, para que funcione, la PWA debe estar instalada en la pantalla de inicio del usuario (como un acceso directo o mediante "Add to Home Screen"). Si la PWA no está instalada, las llamadas a setAppBadge o clearAppBadge serán ignoradas o generarán errores.
// Detección de soporte para la Badging API
if ('setAppBadge' in navigator) {
console.log('La Badging API es compatible con este navegador.');
} else {
console.log('La Badging API NO es compatible con este navegador.');
}
🛠️ Requisitos Previos para Implementar la Badging API
Para poder utilizar la Badging API en tu PWA, necesitas cumplir con los siguientes requisitos:
- Tu aplicación debe ser una PWA: Esto significa que debe tener un manifiesto de aplicación web (
manifest.json) y un Service Worker registrado. - El Service Worker debe estar activo: Las operaciones de badge suelen realizarse desde el Service Worker para asegurar que funcionen incluso cuando la aplicación no está en primer plano.
- La PWA debe estar instalada: El usuario debe haber añadido la PWA a su pantalla de inicio (
Add to Home ScreenoInstalar Aplicación). Sin la instalación, la API no tendrá efecto en el icono del sistema operativo. - Contexto seguro (HTTPS): Como la mayoría de las APIs modernas de la web, la Badging API solo funciona en contextos seguros (servido a través de HTTPS o localhost).
📄 Manifest.json Básico
Aquí tienes un ejemplo de un manifest.json mínimo para tu PWA:
{
"name": "Mi PWA con Badges",
"short_name": "PWA Badges",
"description": "Una PWA demostrando la Badging API",
"start_url": "/",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#007bff",
"icons": [
{
"src": "./images/icon-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "./images/icon-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
]
}
Asegúrate de enlazar este manifiesto en tu index.html:
<link rel="manifest" href="/manifest.json">
👷 Service Worker Básico
Un service-worker.js básico que se encargará de registrar el Service Worker y de donde podremos manejar las notificaciones:
// service-worker.js
const CACHE_NAME = 'pwa-badge-cache-v1';
const urlsToCache = [
'/',
'/index.html',
'/styles.css',
'/app.js',
'/manifest.json',
'/images/icon-192x192.png',
'/images/icon-512x512.png'
];
self.addEventListener('install', event => {
event.waitUntil(
caches.open(CACHE_NAME)
.then(cache => {
console.log('Opened cache');
return cache.addAll(urlsToCache);
})
);
});
self.addEventListener('fetch', event => {
event.respondWith(
caches.match(event.request)
.then(response => {
if (response) {
return response;
}
return fetch(event.request);
})
);
});
self.addEventListener('activate', event => {
console.log('Service Worker activado.');
});
// Aquí se añadirán los manejadores para badges y mensajes
Y regístralo en tu app.js (o directamente en index.html):
// app.js
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/service-worker.js')
.then(registration => {
console.log('Service Worker registrado con éxito:', registration);
})
.catch(error => {
console.error('Fallo en el registro del Service Worker:', error);
});
});
}
📝 Implementación de la Badging API: Paso a Paso
La implementación de la Badging API se divide principalmente en dos funciones: setAppBadge() para establecer la insignia y clearAppBadge() para eliminarla. Estas funciones pueden ser llamadas tanto desde el contexto de la ventana principal de la PWA como desde el Service Worker.
1. Estableciendo una Insignia Numérica 🔢
Para mostrar un número específico en el icono de tu PWA, utiliza navigator.setAppBadge(count), donde count es un número entero positivo. Si count es 0, la insignia se elimina.
Desde el Contexto de la Ventana Principal (Página HTML):
Esto es útil para actualizar la insignia en respuesta a acciones del usuario o datos que se cargan mientras la aplicación está abierta.
// app.js o en un script dentro de index.html
async function updateBadge(count) {
if ('setAppBadge' in navigator) {
try {
await navigator.setAppBadge(count);
console.log(`Insignia establecida a: ${count}`);
} catch (error) {
console.error('Error al establecer la insignia:', error);
}
} else {
console.warn('La API de Badging no es compatible.');
}
}
// Ejemplo de uso:
document.getElementById('set-badge-button').addEventListener('click', () => {
const newCount = parseInt(document.getElementById('badge-input').value, 10);
if (!isNaN(newCount) && newCount >= 0) {
updateBadge(newCount);
}
});
Para esto, tu index.html podría tener:
<p>
<label for="badge-input">Número de insignias:</label>
<input type="number" id="badge-input" value="5" min="0">
<button id="set-badge-button">Establecer Insignia</button>
</p>
Desde el Service Worker:
Este es el escenario más potente, ya que permite actualizar la insignia incluso cuando la aplicación está cerrada. Esto se logra generalmente en respuesta a eventos como Notificaciones Push o Sincronización en Segundo Plano. El Service Worker puede recibir mensajes de la aplicación principal o de un servidor.
// service-worker.js
// ... (código existente del Service Worker)
self.addEventListener('message', async (event) => {
if (event.data && event.data.type === 'SET_BADGE') {
const count = event.data.count;
if ('setAppBadge' in navigator) {
try {
await navigator.setAppBadge(count);
console.log(`Service Worker estableció la insignia a: ${count}`);
} catch (error) {
console.error('Service Worker: Error al establecer la insignia:', error);
}
} else {
console.warn('Service Worker: La API de Badging no es compatible.');
}
}
});
// Ejemplo: Actualizar insignia al recibir una push notification
self.addEventListener('push', event => {
const data = event.data.json();
console.log('Push received with data:', data);
if (data.badgeCount && 'setAppBadge' in navigator) {
event.waitUntil(navigator.setAppBadge(data.badgeCount));
}
const title = data.title || 'Nueva Notificación';
const options = {
body: data.body || 'Tienes una nueva actualización.',
icon: './images/icon-192x192.png'
};
event.waitUntil(self.registration.showNotification(title, options));
});
Para enviar un mensaje desde la página a tu Service Worker:
// app.js
async function sendMessageToServiceWorker(type, data) {
if (navigator.serviceWorker.controller) {
navigator.serviceWorker.controller.postMessage({ type, ...data });
} else {
console.warn('No hay Service Worker activo para enviar mensajes.');
}
}
document.getElementById('send-badge-to-sw-button').addEventListener('click', () => {
const count = parseInt(document.getElementById('badge-sw-input').value, 10);
if (!isNaN(count) && count >= 0) {
sendMessageToServiceWorker('SET_BADGE', { count });
}
});
2. Estableciendo una Insignia de Punto (Booleana) ⚫
Para mostrar un simple punto (sin número) en el icono, llama a navigator.setAppBadge() sin argumentos o con null, undefined, true, o false (donde true establecería un punto, y false/null/undefined lo eliminaría).
// app.js
async function setDotBadge() {
if ('setAppBadge' in navigator) {
try {
await navigator.setAppBadge(); // Establece un punto
console.log('Insignia de punto establecida.');
} catch (error) {
console.error('Error al establecer la insignia de punto:', error);
}
}
}
document.getElementById('set-dot-badge-button').addEventListener('click', setDotBadge);
3. Borrando la Insignia ❌
Para eliminar cualquier insignia del icono de tu aplicación, utiliza navigator.clearAppBadge().
Desde la Ventana Principal:
Ideal para cuando el usuario ha visto la información, ha leído los mensajes, etc.
// app.js
async function clearBadge() {
if ('clearAppBadge' in navigator) {
try {
await navigator.clearAppBadge();
console.log('Insignia borrada.');
} catch (error) {
console.error('Error al borrar la insignia:', error);
}
}
}
document.getElementById('clear-badge-button').addEventListener('click', clearBadge);
Desde el Service Worker:
Útil después de una sincronización, o cuando un mensaje del servidor indica que el contador debe restablecerse.
// service-worker.js
self.addEventListener('message', async (event) => {
if (event.data && event.data.type === 'CLEAR_BADGE') {
if ('clearAppBadge' in navigator) {
try {
await navigator.clearAppBadge();
console.log('Service Worker borró la insignia.');
} catch (error) {
console.error('Service Worker: Error al borrar la insignia:', error);
}
}
}
});
Para activar esto desde la página:
// app.js
document.getElementById('clear-badge-sw-button').addEventListener('click', () => {
sendMessageToServiceWorker('CLEAR_BADGE');
});
💡 Casos de Uso Comunes para la Badging API
La Badging API es increíblemente versátil y puede mejorar la UX de diversas PWAs. Aquí algunos ejemplos prácticos:
✉️ Aplicaciones de Mensajería o Correo Electrónico
- Uso: Mostrar el número de mensajes no leídos.
- Flujo: Cuando llega un nuevo mensaje (detectado por un Service Worker o por la PWA en primer plano), se incrementa el contador y se llama a
navigator.setAppBadge(nuevoContador). Al abrir la aplicación y leer los mensajes, se llama anavigator.clearAppBadge()onavigator.setAppBadge(0).
🛒 Tiendas Online (Carrito de Compras)
- Uso: Mostrar el número de ítems en el carrito de compras.
- Flujo: Cada vez que el usuario añade o quita un producto del carrito, la PWA actualiza la insignia con el nuevo total de ítems.
🔔 Centros de Notificaciones
- Uso: Indicar la cantidad de notificaciones pendientes de revisión.
- Flujo: El Service Worker recibe datos de nuevas notificaciones y actualiza el contador en la insignia. Cuando el usuario visita el centro de notificaciones en la PWA, la insignia se resetea.
✅ Listas de Tareas o Gestión de Proyectos
- Uso: Mostrar el número de tareas pendientes para hoy o tareas asignadas al usuario.
- Flujo: La PWA calcula las tareas pendientes y actualiza la insignia. Una sincronización en segundo plano puede actualizar esto incluso si la aplicación no está abierta.
🎮 Aplicaciones de Juegos
- Uso: Indicar vidas restantes, nuevos logros o mensajes de otros jugadores.
- Flujo: Dependiendo del estado del juego o de la actividad social, la PWA puede usar insignias numéricas o de punto.
🔍 Consideraciones Avanzadas y Mejores Prácticas
Para sacar el máximo provecho de la Badging API y ofrecer una experiencia de usuario óptima, ten en cuenta las siguientes consideraciones:
1. Detección de Características y Fallback
Dado que el soporte no es universal, siempre debes verificar si la API está disponible antes de intentar usarla. Si no lo está, puedes implementar un fallback (por ejemplo, depender únicamente de las Notificaciones Push).
if ('setAppBadge' in navigator) {
// Usar Badging API
} else {
// Implementar fallback o simplemente no usar insignias
}
2. Gestión del Estado Persistente
Si tu insignia representa un contador (como mensajes no leídos), este estado debe ser persistente. Si la PWA se cierra y se vuelve a abrir, la insignia debe reflejar el estado correcto. Esto puede lograrse almacenando el contador en localStorage, sessionStorage o, preferiblemente, en IndexedDB si el Service Worker necesita acceder a él directamente y mantenerlo sincronizado.
Ejemplo con IndexedDB (pseudo-código para el Service Worker):
// service-worker.js (ejemplo conceptual)
async function getBadgeCountFromDB() {
// Lógica para obtener el contador de IndexedDB
// Retorna 0 si no hay contador o si es la primera vez
}
async function saveBadgeCountToDB(count) {
// Lógica para guardar el contador en IndexedDB
}
self.addEventListener('push', async event => {
const data = event.data.json();
let currentCount = await getBadgeCountFromDB();
currentCount = (currentCount || 0) + (data.newItems || 0);
await saveBadgeCountToDB(currentCount);
if ('setAppBadge' in navigator) {
event.waitUntil(navigator.setAppBadge(currentCount));
}
// ... mostrar notificación
});
self.addEventListener('message', async (event) => {
if (event.data && event.data.type === 'CLEAR_BADGE') {
await saveBadgeCountToDB(0); // Resetear en DB
if ('clearAppBadge' in navigator) {
event.waitUntil(navigator.clearAppBadge());
}
}
});
3. Comunicación entre Ventana Principal y Service Worker
Es fundamental establecer un mecanismo de comunicación robusto entre la ventana principal de tu PWA y su Service Worker. Esto es necesario para que la interfaz de usuario pueda enviar comandos al Service Worker (ej. "borrar insignia cuando el usuario lee mensajes") y para que el Service Worker pueda actualizar la insignia de forma asíncrona.
postMessage(): El método más común para la comunicación bidireccional.- Broadcast Channel API: Para comunicación uno a muchos (una ventana a múltiples Service Workers o viceversa).
4. Coherencia Visual
Considera cómo el badge aparecerá en diferentes sistemas operativos y con diferentes temas (claro/oscuro). Aunque el navegador y el sistema operativo manejan el renderizado final, asegúrate de que el uso de badges sea intuitivo para tus usuarios.
5. Rendimiento y Eficiencia Energética
Aunque la Badging API es generalmente ligera, evita actualizar la insignia con demasiada frecuencia. Las actualizaciones constantes pueden consumir recursos y batería, especialmente si el Service Worker está constantemente "despertándose" para hacerlo. Agrupa las actualizaciones cuando sea posible.
6. Desinstalación de la PWA
Cuando un usuario desinstala tu PWA, el sistema operativo debería eliminar automáticamente cualquier insignia persistente. No obstante, es una buena práctica asegurar que tu lógica de aplicación no intente establecer insignias en un PWA desinstalada (aunque las llamadas a la API simplemente fallarán en ese caso).
7. Enfoque Offline-First
Integra la Badging API en tu estrategia offline-first. Por ejemplo, si un usuario está offline y realiza una acción que debería generar una insignia, el Service Worker podría almacenar ese cambio localmente y aplicarlo una vez que la conexión se restablezca.
🌐 Ejemplo Práctico Completo: PWA de Mensajería Simple con Badges
Vamos a construir una PWA simple que simule un sistema de mensajería para demostrar la Badging API. Tendremos:
- Una interfaz principal para enviar "mensajes" (simulados).
- Un Service Worker que gestionará los mensajes y actualizará la insignia.
- Botones para establecer, incrementar y borrar la insignia.
Estructura de Archivos:
my-badge-pwa/
├── index.html
├── style.css
├── app.js
├── service-worker.js
└── images/
├── icon-192x192.png
└── icon-512x512.png
1. index.html
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PWA Mensajería con Badges</title>
<link rel="manifest" href="/manifest.json">
<link rel="stylesheet" href="/style.css">
<link rel="icon" href="/images/icon-192x192.png">
</head>
<body>
<header>
<h1>📨 Mensajería PWA con Badges</h1>
</header>
<main>
<section class="card">
<h2>Panel de Mensajes</h2>
<p>Simula la recepción y gestión de mensajes para probar la API de Badges.</p>
<div class="controls">
<button id="receive-message-btn">Recibir Nuevo Mensaje</button>
<button id="clear-all-messages-btn">Borrar Todos los Mensajes</button>
</div>
<div id="message-count-display">Mensajes no leídos: <span id="unread-count">0</span></div>
</section>
<section class="card">
<h2>Control Manual de Insignia</h2>
<p>Puedes establecer o borrar la insignia manualmente para depuración.</p>
<div class="controls">
<input type="number" id="manual-badge-input" value="3" min="0">
<button id="set-manual-badge-btn">Establecer Manual</button>
<button id="clear-manual-badge-btn">Borrar Manual</button>
<button id="set-dot-badge-btn">Establecer Punto</button>
</div>
</section>
<section class="card" id="log-section">
<h2>Registro de Eventos</h2>
<pre id="app-log"></pre>
</section>
</main>
<script src="/app.js"></script>
</body>
</html>
2. style.css
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
margin: 0;
background-color: #f4f7f6;
color: #333;
display: flex;
flex-direction: column;
align-items: center;
min-height: 100vh;
}
header {
background-color: #007bff;
color: white;
padding: 20px;
width: 100%;
text-align: center;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
h1 {
margin: 0;
font-size: 1.8em;
}
main {
padding: 20px;
max-width: 800px;
width: 100%;
display: grid;
gap: 20px;
grid-template-columns: 1fr;
}
.card {
background-color: white;
padding: 25px;
border-radius: 8px;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}
h2 {
color: #007bff;
margin-top: 0;
border-bottom: 1px solid #eee;
padding-bottom: 10px;
margin-bottom: 20px;
}
.controls button,
.controls input[type="number"] {
padding: 10px 15px;
margin: 5px;
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 0.9em;
}
.controls button {
background-color: #28a745;
color: white;
}
.controls button:hover {
opacity: 0.9;
}
.controls #clear-all-messages-btn,
.controls #clear-manual-badge-btn {
background-color: #dc3545;
}
.controls #set-manual-badge-btn,
.controls #set-dot-badge-btn {
background-color: #ffc107;
color: #333;
}
input[type="number"] {
border: 1px solid #ccc;
width: 80px;
}
#message-count-display {
margin-top: 20px;
font-size: 1.1em;
font-weight: bold;
color: #007bff;
}
#unread-count {
color: #dc3545;
}
#log-section {
background-color: #e9ecef;
color: #000;
border-radius: 8px;
}
#app-log {
white-space: pre-wrap;
max-height: 200px;
overflow-y: auto;
background-color: #e9ecef;
padding: 10px;
border-radius: 5px;
border: 1px solid #dee2e6;
}
/* Callout styles */
.callout {
padding: 15px;
margin-bottom: 20px;
border-left: 5px solid;
border-radius: 4px;
background-color: #f8f9fa;
}
.callout.tip { border-color: #28a745; background-color: #e6ffed; }
.callout.warning { border-color: #dc3545; background-color: #ffe6e6; }
.callout.note { border-color: #007bff; background-color: #e0f2ff; }
.callout.important { border-color: #ffc107; background-color: #fff8e6; }
/* Badge styles */
.badge {
display: inline-block;
padding: .25em .4em;
font-size: 75%;
font-weight: 700;
line-height: 1;
text-align: center;
white-space: nowrap;
vertical-align: baseline;
border-radius: .25rem;
color: #fff;
margin: 0 2px;
}
.badge.green { background-color: #28aa45; }
.badge.blue { background-color: #007bff; }
.badge.red { background-color: #dc3545; }
.badge.yellow { background-color: #ffc107; color: #333; }
.badge.purple { background-color: #6f42c1; }
/* Progress bar styles */
.progress-bar {
width: 100%;
background-color: #e0e0e0;
border-radius: 5px;
overflow: hidden;
margin: 20px 0;
}
.progress-fill {
height: 25px;
line-height: 25px;
color: white;
text-align: center;
background-color: #007bff;
border-radius: 5px;
transition: width 0.5s ease-in-out;
}
/* Timeline styles */
.timeline {
position: relative;
margin: 20px 0;
padding-left: 20px;
}
.timeline::before {
content: '';
position: absolute;
left: 0;
top: 0;
width: 2px;
background-color: #ccc;
height: 100%;
}
.timeline-item {
position: relative;
margin-bottom: 20px;
padding-left: 20px;
}
.timeline-item::before {
content: '';
position: absolute;
left: -6px;
top: 0;
width: 14px;
height: 14px;
border-radius: 50%;
background-color: #007bff;
border: 2px solid white;
}
3. app.js
// app.js
let unreadMessages = 0;
const unreadCountDisplay = document.getElementById('unread-count');
const appLog = document.getElementById('app-log');
function log(message) {
const timestamp = new Date().toLocaleTimeString();
appLog.textContent += `[${timestamp}] ${message}\n`;
appLog.scrollTop = appLog.scrollHeight; // Auto-scroll to bottom
}
async function updateBadgeCount() {
if ('setAppBadge' in navigator) {
try {
await navigator.setAppBadge(unreadMessages);
log(`Insignia actualizada a: ${unreadMessages}`);
} catch (error) {
log(`Error al establecer la insignia: ${error}`);
}
} else {
log('La API de Badging no es compatible en este navegador/contexto.');
}
unreadCountDisplay.textContent = unreadMessages;
}
async function clearBadge() {
if ('clearAppBadge' in navigator) {
try {
await navigator.clearAppBadge();
log('Insignia borrada.');
} catch (error) {
log(`Error al borrar la insignia: ${error}`);
}
} else {
log('La API de Badging no es compatible para borrar.');
}
unreadMessages = 0;
unreadCountDisplay.textContent = unreadMessages;
}
async function setDotBadge() {
if ('setAppBadge' in navigator) {
try {
await navigator.setAppBadge(); // Set a dot badge
log('Insignia de punto establecida.');
} catch (error) {
log(`Error al establecer la insignia de punto: ${error}`);
}
}
}
// Service Worker Registration
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/service-worker.js')
.then(registration => {
log('Service Worker registrado con éxito: ' + registration.scope);
// Check current badge count on load if possible (e.g., from IndexedDB in SW)
// For this example, we'll initialize to 0 and rely on SW messages or direct calls
updateBadgeCount(); // Initial badge update
})
.catch(error => {
log('Fallo en el registro del Service Worker: ' + error);
});
});
// Listen for messages from Service Worker
navigator.serviceWorker.addEventListener('message', event => {
if (event.data && event.data.type === 'BADGE_UPDATED') {
unreadMessages = event.data.count;
updateBadgeCount();
log(`Mensaje del SW: Insignia actualizada a ${event.data.count}`);
}
});
} else {
log('Service Workers no son compatibles con este navegador.');
}
// Event Listeners for UI buttons
document.getElementById('receive-message-btn').addEventListener('click', () => {
unreadMessages++;
updateBadgeCount();
log('Nuevo mensaje recibido. Incrementar insignia.');
});
document.getElementById('clear-all-messages-btn').addEventListener('click', () => {
clearBadge();
log('Todos los mensajes borrados. Borrar insignia.');
});
document.getElementById('set-manual-badge-btn').addEventListener('click', () => {
const count = parseInt(document.getElementById('manual-badge-input').value, 10);
if (!isNaN(count) && count >= 0) {
unreadMessages = count;
updateBadgeCount();
log(`Insignia establecida manualmente a: ${count}`);
} else {
log('Entrada inválida para insignia manual.');
}
});
document.getElementById('clear-manual-badge-btn').addEventListener('click', () => {
clearBadge();
log('Insignia borrada manualmente.');
});
document.getElementById('set-dot-badge-btn').addEventListener('click', () => {
setDotBadge();
log('Insignia de punto establecida manualmente.');
});
// Initial badge setup on page load (can be fetched from persistence if available)
window.addEventListener('DOMContentLoaded', () => {
updateBadgeCount();
});
4. service-worker.js
// service-worker.js
const CACHE_NAME = 'pwa-badge-cache-v1';
const urlsToCache = [
'/',
'/index.html',
'/style.css',
'/app.js',
'/manifest.json',
'/images/icon-192x192.png',
'/images/icon-512x512.png'
];
let currentBadgeCount = 0; // Simulate persistent storage for badge count
async function updateBadgeInSW(count) {
if ('setAppBadge' in navigator) {
try {
await navigator.setAppBadge(count);
console.log(`SW: Insignia actualizada a: ${count}`);
} catch (error) {
console.error('SW: Error al establecer la insignia:', error);
}
} else {
console.warn('SW: La API de Badging no es compatible.');
}
}
async function clearBadgeInSW() {
if ('clearAppBadge' in navigator) {
try {
await navigator.clearAppBadge();
console.log('SW: Insignia borrada.');
} catch (error) {
console.error('SW: Error al borrar la insignia:', error);
}
} else {
console.warn('SW: La API de Badging no es compatible para borrar.');
}
}
self.addEventListener('install', event => {
console.log('SW: Instalando...');
event.waitUntil(
caches.open(CACHE_NAME)
.then(cache => {
console.log('SW: Cache abierto');
return cache.addAll(urlsToCache);
})
.then(() => self.skipWaiting())
);
});
self.addEventListener('activate', event => {
console.log('SW: Activado.');
event.waitUntil(
caches.keys().then(cacheNames => {
return Promise.all(
cacheNames.map(cacheName => {
if (cacheName !== CACHE_NAME) {
console.log('SW: Borrando cache antiguo', cacheName);
return caches.delete(cacheName);
}
return Promise.resolve();
})
);
})
.then(() => self.clients.claim())
.then(() => {
// On activation, clear any old badge
clearBadgeInSW();
currentBadgeCount = 0;
})
);
});
self.addEventListener('fetch', event => {
event.respondWith(
caches.match(event.request)
.then(response => {
if (response) {
return response;
}
return fetch(event.request);
})
);
});
// Listen for messages from the main thread (app.js)
self.addEventListener('message', async (event) => {
console.log('SW: Mensaje recibido:', event.data);
if (event.data && event.data.type === 'INCREMENT_BADGE') {
currentBadgeCount = (currentBadgeCount || 0) + 1;
await updateBadgeInSW(currentBadgeCount);
// Send update back to main thread if needed
event.source.postMessage({ type: 'BADGE_UPDATED', count: currentBadgeCount });
} else if (event.data && event.data.type === 'SET_BADGE_SW') {
currentBadgeCount = event.data.count;
await updateBadgeInSW(currentBadgeCount);
event.source.postMessage({ type: 'BADGE_UPDATED', count: currentBadgeCount });
} else if (event.data && event.data.type === 'CLEAR_BADGE_SW') {
currentBadgeCount = 0;
await clearBadgeInSW();
event.source.postMessage({ type: 'BADGE_UPDATED', count: currentBadgeCount });
}
});
// Simulate a background push notification updating the badge
self.addEventListener('push', event => {
console.log('SW: Push notification recibida.');
const data = event.data ? event.data.json() : { title: 'Nuevo Mensaje!', body: 'Tienes un mensaje no leído.', newCount: 1 };
currentBadgeCount = (currentBadgeCount || 0) + (data.newCount || 0);
// Update badge even if no visual notification is shown
event.waitUntil(
updateBadgeInSW(currentBadgeCount)
);
const title = data.title;
const options = {
body: data.body,
icon: '/images/icon-192x192.png',
badge: '/images/icon-192x192.png' // This badge is for the notification itself
};
event.waitUntil(
self.registration.showNotification(title, options)
);
});
// Example of background sync updating the badge
self.addEventListener('sync', event => {
if (event.tag === 'sync-new-messages') {
console.log('SW: Sincronización en segundo plano para nuevos mensajes.');
event.waitUntil(
(async () => {
// Simulate fetching new messages and updating badge
const newMessagesFetched = Math.floor(Math.random() * 3) + 1; // 1-3 new messages
currentBadgeCount = (currentBadgeCount || 0) + newMessagesFetched;
await updateBadgeInSW(currentBadgeCount);
// Optionally, send a message back to open clients
const clients = await self.clients.matchAll();
clients.forEach(client => client.postMessage({ type: 'BADGE_UPDATED', count: currentBadgeCount }));
})()
);
}
});
Cómo Probar el Ejemplo:
- Guarda los archivos: Coloca todos los archivos en una carpeta (
my-badge-pwa). Asegúrate de tener las imágenes de iconos en la carpetaimages/. - Sirve localmente con HTTPS: Necesitarás un servidor web local que sirva los archivos a través de HTTPS (ej.
http-server -So un servidor con certificados SSL configurados). Si no tienes uno, puedes usar VS Code con la extensión "Live Server" y habilitar "Serve HTTPS" en sus ajustes. - Instala la PWA: Abre tu PWA en Chrome o Edge. Deberías ver un botón de instalación en la barra de direcciones o en el menú del navegador. Instala la aplicación.
- Experimenta:
- Haz clic en "Recibir Nuevo Mensaje". Verás cómo
unreadMessagesse incrementa y la insignia se actualiza en el icono de tu PWA en la barra de tareas/escritorio. - Haz clic en "Borrar Todos los Mensajes". La insignia debería desaparecer.
- Usa los controles manuales para establecer un número específico o un punto.
- Cierra la ventana del navegador de la PWA pero mantén la aplicación instalada. Intenta enviar un mensaje simulado desde un Service Worker (tendrías que simular un push o background sync, que es más avanzado, pero la lógica de SW está ahí).
- Haz clic en "Recibir Nuevo Mensaje". Verás cómo
¿Por qué mi insignia no aparece?
Asegúrate de que tu PWA esté *instalada* en el sistema operativo (mediante "Add to Home Screen" o "Instalar aplicación"). La API de Badges solo funciona para aplicaciones instaladas. También, verifica la compatibilidad del navegador y que estés sirviendo la PWA a través de HTTPS.
¿Puedo cambiar el color o diseño del badge?
No directamente a través de la API. El aspecto visual de la insignia (color, fuente, posición) es controlado enteramente por el sistema operativo y el navegador, no por el desarrollador. La API solo permite establecer un número o un punto.
Conclusión ✨
La API de Notificación de Badges es una adición poderosa al conjunto de herramientas de las Progressive Web Apps, permitiendo a los desarrolladores crear experiencias de usuario que se sienten más nativas y atractivas. Al proporcionar una forma sutil pero efectiva de comunicar información importante de un vistazo, las insignias pueden mejorar el re-engagement y la usabilidad de tu PWA sin ser intrusivas.
Al integrar la Badging API con una buena gestión del estado y comunicación entre el hilo principal y el Service Worker, puedes ofrecer a tus usuarios una PWA que no solo funciona de manera excepcional, sino que también se siente como una parte integral de su sistema operativo. ¡Aprovecha esta API para que tus usuarios estén siempre informados y conectados con tu aplicación!
Tutoriales relacionados
- Asegurando la Conectividad en PWAs: Estrategias Offline-First con IndexedDBintermediate18 min
- Aprovechando la API de Compartición Web en PWAs: Comparte Contenido de Forma Nativaintermediate15 min
- Notificaciones Push en PWA: Re-engagement y Experiencia de Usuario Mejoradaintermediate15 min
- Aprovechando la API de Acceso al Sistema de Archivos en PWAs: Persistencia Avanzadaintermediate20 min
- Gestionando la Duración del Caché en PWAs: Estrategias de Expiración con Workboxintermediate15 min
Comentarios (0)
Aún no hay comentarios. ¡Sé el primero!