Habilitar el soporte de service worker implica algo más que registrarlo; también proporciona servicios que puedes usar para interactuar con el service worker y controlar la caché de tu aplicación.
Servicio SwUpdate
El servicio SwUpdate te da acceso a eventos que indican cuándo el service worker descubre e instala una actualización disponible para tu aplicación.
El servicio SwUpdate admite tres operaciones diferentes:
- Recibir notificaciones cuando se detecta una versión actualizada en el servidor, cuando se instala y está lista para usarse localmente o cuando una instalación falla.
- Pedirle al service worker que verifique en el servidor si hay nuevas actualizaciones.
- Pedirle al service worker que active la versión más reciente de la aplicación para la pestaña actual.
Actualizaciones de versión
versionUpdates es una propiedad Observable de SwUpdate y emite cinco tipos de eventos:
| Tipos de eventos | Detalles |
|---|---|
VersionDetectedEvent |
Se emite cuando el service worker detecta una nueva versión de la aplicación en el servidor y está a punto de comenzar a descargarla. |
NoNewVersionDetectedEvent |
Se emite cuando el service worker verifica la versión de la aplicación en el servidor y no encuentra una versión nueva. |
VersionReadyEvent |
Se emite cuando hay una nueva versión disponible para que los clientes la activen. Puede usarse para notificar a la persona usuaria que hay una actualización disponible o para pedirle que actualice la página. |
VersionInstallationFailedEvent |
Se emite cuando la instalación de una nueva versión falla. Puede utilizarse con fines de registro o monitoreo. |
VersionFailedEvent |
Se emite cuando una versión encuentra una falla crítica (como errores de hashes rotos) que afecta a todas las personas usuarias que utilizan esa versión. Proporciona detalles del error para depuración y transparencia. |
log-update.service.ts
import {inject, Injectable} from '@angular/core';import {SwUpdate, VersionReadyEvent} from '@angular/service-worker';@Injectable({providedIn: 'root'})export class LogUpdateService { private updates = inject(SwUpdate); constructor() { this.updates.versionUpdates.subscribe((evt) => { switch (evt.type) { case 'VERSION_DETECTED': console.log(`Downloading new app version: ${evt.version.hash}`); break; case 'VERSION_READY': console.log(`Current app version: ${evt.currentVersion.hash}`); console.log(`New app version ready for use: ${evt.latestVersion.hash}`); break; case 'VERSION_INSTALLATION_FAILED': console.log(`Failed to install app version '${evt.version.hash}': ${evt.error}`); break; case 'VERSION_FAILED': console.log(`Version '${evt.version.hash}' failed with error: ${evt.error}`); break; } }); }}
Verificando actualizaciones
Es posible pedirle al service worker que revise si se desplegó alguna actualización en el servidor. El service worker busca actualizaciones durante la inicialización y en cada solicitud de navegación—es decir, cuando la persona usuaria navega desde una dirección diferente hacia tu aplicación. Sin embargo, podrías optar por verificar las actualizaciones manualmente si tu sitio cambia con frecuencia o si quieres que las actualizaciones sucedan según un cronograma.
Hazlo con el método checkForUpdate():
check-for-update.service.ts
import {ApplicationRef, inject, Injectable} from '@angular/core';import {SwUpdate} from '@angular/service-worker';import {concat, interval} from 'rxjs';import {first} from 'rxjs/operators';@Injectable({providedIn: 'root'})export class CheckForUpdateService { private appRef = inject(ApplicationRef); private updates = inject(SwUpdate); constructor() { // Allow the app to stabilize first, before starting // polling for updates with `interval()`. const appIsStable$ = this.appRef.isStable.pipe(first((isStable) => isStable === true)); const everySixHours$ = interval(6 * 60 * 60 * 1000); const everySixHoursOnceAppIsStable$ = concat(appIsStable$, everySixHours$); everySixHoursOnceAppIsStable$.subscribe(async () => { try { const updateFound = await this.updates.checkForUpdate(); console.log(updateFound ? 'A new version is available.' : 'Already on the latest version.'); } catch (err) { console.error('Failed to check for updates:', err); } }); }}
Este método devuelve un Promise<boolean> que indica si hay una actualización disponible para activar.
La verificación puede fallar, lo que provocará que la Promise se rechace.
Estabilización y registro del service worker
Para evitar que el renderizado inicial de la página se vea afectado de forma negativa, el servicio del service worker de Angular espera de manera predeterminada hasta 30 segundos a que la aplicación se estabilice antes de registrar el script del ServiceWorker.
Consultar constantemente si hay actualizaciones, por ejemplo, con setInterval() o con interval() de RxJS, impide que la aplicación se estabilice y evita que el script del ServiceWorker se registre en el navegador hasta que se alcanza el límite máximo de 30 segundos.
Esto aplica a cualquier tipo de sondeo que haga tu aplicación. Consulta la documentación de isStable para obtener más información.
Evita ese retraso esperando primero a que la aplicación se estabilice antes de comenzar a consultar por actualizaciones, como se muestra en el ejemplo anterior. Como alternativa, puedes definir una estrategia de registro diferente para el ServiceWorker.
Actualizando a la versión más reciente
Puedes actualizar una pestaña existente a la versión más reciente recargando la página tan pronto como una nueva versión esté lista. Para evitar interrumpir el progreso de la persona usuaria, en general es recomendable mostrar un aviso y pedir que confirme si desea recargar la página y actualizar a la versión más reciente:
prompt-update.service.ts
import {inject, Injectable} from '@angular/core';import {filter, map} from 'rxjs/operators';import {SwUpdate, VersionReadyEvent} from '@angular/service-worker';function promptUser(event: VersionReadyEvent): boolean { return true;}@Injectable({providedIn: 'root'})export class PromptUpdateService { constructor() { const swUpdate = inject(SwUpdate); swUpdate.versionUpdates .pipe(filter((evt): evt is VersionReadyEvent => evt.type === 'VERSION_READY')) .subscribe((evt) => { if (promptUser(evt)) { // Reload the page to update to the latest version. document.location.reload(); } }); // ... const updatesAvailable = swUpdate.versionUpdates.pipe( filter((evt): evt is VersionReadyEvent => evt.type === 'VERSION_READY'), map((evt) => ({ type: 'UPDATE_AVAILABLE', current: evt.currentVersion, available: evt.latestVersion, })), ); }}
Seguridad de actualizar sin recargar
Llamar a activateUpdate() actualiza una pestaña a la versión más reciente sin recargar la página, pero esto podría romper la aplicación.
Actualizar sin recargar puede crear una discrepancia de versiones entre el shell de la aplicación y otros recursos de la página, como los chunks cargados de forma diferida, cuyos nombres de archivo pueden cambiar entre versiones.
Debes usar activateUpdate() solo si estás seguro de que es seguro para tu caso específico.
Manejar un estado irrecuperable
En algunos casos, la versión de la aplicación que el service worker usa para atender a una persona usuaria puede quedar en un estado roto que no se puede recuperar sin recargar completamente la página.
Por ejemplo, imagina el siguiente escenario:
Una persona usuaria abre la aplicación por primera vez y el service worker almacena en caché la versión más reciente de la aplicación. Supón que los recursos cacheados de la aplicación incluyen
index.html,main.<main-hash-1>.jsylazy-chunk.<lazy-hash-1>.js.La persona usuaria cierra la aplicación y no la abre durante un tiempo.
Después de un tiempo, se despliega en el servidor una nueva versión de la aplicación. Esta versión más reciente incluye los archivos
index.html,main.<main-hash-2>.jsylazy-chunk.<lazy-hash-2>.js.
IMPORTANTE: Los hashes ahora son diferentes porque el contenido de los archivos cambió. La versión anterior ya no está disponible en el servidor.
Mientras tanto, el navegador de la persona usuaria decide expulsar
lazy-chunk.<lazy-hash-1>.jsde su caché. Los navegadores pueden decidir expulsar recursos específicos (o todos) de una caché para recuperar espacio en disco.La persona usuaria vuelve a abrir la aplicación. El service worker sirve la última versión que conoce en ese momento, es decir, la versión anterior (
index.htmlymain.<main-hash-1>.js).En algún momento posterior, la aplicación solicita el bundle diferido
lazy-chunk.<lazy-hash-1>.js.El service worker no puede encontrar el recurso en la caché (recuerda que el navegador lo expulsó). Tampoco puede obtenerlo del servidor (porque el servidor ahora solo tiene
lazy-chunk.<lazy-hash-2>.jsde la versión más reciente).
En el escenario anterior, el service worker no puede servir un recurso que normalmente estaría en caché.
Esa versión particular de la aplicación está rota y no hay forma de corregir el estado del cliente sin recargar la página.
En esos casos, el service worker notifica al cliente enviando un evento UnrecoverableStateEvent.
Suscríbete a SwUpdate#unrecoverable para recibir la notificación y manejar estos errores.
handle-unrecoverable-state.service.ts
import {inject, Injectable} from '@angular/core';import {SwUpdate} from '@angular/service-worker';function notifyUser(message: string): void {}@Injectable({providedIn: 'root'})export class HandleUnrecoverableStateService { private updates = inject(SwUpdate); constructor() { this.updates.unrecoverable.subscribe((event) => { notifyUser( 'An error occurred that we cannot recover from:\n' + event.reason + '\n\nPlease reload the page.', ); }); }}
Más sobre los service workers de Angular
También podría interesarte lo siguiente: