`Service Worker` en JavaScript

`Service Worker` en JavaScript

Este artículo explica el concepto de Service Worker en JavaScript.

Explicaremos paso a paso desde los conceptos básicos de Service Worker hasta el control de caché práctico.

YouTube Video

offline.html
 1<!DOCTYPE html>
 2<html lang="en">
 3<head>
 4  <meta charset="UTF-8">
 5  <title>Offline</title>
 6</head>
 7<body>
 8  <h1>You are offline</h1>
 9  <p>This is the offline fallback page.</p>
10</body>
11</html>
style.css
1body {
2  font-family: sans-serif;
3  background-color: #f0f0f0;
4  padding: 20px;
5}
6h1 {
7  color: #333;
8}
javascript-service-worker.html
  1<!DOCTYPE html>
  2<html lang="en">
  3<head>
  4  <meta charset="UTF-8">
  5  <title>JavaScript &amp; HTML</title>
  6  <style>
  7    * {
  8        box-sizing: border-box;
  9    }
 10
 11    body {
 12        margin: 0;
 13        padding: 1em;
 14        padding-bottom: 10em;
 15        font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
 16        background-color: #f7f9fc;
 17        color: #333;
 18        line-height: 1.6;
 19    }
 20
 21    .container {
 22        max-width: 800px;
 23        margin: 0 auto;
 24        padding: 1em;
 25        background-color: #ffffff;
 26        border: 1px solid #ccc;
 27        border-radius: 10px;
 28        box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
 29    }
 30
 31    .container-flex {
 32        display: flex;
 33        flex-wrap: wrap;
 34        gap: 2em;
 35        max-width: 1000px;
 36        margin: 0 auto;
 37        padding: 1em;
 38        background-color: #ffffff;
 39        border: 1px solid #ccc;
 40        border-radius: 10px;
 41        box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
 42    }
 43
 44    .left-column, .right-column {
 45        flex: 1 1 200px;
 46        min-width: 200px;
 47    }
 48
 49    h1, h2 {
 50        font-size: 1.2rem;
 51        color: #007bff;
 52        margin-top: 0.5em;
 53        margin-bottom: 0.5em;
 54        border-left: 5px solid #007bff;
 55        padding-left: 0.6em;
 56        background-color: #e9f2ff;
 57    }
 58
 59    button {
 60        display: block;
 61        margin: 1em auto;
 62        padding: 0.75em 1.5em;
 63        font-size: 1rem;
 64        background-color: #007bff;
 65        color: white;
 66        border: none;
 67        border-radius: 6px;
 68        cursor: pointer;
 69        transition: background-color 0.3s ease;
 70    }
 71
 72    button:hover {
 73        background-color: #0056b3;
 74    }
 75
 76    #output {
 77        margin-top: 1em;
 78        background-color: #1e1e1e;
 79        color: #0f0;
 80        padding: 1em;
 81        border-radius: 8px;
 82        min-height: 200px;
 83        font-family: Consolas, monospace;
 84        font-size: 0.95rem;
 85        overflow-y: auto;
 86        white-space: pre-wrap;
 87    }
 88
 89    .highlight {
 90        outline: 3px solid #ffc107; /* yellow border */
 91        background-color: #fff8e1;  /* soft yellow background */
 92        transition: background-color 0.3s ease, outline 0.3s ease;
 93    }
 94
 95    .active {
 96        background-color: #28a745; /* green background */
 97        color: #fff;
 98        box-shadow: 0 0 10px rgba(40, 167, 69, 0.5);
 99        transition: background-color 0.3s ease, box-shadow 0.3s ease;
100    }
101  </style>
102</head>
103<body>
104    <div class="container">
105        <h1>JavaScript Console</h1>
106        <button id="executeBtn">Execute</button>
107        <div id="output"></div>
108    </div>
109
110    <div class="container">
111        <h2>HTML Sample</h2>
112        <button id="fetchBtn">Fetch Test</button>
113    </div>
114
115    <script>
116        // Override console.log to display messages in the #output element
117        (function () {
118            // Override console.log
119            const originalLog = console.log;
120            console.log = function (...args) {
121                originalLog.apply(console, args);
122                const message = document.createElement('div');
123                message.textContent = args.map(String).join(' ');
124                output.appendChild(message);
125            };
126
127            // Override console.error
128            const originalError = console.error;
129            console.error = function (...args) {
130                originalError.apply(console, args);
131                const message = document.createElement('div');
132                message.textContent = args.map(String).join(' ');
133                message.style.color = 'red'; // Color error messages red
134                output.appendChild(message);
135            };
136        })();
137
138        document.getElementById('executeBtn').addEventListener('click', () => {
139            // Prevent multiple loads
140            if (document.getElementById('externalScript')) return;
141
142            const script = document.createElement('script');
143            script.src = 'javascript-service-worker.js';
144            script.id = 'externalScript';
145            //script.onload = () => console.log('javascript-service-worker.js loaded and executed.');
146            //script.onerror = () => console.log('Failed to load javascript-service-worker.js.');
147            document.body.appendChild(script);
148        });
149    </script>
150</body>
151</html>

