PWA y Services Worker en Angular

2
15920

Índice de contenidos

1. Introducción

Actualmente está en auge las aplicaciones web progresivas (PWA), ¿pero qué son?
En este tutorial explicaremos qué son las PWA, cuáles son los pasos a seguir para convertir tu aplicación angular en PWA y cuáles son las ventajas de esta forma de hacer aplicaciones webs.
Ademas profundizaremos en los service workers y veremos cómo podemos configurarlos de forma manual.


2. Entorno

El tutorial está escrito usando el siguiente entorno:

  • Hardware: Portátil MacBook Pro 17′ (2,5 GHz Intel Core i7, 16GB DDR3).
  • Sistema Operativo: Mac OS Sierra 10.12.6
  • Entorno de desarrollo: visual studio code


3. ¿Qué son las PWA?

Son aplicaciones web que tienen las siguientes características:

  • Son progresivas, esto es que funcionan para cualquier usuario en cualquier navegador.
  • Son responsive, se adaptan a cualquier pantalla, ya sea móvil, tablet o pc.
  • Funcionan sin conexión.
  • Pueden ser instaladas en el móvil.
  • Se puede acceder directamente a ellas a través de una url.

Desventajas

  • Solo funcionan en Chrome.
  • Su funcionamiento no puede llegar a ser tan fluido como las apps nativas.
  • Las aplicaciones PWA deben ser SPA
  • La aplicación debe tener una primera conexión para cargar los datos, después ya puede trabajar en offline, porque ya se habrá cargado la caché.


4. PWA en Angular

En angular el uso de PWA es realmente sencillo. Solo tenemos que instalar:

@angular/service-worker:

npm install @angular/service-worker --save

Y generar el archivo de manifiesto (manifest.json)

{
    "short_name": "BigFood",
    "name": "BigFood",
    "icons": [
   
      {
        "src":"/assets/icon512.png",
        "sizes": "512x512",
        "type": "image/png"
      }       
    ],
    "start_url": "index.html",
    "background_color": "#FBFBFB",
    "theme_color": "#022E63",
    "display": "standalone",
    "orientation": "portrait"
  }

El archivo de manifiesto debe tener los siguientes flags:

  • short_name:es el nombre que se mostrará en el home de tu móvil, junto al icono
  • icons: debe tener al menos uno con un tamaño de 512×512, este icono será el acceso a tu aplicación. También se usa cuando carga la aplicación, como icono de bienvenida.
  • start_url:aquí debemos poner la entrada a la aplicación, en nuestro caso index.html
  • theme_color:es el nombre que se mostrará en el home de tu móvil, junto al icono
  • background_color:es el color de fondo de la aplicación
  • display:define la preferencia de presentación, para PWA debe ponerse en standalone, así nos permite crear lanzador en nuestro home del teléfono.
    .
  • orientation:portrait (vertical) o landscape (horizontal).

