Introducción a los Service Workers

Service Worker, el santo grial de PWA o aplicaciones web progresivas. Pero, ¿qué es un PWA?.

Google dice algo poco específico, son “experiencias”, otros llaman “aplicaciones web progresivas”, otros “como aplicaciones nativas”, pero para todos ustedes la explicación más fácil es que una PWA es una aplicación web con, al menos, un trabajador de servicios con caché.

Hace tiempo, habrás visto tipos de web (por ejemplo, un chat) que se comunican con un servidor y devuelven (y envían) texto a un grupo de personas. O bien, antes de frameworks como AngularJS (o angular, o el framework típico o biblioteca), o incluso la ejecución en segundo plano del código dentro de su navegador.

Web Worker

Lo que os acabo de contar es un Web Worker. Un web worker es una pieza de código (javascript) que puede ejecutar en su navegador en segundo plano y puede comunicarse para enviar y recibir “mensajes”. Su uso, aunque sencillo en si, era complicado para la mayoría de la gente con todas las ventajas que ofrecía hasta la creación de frameworks y bibliotecas (Angular, React, Vue, esas cosas…).

Por ejemplo, un web worker típico para conocer números primos será así:

El archivo HTML:


<!DOCTYPE html>
<html lang="en" dir="ltr">
  <head>
    <meta charset="utf-8">
    <title>Prime number</title>
  </head>
  <body>
    <h1>Prime number worker</h1>
    <article>
      Primer numerb!: 
      <strong><div id="result"></div></strong>
    </article>
    <script>
    var worker = new Worker('calculateprime.js')
    worker.onmessage = function (event) {
      document.getElementById('div').textContent = event.data;
    };
    </script>
  </body>
</html>

Y ahora el javascrtip calculateprime.js

var n = 1;
search: while (true) {
  n += 1;
  for (var i = 2; i <=n; i += 1) {
    if (n % i == 0) {
        continue search;
    }
    postMessage(n);
  }
}

Cuando cargas el archivo index.html en un navegador, verás cómo muestra los números primos uno después de otro. Es muy sencillo de comprender el código si conoces Javascript básico, simplemente creamos un objeto nuevo que es un worker y lo asociamos al fichero javascript donde esta el código. Pero lo más importante es que puede ver cómo en el archivo index.html creamos un worker que recibe mensajes con el método onmessage que funciona cuando el alculateprime.js envía un mensaje a través de postMessage. Así es como un worker y el "cliente" pasan mensajes de uno a otro (y puede ser cualquier cosa, no solo un número o una cadena, puede ser un array o un objeto).

El worker corre con la página pero en segundo plano, en un hilo separado de la web. Es decir, la web se ejecuta en un hilo y el worker en otro separados en memoria.

Un Service Worker

Ahora que, con este ejemplo, hemos visto que es un web worker y, esperemos, entendemos un poco más como funcionan los frameworks y librerías (recordad, React, Vue, Angular, Polymer...), vamos a echarle un ojo a un Service Worker.

El elemento principal de una PWA es un Service Worker. Un Service Worker es un worker (como el de antes) con unas tareas un poco especificas que nos permiten los navegadores modernos como tener un storage (un sitio donde poder tener una cache de ficheros, sesiones, cookies), un web SQL (como una base de datos) donde puedes tener o crear una pequeña base de datos donde almacenar cosas. Ya sabeis, todas esas cosas de los navegadores modernos.

Resumiendo. Si creamos un worker que usa el storage del navegador para poner ahí ficheros de la aplicación y creamos un método para capturar las operaciones de fetch de tu página web, si pedimos algo que tenga en la cache (por ejemplo) si no tienes conexión, la web funcionara como una aplicación... y ya tenemos una PWA.

Vamos a hacer un ejemplo, primero el HTML:

<!DOCTYPE html>
<html lang="en" dir="ltr">
  <head>
    <meta charset="utf-8">
    <title>Service Worker 101</title>
  </head>
  <body>

    <center><img></center>

    <script>if ('serviceWorker' in navigator) {
        window.addEventListener('load', function() {
          navigator.serviceWorker.register('service.js')
          .then(function(registration) {
           // Successful registration
           console.log('Hooray. Registration successful, scope is:', registration.scope);
          }).catch(function(err) {
           // Failed registration, service worker won’t be installed
           console.log('Whoops. Service worker registration failed, error:', err);
          });
        });
      }

      // Fetch example
      var Image = document.querySelector('img');

      fetch('flores.jpg')
      .then(function(response) {
        console.log('Fetching on the page ok!');
        return response.blob();
      })
      .then(function(myBlob) {
        var objectURL = URL.createObjectURL(myBlob);
        Image.src = objectURL;
      });

    </script>
  </body>