Service Worker en JavaScript

Service Worker es una funcionalidad de JavaScript que se sitúa entre el navegador y la red, permitiendo el almacenamiento en caché de solicitudes y soporte sin conexión. Es una tecnología fundamental de las PWAs (Aplicaciones Web Progresivas) y brinda una experiencia similar a una aplicación nativa a las aplicaciones web.

¿Qué es un Service Worker?

Service Worker es un archivo JavaScript que se ejecuta en el hilo de fondo del navegador. Se ejecuta en un hilo separado de la página, no puede acceder a la interfaz de usuario, pero puede interceptar solicitudes de red, gestionar la caché y manejar notificaciones push.

Las características clave de un Service Worker incluyen lo siguiente:.

  • Solo funciona a través de HTTPS, excepto en localhost.
  • Utiliza una API asíncrona basada en Promesas.
  • Es orientado a eventos, utilizando eventos como install, activate, fetch y push.

Registrando un Service Worker

Primero, escribamos el código para registrar un Service Worker en el navegador.

 1if ('serviceWorker' in navigator) {
 2    window.addEventListener('load', () => {
 3        navigator.serviceWorker.register('/sw.js')
 4        .then(registration => {
 5            console.log(
 6                'Service Worker registered with scope:',
 7                registration.scope
 8            );
 9        })
10        .catch(error => {
11            console.error('Service Worker registration failed:', error);
12        });
13    });
14}

Explicación

  • Utiliza navigator.serviceWorker.register() para registrar /sw.js (el archivo del Service Worker).
  • Puedes usar then para manejar el éxito y catch para manejar errores durante el registro.
  • registration.scope representa el rango de rutas (alcance) afectadas por el Service Worker.
  • Por defecto, el alcance es el directorio donde se encuentra el archivo registrado (en este caso, /sw.js) y sus subdirectorios.

Alcance del Service Worker

Si deseas limitar el alcance, puedes especificar el scope usando el segundo argumento de register.

1navigator.serviceWorker.register('/sw.js', { scope: '/app/' })
2.then(registration => {
3    console.log(
4        'Service Worker registered with scope:',
5        registration.scope
6    );
7});

Explicación

  • Con esta configuración, solo las páginas bajo /app/ serán controladas por el Service Worker.

Creando el archivo Service Worker

A continuación, crea un archivo llamado sw.js e implementa los eventos básicos.

1// sw.js
2const CACHE_NAME = 'my-cache-v1';
3const urlsToCache = [
4    '/',
5    '/index.html',
6    '/styles.css',
7    '/script.js',
8    '/offline.html'
9];

Este código define una lista de recursos que se deben almacenar en caché.

Funciones y mecanismos de cada evento

install

 1// Install event (initial caching)
 2self.addEventListener('install', event => {
 3    console.log('[ServiceWorker] Install');
 4    event.waitUntil(
 5        caches.open(CACHE_NAME).then(cache => {
 6            console.log('[ServiceWorker] Caching app shell');
 7            return cache.addAll(urlsToCache);
 8        })
 9    );
10});
  • self.addEventListener('install') se activa cuando el Service Worker se registra por primera vez. En esta etapa, los archivos necesarios se almacenan previamente en caché.

activate

 1// Activation event (delete old caches)
 2self.addEventListener('activate', event => {
 3    console.log('[ServiceWorker] Activate');
 4    event.waitUntil(
 5        caches.keys().then(keyList => {
 6            return Promise.all(keyList.map(key => {
 7                if (key !== CACHE_NAME) {
 8                    console.log('[ServiceWorker] Removing old cache:', key);
 9                    return caches.delete(key);
10                }
11            }));
12        })
13    );
14    return self.clients.claim();
15});
  • En el evento activate, las cachés antiguas se eliminan para optimizar el almacenamiento. Solo se mantiene la caché de la nueva versión.

fetch

1// Fetch event (cache-first strategy)
2self.addEventListener('fetch', event => {
3  console.log('[ServiceWorker] Fetch', event.request.url);
4    event.respondWith(
5        caches.match(event.request).then(response => {
6            return response || fetch(event.request).catch(() => caches.match('/offline.html'));
7        })
8    );
9});

Todas las solicitudes HTTP son interceptadas: si existe una versión en caché, se devuelve; de lo contrario, se obtiene de la red. Cuando no hay conexión, se devuelve una página alternativa (por ejemplo, offline.html).

Confirmando la operación

Vamos a comprobar realmente cómo funciona el Service Worker.

 1document.getElementById('fetchBtn').addEventListener('click', () => {
 2    fetch('/style.css')
 3        .then(response => response.text())
 4        .then(data => {
 5            console.log('Fetched data:', data);
 6        })
 7        .catch(error => {
 8            console.error('Fetch failed:', error);
 9        });
10});
  • Aquí, comprobamos el registro del Service Worker y el comportamiento al obtener recursos haciendo clic en el botón de prueba.

Ejemplos de estrategias de caché

Las siguientes son estrategias comunes de almacenamiento en caché:.

Primero la caché

Aquí tienes un ejemplo de implementación para la estrategia Cache First:.

1self.addEventListener('fetch', event => {
2    event.respondWith(
3        caches.match(event.request).then(response => {
4            return response || fetch(event.request);
5        })
6    );
7});
  • Este código implementa una estrategia de cache primero, donde el recurso solicitado se devuelve desde la caché si está disponible; si no, se obtiene de la red.

Primero la red

Aquí tienes un ejemplo de implementación para la estrategia Network First:.

 1self.addEventListener('fetch', event => {
 2    event.respondWith(
 3        fetch(event.request)
 4            .then(response => {
 5                return caches.open(CACHE_NAME).then(cache => {
 6                    cache.put(event.request, response.clone());
 7                    return response;
 8                });
 9            })
10            .catch(() => caches.match(event.request))
11    );
12});
  • Este código implementa una estrategia de red primero, donde el recurso solicitado se obtiene primero de la red, y si eso falla, se recupera desde la caché.

Solo cachear estilos y JavaScript, acceder a las APIs en tiempo real

Aquí tienes un ejemplo de implementación donde los estilos y JavaScript se almacenan en caché mientras que las APIs se acceden en tiempo real:.

 1self.addEventListener('fetch', event => {
 2    if (event.request.url.includes('/api/')) {
 3        // Fetch API responses in real-time without caching
 4        return;
 5    }
 6
 7    // Use cache-first strategy for static files
 8    event.respondWith(
 9        caches.match(event.request).then(response => {
10            return response || fetch(event.request);
11        })
12    );
13});
  • Este código siempre accede a las solicitudes de API en tiempo real y aplica una estrategia de cache primero a archivos estáticos como hojas de estilo y JavaScript.

Flujo de actualización

El flujo de actualización de un Service Worker es el siguiente:.

  1. Se detecta un nuevo sw.js.
  2. Se dispara el evento install.
  3. Espera hasta que el Service Worker anterior quede inactivo.
  4. Se dispara el evento activate.
  5. Se cambia al nuevo Service Worker.
  6. Se dispara el evento controllerchange.

Detección de actualizaciones

Una vez que un Service Worker está instalado, el anterior se sigue utilizando hasta la próxima visita. Para aplicar actualizaciones, es común utilizar código que detecta actualizaciones y recarga la página.

1navigator.serviceWorker.addEventListener('controllerchange', () => {
2    window.location.reload();
3});
  • El evento controllerchange se dispara cuando el controlador del Service Worker, es decir, el Service Worker que controla la página actual, cambia.
  • Las páginas que ya están abiertas continúan utilizando el Service Worker actual, y el nuevo Service Worker instalado no tiene efecto inmediato en esas páginas. Por lo tanto, se utiliza una técnica en la que el evento controllerchange se utiliza para detectar que un nuevo controlador se ha activado, y luego la página se recarga para aplicar la actualización de inmediato.

Precauciones y buenas prácticas

Al usar Service Worker, ten en cuenta lo siguiente:.

  • Se requiere HTTPS Por restricciones de seguridad, no funciona sobre http:// excepto en localhost.

  • Nombres de archivos con hash El nombre de la caché puede incluir el nombre del archivo, la URL y la información de la versión.

  • Comunicación con clientes Utiliza postMessage para comunicarte entre el Service Worker y el JavaScript de la página.

Resumen

Service Worker es una tecnología esencial para el soporte sin conexión y la mejora de rendimiento en las aplicaciones web. Al comprender el flujo básico de instalación, activación y manejo de 'fetch', e implementar estrategias de caché adecuadas, puedes construir aplicaciones web de mayor calidad.

Puedes seguir el artículo anterior utilizando Visual Studio Code en nuestro canal de YouTube. Por favor, también revisa nuestro canal de YouTube.

YouTube Video