Para activar el service worker en Angular, hay que poner el flag service-worker a true en el fichero angular-cli.json.

  "apps": [
    {
      "root": "src",
      "outDir": "dist",
      "assets": [
        "assets",
        "favicon.ico",
        "manifest.json",
        "service-worker.js"
      ],
      "index": "index.html",
      "main": "main.ts",
      "polyfills": "polyfills.ts",
      "test": "test.ts",
      "tsconfig": "tsconfig.json",
      "testTsconfig": "tsconfig.json",
      "prefix": "app",
      "serviceWorker": true,
      "styles": [
        "styles.css"
      ],

O directamente ejecutando, sobre el directorio raíz del proyecto

ng set apps.0.serviceWorker=true

Una vez realizada la build, angular-cli añade sw-register.xxxxx.bundle.js al index.html

Se generan los ficheros:

  • sw-registres.xxxx.bundle.js que es el script de instalación del service worker.
  • worker-service-min.js
  • ngsw-manifiest.json

4.1. El ngsw-manifest

4.1.1. Contenido estático y externo

Con static nos define las urls estáticas seguidas por el hash. Este fichero se genera al realizar la build.

{
  "static": {
    "urls": {
      "/polyfills.dd172c3cbe11ef73974a.bundle.js": "7152d90bc05d5e4b0bcc88a6f5c963fcb864327a",
      "/main.b7bbd0fb1243d2926564.bundle.js": "51d6474180bb1c9981b23e9fc1d14b095fd92837",
      "/sw-register.6819cd2c5fa25470ecf2.bundle.js": "50d549edfbd188ed199d8d0b079afa593202e3ea",
      "/vendor.89bdadb1264db70e13e2.bundle.js": "ad57638b4892001e9f8065ce820b41048162ab2d",
      "/inline.8826fad66f99f9e4e1d3.bundle.js": "c5d716befeb16d6347415f64b0c6e06bba88a31e",
      "/styles.c75263eca8421e315975.bundle.css": "e5afde1ebe01d20cc2b2fc35fc6a75d4cd5e7ac3",
      "/assets/404.png": "9e8a41400f8c9eaa0a76e1c3cd6b76a393531d2b",
      "/assets/activity-icons-md.png": "e16b02a7a51f715161c25aeeb6be6f44e7def44e",
      "/assets/activity-icons-sm.png": "067f03197440f0b34a22c5dddfd404428c94eb44",
      "/assets/activity-icons.png": "d330226bd16b81810dba8ff3fb43bdcc796732e3",
      "/assets/fitness.png": "ee4b4e038ce4a7079e3605277d318fe631c6293e",
      "/assets/food.png": "5796b0c1ec43db7438fcad3d87df61b895c12978",
      "/assets/google.png": "9ca264564e0c561259061a451c8c9cb5534fae04",
      "/assets/hombre.png": "81711b04ebd73263bea8958d81704aa42b560fdf",
      "/assets/icon-white.png": "800cbf70225fef25dc8bb0623c9ddfb554000426",
      "/assets/icon.png": "34b1b50a5d7e1810fceec47d4e99f2039d74bec0",
      "/assets/icon144.png": "b4e5876b7a45dd4395e5fb03d6616bca0d6173ad",
      "/assets/icon192.png": "a3b3b97f897b402ba0ed19b550fb12233e5fe3a2",
      "/assets/icon3.png": "dcd02215ba3d10c0ad0ad0f0d3c801e61ce9f2da",
      "/assets/icon512.png": "4abce2802be8fb58d50c9c084556e6e3cb1ceb68",
      "/assets/icons-pack.jpg": "ce16b0d295350a7b1dc87297aee5f0b4db6e2d62",
      "/assets/mujer.png": "9201085c4f442418fa31735b14ef4ccd2cde2a13",
      "/assets/offline.png": "ab450a496257366c5bebd2cbd0d126b385bd11b1",
      "/favicon.ico": "eff98a58c41f6d64d5b9287e0e33a29d952639f0",
      "/manifest.json": "cad6d47e96d72e3fc265e1ca628fdb69f19033de",
      "/index.html": "46e652b9b25adfc15ca82ad200becb907397d295"
    },
    "_generatedFromWebpack": true
  }
}

Si tienes contenido externo, el archivo ngsw-manifest vendrá con esta sección:

4.1.2 Routing

Service worker solo dispone de los recursos almacenados en la caché, si deseamos añadir las rutas de nuestra aplicación tendríamos que instalar:

npm install sw-precache

Después creamos sw-precache-config.js en la carpeta src y configuramos:

  • staticFileGlobs: le decimos donde están los archivos estáticos
  • navigateFallback: si se intenta acceder a una ruta que no existe, se le redirige a la ruta que se informe aqui
  • root: donde esta los ficheros generados con la build
  • stripPrefix: elimina la cadena especificada del comienzo de las URL, de la ruta de acceso, en tiempo de ejecución.
module.exports = {
  staticFileGlobs: [
    'dist/**.html',
    'dist/**.js',
    'dist/**.css',
    'dist/assets/**.png',
    'dist/vendor.**.bundle.js'
  ],
  root: 'dist',
  stripPrefix: 'dist/',
  navigateFallback: '/index.html',
  runtimeCaching: [
    {
      urlPattern: /^(https\:)(\/\/)([^\:\/]*)(\:\d{1,5})?(\/)([^\/\?\&\#\:]*)?\/?([^\/\?\&\#\:]*)?\/?([^\/\?\&\#\:]*)?\/?([^\/\?\&\#\:]*)?\/?([^\/\?\&\#\:]*)?\/?([^\/\?\&\#\:]*)?\/?([^\/\?\&\#\:]*)?\/?([^\/\?\&\#\:]*)?\/?(.*)$/,
      handler: 'cache-first',
    },
    {
      urlPattern: /\/food-list\//,
      handler: 'cache-first',
    },
    {
      urlPattern: /\/profile\//,
      handler: 'cache-first',
    },
    {
      urlPattern: /\/daily\//,
      handler: 'cache-first',
    },
    {
      urlPattern: /\/progress\//,
      handler: 'cache-first',
    },
    
  ]
};

4.2. Lo vemos en DevTools

En devTools de Chrome podemos ver como se registra el service worker

También vemos las cachés que da de alta

Con esto finalizamos la configuración del service worker en Angular y hemos convertido nuestra app en PWA.

Ahora, si deseamos realizar cambios en el service worker o personalizarlo a nuestro gusto, tendríamos que quitar toda esta configuración y hacerlo a mano.
En los siguientes puntos, quitaremos el automático de Angular y os mostraremos la forma de configurar nuestro service worker de forma manual, por lo que conseguiremos un un mayor control de las peticiones y las cachés.

5. Profundizando en los Services Worker

Los services worker son secuencias de comandos que se ejecutan en segundo plano en el navegador. La función principal es la capacidad de interceptar y manejar solicitudes de red, incluida la administración de un caché. Donde el service worker se apoya para tener la información disponible en caso de:

  • Conexión baja: también conocida como Lie-fi, que hace referencia a ese estado donde la conexión es muy débil como para obtener información, pero sigue siendo más que no tener conexión, por tanto intenta conectar… conectando…así que esperas…, pero nunca conecta… 🙁
  • Sin conexión

Con los service workers mejoramos la experiencia de usuario, haciendo que siempre tenga disponible la información. Si bien habrá veces que no sea información actualizada al segundo, pero en cuanto la aplicación disponga de conexión nuevamente, actualizará la página.

La anterior app estaba hecha con el service worker de Angular, aquí como lo vamos a hacer de forma manual, creamos otra app. Para bajaros el código, ejecutad este comando:

git clone https://github.com/Kiketic/marvelAngularServiceWorker.git

5.1. Ciclo de vida

El ciclo de vida de los service worker consta de instalación, activación, Idle (espera), fetch/message y terminado

Mediante el siguiente el script llamamos a nuestro service worker para que comience con su instalación.


    if ('serviceWorker' in navigator) {
      navigator.serviceWorker.register('/sw.js').then(function (reg) {

        if (reg.installing) {
          console.log('Service worker installing');
        } else if (reg.waiting) {
          console.log('Service worker installed');
        } else if (reg.active) {
          console.log('Service worker active');
        }

      }).catch(function (error) {
        // registration failed
        console.log('Registration failed with ' + error);
      });
    }

  

5.1.1. Instalación

Al iniciar el serviceworker se debe registrar, para ello realiza la instalación, aquí es donde se puede almacenar los archivos estáticos en la caché.

 var staticCacheName = 'marvel-v3'
self.addEventListener('install', function(event) {
  event.waitUntil(
    caches.open(staticCacheName).then(function(cache) {
      return cache.addAll([
        '/',
        '/assets/marvel.png',
        'index.html',
        'https://fonts.gstatic.com/s/roboto/v15/2UX7WLTfW3W8TclTUvlFyQ.woff',
        'https://fonts.gstatic.com/s/roboto/v15/d-6IYplOFocCacKzxwXSOD8E0i7KZn-EPnyo3HZu7kw.woff'
      ]);
    })
  );
});

5.1.2. Activación

Después de la instalación, comenzará el paso de activación. Aquí podremos administrar los cachés anteriores. En la siguiente imagen podemos ver el proceso de activación, en él hemos programado un filtro para que coja las cachés antiguas y borre todas las caché que empiecen por “marvel-” y que no sean la versión actual.

self.addEventListener('activate', event => {
   event.waitUntil(
      caches.keys().then( cacheNames => {
      return Promise.all(
        cacheNames.filter(cacheName => {
          return cacheName.startsWith('marvel-') && cacheName != staticCacheName;
        }).map(cacheName => {
          return caches.delete(cacheName);
        })
      );
    })
  )
});

Después de arrancar el service worker se ha instalado y activado

5.1.3. Fetch

Siguiendo al proceso de activación, está el proceso fetch, donde el service worker controlará todas las páginas que están a su alcance.

 self.addEventListener('fetch', event => {
  event.respondWith(
    caches.match(event.request).then(response => {
      return response || fetch(event.request);
    }).catch(function() {
      // If both fail, show a generic fallback:
      return caches.match('/offline.html');
    })
  );
});

En el proceso fetch hemos optado por programar offline-first, esto es que primero miramos la caché (2), y cargamos lo que tengamos (que hemos guardado en el proceso de instalación), después vamos a Internet a por nueva información(3), y la descargamos(4), finalmente pintamos en el navegador toda la información.

En el caso de que fallen las dos peticiones, caché y network, podemos mostrar una página informando que no tenemos conexión, esto solo pasará cuando la caché esté vacía y no tengamos red.

Una vez que se instale la caché ya tendrá una versión con los estáticos seleccionados en el proceso de instalación.

Existen otras opciones, como por ejemplo ‘network & cache race’, que realiza la petición tanto a la red como a la caché. Si la caché tiene la información solicitada, esta siempre será la que se muestre en el navegador, porque será la que se obtenga antes, por tanto no será información actualizada.

Hay otra versión, para mí la mejor, pero también más complicada, llamada caché y después network. Esta consiste en que primero el service worker mira en a la caché, recuperamos toda la información cacheada, después va a Internet y consigue toda la información actualizada, la muestra en la página y además actualiza la caché. Así que cada vez que hace el fetch, actualiza la caché, esto nos permite tener la información actualizada, incluso cuando tengamos cortes de conexión.

Código a añadir en el index.html

Código en el ServiceWorker:

Aquí se le indica al SW que abra la caché, vaya a Internet y finalmente actualice la caché

5.2. Disponibilidad en Navegadores

En cuanto a los navegadores, los service worker aún no están estandarizados, pero parece ser que se está apostando por ello, ya solo falta Safari y Edge por completar su implementación.

6. Conclusiones

Los service worker son el futuro en cuanto a la plataforma web, pero las aplicaciones PWA aún están muy lejos de sustituir a las nativas. Aunque las PWA funcionen bien, en apps grandes, no llegan a la estabilidad y robustez que tienen las nativas.

Solo desarrollaría una PWA cuando las condiciones económicas de la empresa o cliente no permitan invertir en dos aplicaciones, una web para los accesos desde portátil o PC y otra nativa para móviles y tablets.

7. Referencias

2 COMENTARIOS

  1. Hola
    Hemos creado una pagina con un botón para añadir o instalar la APP-WEB.
    No la tenemos publicada, ya que nos falta un tema de enlaces internos de un iframe, pero nos funciona correctamente aunque solo en teléfonos android, Los Iphone, aunque utilicen el Chrome, no funciona.
    Es posible o podemos tener problemas en el código.
    Ahora mismo no sé muy bien como funciona, ya que lo hizo un estudiante en sus practicas, y debo investigar como esta hecho.

    Muchas gracias

DEJA UNA RESPUESTA

Por favor ingrese su comentario!

He leído y acepto la política de privacidad

Por favor ingrese su nombre aquí

Información básica acerca de la protección de datos

  • Responsable:
  • Finalidad:
  • Legitimación:
  • Destinatarios:
  • Derechos:
  • Más información: Puedes ampliar información acerca de la protección de datos en el siguiente enlace:política de privacidad