</html>

Y ahora el fichero service.js

// Vars
const PRECACHE = 'precache-v1';
const RUNTIME = 'runtime';
var CACHE_NAME = 'my-first-serviceworker';
var urlsToCache = [
  '//flores.jpg',
  'index.html',
];

self.addEventListener('install', function(event) {
  console.log('Installing!');
  event.waitUntil(
    caches.open(CACHE_NAME)
   .then(function(cache) {
     console.log('Addiding files to cache!');
     console.log(cache);
     // Open a cache and cache our files
     return cache.addAll(urlsToCache);
   })
   // Skip waiting if it's on the cache
   .then(self.skipWaiting())
  );
});

// Activate event
self.addEventListener('activate', function(event) {
  console.log('Activating event '+ event);
});

//Control fetch
self.addEventListener('fetch', function(event) {
    console.log('Fetching some data');
    console.log(event.request.url);
    event.respondWith(
        caches.match(event.request).then(function(response) {
            console.log('File is on the cache! Hooray! Let\'s take it from there!');
            return response || fetch(event.request);
        })
    );
});

Aunque parezca muy complicado es muy muy sencillo. Observando el fichero html verás que hay un tag "img" vacio, no apunta a ningún sitio y por lo tanto no cargaría absolutamente nada pero, en el javascript veras que hay un fetch que, realmente, hace una petición de un fichero y carga una imagen (cualquiera, en mi caso unas flores).

Ademas, en el HTML vemos como se instala el worker preguntando al navegador si tiene capacidad de aceptar service worker. Si la respuesta es afirmativa, se registra y punto pelota.

Hay diferentes formas, pero la mas usual es la que veis, añadir un evento al load de la pagina que sea registrar el service worker con un promise (que nunca se sabe) a fin de poder mandarle a otras versiones o hacer otras cosas si no funciona.

En el worker (el fichero javascript) que es el que compone el service worker verás ciertas cosas importantes, vamos, el mejunje de un service worker.

Lo primero a tener en cuenta es que lo que haremos realmente sera que todo lo controlamos a traves de los eventos del navegador. Es decir, controlaremos los eventos.

Si observais, vereis que todo se llama a traves del self porque este hace referencia a la ventana de forma que no afecte al resto de pestañas o ventanas que tenga el navegador abiertas.

¿Y que eventos son los que tenemos que tener en cuenta?. Aunque hay muchos ejemplos, los principales a usar son install, fetch y activate.

El evento install se dispara cuando, obviamente, se instala el service worker. Normalmente se usa para crear las condiciones necesarias para que el service worker funcione por ejemplo la cache, meter los ficheros en ella y todas esas cosas. Recodad que se dispara una unica vez y solo cuando se instala.

El evento activate se dispara cuando se activa el service worker, antes de realizar cualquier acción. Normalmente se usa para revisar la cache (por si cambia) o incluso para instalar una versión mejor del propio service worker (por si actualizais el código y esas cosas... recordad que la comparación es binaria).

Y por último, el evento fetch se dispara cuando la ventana realiza un fetch, es decir, una petición. Se suele usar para lo que veis en el ejemplo, interceptarlo, revisar si el fichero esta en la cache o no y enviarlo de la misma.

Por lo tanto, en el ejemplo, hemos creado una cache que, si no esta conectado a red, mostrara la imagen de las flores igualmente e incluso si esta conectado a red al cargarlo de la cache ira mucho más rápido.

Luego a partir de aquí podéis hacer lo que mas os guste como por ejemplo cargar imágenes "vacías" que hagan de placeholder de objetos. Esto seguro que lo habéis visto en Youtube o en Facebook (por poner dos ejemplos) que muestra placeholders mientras se trae vuestras notificaciones o los videos o lo que sea de forma que, al usuario, le hace más agradable la espera y no le cambia todo de golpe.

Por supuesto hay muchos más eventos que se pueden controlar desde un service worker. Os recomiendo echarle un ojo a este repositorio en GitHub donde hay muchisimos ejemplos: https://github.com/GoogleChrome/samples/tree/gh-pages/service-worker

Bueno, esto es el típico service worker. Si lo ejecutáis y abrís la consola del navegador, veréis (por el código) que va haciendo y donde, porque siempre es bueno para aprender.

Y por supuesto, se pueden hacer muchas cosas, pero esto es una simple introducción.

Publicado por mi: Linkedin (english)

tuti

Técnico en Comunicación Interna de la UVa

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.