Developing Progressive Web Apps 02.0: Inicio rápido sin conexión

Este codelab forma parte del curso de capacitación Developing Progressive Web Apps, desarrollado por el equipo de capacitación de Google Developers. Aprovecharás al máximo este curso si trabajas con los codelabs de forma secuencial.

Para obtener detalles completos sobre el curso, consulta la descripción general de Developing Progressive Web Apps.

Introducción

En este lab, usarás Lighthouse para auditar un sitio web según los estándares de las aplicaciones web progresivas (AWP). También agregarás funcionalidad sin conexión con la API de Service Worker.

Qué aprenderás

  • Cómo auditar sitios con Lighthouse
  • Cómo agregar capacidades sin conexión a una aplicación

Lo que debe saber

  • Conocimientos básicos de HTML, CSS y JavaScript
  • Conocimiento de las promesas de ES2015

Lo que necesitarás

  • Una computadora con acceso a la terminal o al shell
  • Conexión a Internet
  • Navegador Chrome (para usar Lighthouse)
  • Un editor de texto
  • Opcional: Chrome en un dispositivo Android

Descarga o clona el repositorio de pwa-training-labs desde GitHub y, si es necesario, instala la versión LTS de Node.js.

Navega al directorio offline-quickstart-lab/app/ y, luego, inicia un servidor de desarrollo local:

cd offline-quickstart-lab/app
npm install
node server.js

Puedes detener el servidor en cualquier momento con Ctrl-c.

Abre el navegador y navega a localhost:8081/. Deberías ver que el sitio es una página web simple y estática.

Nota: Cancela el registro de todos los service workers y borra todas las memorias caché de service workers para localhost, de modo que no interfieran en el lab. En las Herramientas para desarrolladores de Chrome, puedes hacer clic en Borrar datos del sitio en la sección Borrar almacenamiento de la pestaña Aplicación.

Abre la carpeta offline-quickstart-lab/app/ en tu editor de texto preferido. La carpeta app/ es donde compilarás el lab.

Esta carpeta contiene lo siguiente:

  • La carpeta images/ contiene imágenes de muestra
  • styles/main.css es la hoja de estilo principal
  • index.html es la página HTML principal de nuestro sitio de ejemplo.
  • package-lock.json y package.json hacen un seguimiento de las dependencias de la app (en este caso, las únicas dependencias son para el servidor de desarrollo local).
  • server.js es un servidor de desarrollo local para pruebas
  • service-worker.js es el archivo del trabajador de servicio (actualmente vacío)

Antes de comenzar a realizar cambios en el sitio, hagamos una auditoría con Lighthouse para ver qué se puede mejorar.

Regresa a la app (en Chrome) y abre la pestaña Auditorías de las Herramientas para desarrolladores. Deberías ver el ícono de Lighthouse y las opciones de configuración. Selecciona "Móvil" en Dispositivo, selecciona todas las Auditorías, selecciona cualquiera de las opciones de Limitación y elige Borrar almacenamiento:

Haz clic en Ejecutar auditorías. Las auditorías tardan unos minutos en completarse.

Explicación

Una vez que se complete la auditoría, deberías ver un informe con las puntuaciones en las Herramientas para desarrolladores. Debería mostrar las puntuaciones, algo similar a lo siguiente (las puntuaciones podrían no ser exactamente las mismas):

Nota: Las puntuaciones de Lighthouse son una aproximación y pueden verse afectadas por tu entorno (por ejemplo, si tienes una gran cantidad de ventanas del navegador abiertas). Es posible que tus puntuaciones no sean exactamente iguales a las que se muestran aquí.

Y la sección Progressive Web App debería verse de la siguiente manera:

El informe incluye puntuaciones y métricas en cinco categorías:

  • App web progresiva
  • Rendimiento
  • Accesibilidad
  • Prácticas recomendadas
  • SEO

Como puedes ver, nuestra app obtiene una puntuación baja en la categoría de app web progresiva (AWP). ¡Mejoremos nuestra puntuación!

Tómate un momento para revisar la sección de la PWA del informe y ver qué falta.

Cómo registrar un service worker

Una de las fallas que se indican en el informe es que no hay ningún trabajador de servicio registrado. Actualmente, tenemos un archivo de service worker vacío en app/service-worker.js.

Agrega la siguiente secuencia de comandos a la parte inferior de index.html, justo antes de la etiqueta de cierre </body>:

<script>
if ('serviceWorker' in navigator) {
  window.addEventListener('load', function() {
    navigator.serviceWorker.register('service-worker.js')
      .then(reg => {
        console.log('Service worker registered! 😎', reg);
      })
      .catch(err => {
        console.log('😥 Service worker registration failed: ', err);
      });
  });
}
</script>

Explicación

Este código registra el archivo vacío del service worker service-worker.js una vez que se carga la página. Sin embargo, el archivo de service worker actual está vacío y no hará nada. Agregaremos código de servicio en el siguiente paso.

Recursos de almacenamiento previo en caché

Otro error que se indica en el informe es que la app no responde con un código de estado 200 cuando no hay conexión. Para solucionar este problema, debemos actualizar nuestro service worker.

Agrega el siguiente código al archivo del trabajador de servicio (service-worker.js):

const cacheName = 'cache-v1';
const precacheResources = [
  '/',
  'index.html',
  'styles/main.css',
  'images/space1.jpg',
  'images/space2.jpg',
  'images/space3.jpg'
];

self.addEventListener('install', event => {
  console.log('Service worker install event!');
  event.waitUntil(
    caches.open(cacheName)
      .then(cache => {
        return cache.addAll(precacheResources);
      })
  );
});

self.addEventListener('activate', event => {
  console.log('Service worker activate event!');
});

self.addEventListener('fetch', event => {
  console.log('Fetch intercepted for:', event.request.url);
  event.respondWith(caches.match(event.request)
    .then(cachedResponse => {
        if (cachedResponse) {
          return cachedResponse;
        }
        return fetch(event.request);
      })
    );
});

Ahora, regresa al navegador y actualiza el sitio. Verifica en la consola que el trabajador de servicio haga lo siguiente:

  • registrado
  • Instalada
  • Activado

Nota: Si ya registraste el trabajador de servicio anteriormente o tienes problemas para que se activen todos los eventos, anula el registro de los trabajadores de servicio y actualiza la página. Si eso no funciona, cierra todas las instancias de la app y vuelve a abrirla.

A continuación, finaliza el servidor de desarrollo local en la línea de comandos ejecutando Ctrl + c. Vuelve a actualizar el sitio y observa que se carga incluso si el servidor está sin conexión.

Nota: Es posible que veas un error en la consola que indica que no se pudo recuperar el trabajador de servicio: An unknown error occurred when fetching the script. service-worker.js Failed to load resource: net::ERR_CONNECTION_REFUSED. Este error se muestra porque el navegador no pudo recuperar la secuencia de comandos del trabajador de servicio (porque el sitio está sin conexión), pero eso es lo esperado porque no podemos usar el trabajador de servicio para almacenar en caché. De lo contrario, el navegador del usuario se quedaría con el mismo service worker para siempre.

Explicación

Una vez que la secuencia de comandos de registro registra el service worker en index.html, se produce el evento install del service worker. Durante este evento, el objeto de escucha de eventos install abre una caché con nombre y almacena en caché los archivos especificados con el método cache.addAll. Esto se denomina "almacenamiento previo en caché" porque ocurre durante el evento install, que suele ser la primera vez que un usuario visita tu sitio.

Después de instalar un service worker, y si otro service worker no controla la página en ese momento, el nuevo service worker se "activa" (se activa el objeto de escucha de eventos activate en el service worker) y comienza a controlar la página.

Cuando una página que controla un service worker activado solicita recursos, las solicitudes pasan por el service worker, como un proxy de red. Se activa un evento fetch para cada solicitud. En nuestro service worker, el objeto de escucha de eventos fetch busca en las cachés y responde con el recurso almacenado en caché si está disponible. Si el recurso no está almacenado en caché, se solicita de forma normal.

El almacenamiento en caché de recursos permite que la app funcione sin conexión, ya que evita las solicitudes de red. Ahora nuestra app puede responder con un código de estado 200 cuando no hay conexión.

Nota: En este ejemplo, el evento activate no se usa para nada más que para acceder. El evento se incluyó para ayudar a depurar problemas del ciclo de vida del service worker.

Opcional: También puedes ver los recursos almacenados en caché en la pestaña Application de las Herramientas para desarrolladores expandiendo la sección Cache Storage:

Reinicia el servidor de desarrollo con node server.js y actualiza el sitio. Luego, vuelve a abrir la pestaña Audits en Herramientas para desarrolladores y vuelve a ejecutar la auditoría de Lighthouse seleccionando New Audit (el signo más en la esquina superior izquierda). Cuando finalice la auditoría, deberías ver que la puntuación de nuestra PWA es significativamente mejor, pero aún se puede mejorar. Seguiremos mejorando nuestra puntuación en la siguiente sección.

Nota: Esta sección es opcional porque probar el banner de instalación de la app web está fuera del alcance del lab. Puedes probarlo por tu cuenta con la depuración remota.

Nuestra puntuación de PWA aún no es excelente. Algunos de los errores restantes que se enumeran en el informe son que no se le pedirá al usuario que instale nuestra app web y que no configuramos una pantalla de presentación ni colores de la marca en la barra de direcciones. Podemos corregir estos problemas y, luego, implementar progresivamente la función Agregar a la pantalla principal satisfaciendo algunos criterios adicionales. Lo más importante es que debemos crear un archivo de manifiesto.

Crea un archivo de manifiesto

Crea un archivo en app/ llamado manifest.json y agrega el siguiente código:

{
  "name": "Space Missions",
  "short_name": "Space Missions",
  "lang": "en-US",
  "start_url": "/index.html",
  "display": "standalone",
  "theme_color": "#FF9800",
  "background_color": "#FF9800",
  "icons": [
    {
      "src": "images/touch/icon-128x128.png",
      "sizes": "128x128"
    },
    {
      "src": "images/touch/icon-192x192.png",
      "sizes": "192x192"
    },
    {
      "src": "images/touch/icon-256x256.png",
      "sizes": "256x256"
    },
    {
      "src": "images/touch/icon-384x384.png",
      "sizes": "384x384"
    },
    {
      "src": "images/touch/icon-512x512.png",
      "sizes": "512x512"
    }
  ]
}

Las imágenes a las que se hace referencia en el manifiesto ya se proporcionan en la app.

Luego, agrega el siguiente código HTML en la parte inferior de la etiqueta <head> en index.html:

<link rel="manifest" href="manifest.json">

<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="application-name" content="Space Missions">
<meta name="apple-mobile-web-app-title" content="Space Missions">
<meta name="theme-color" content="#FF9800">
<meta name="msapplication-navbutton-color" content="#FF9800">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="msapplication-starturl" content="/index.html">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">

<link rel="icon" sizes="128x128" href="/images/touch/icon-128x128.png">
<link rel="apple-touch-icon" sizes="128x128" href="/images/touch/icon-128x128.png">
<link rel="icon" sizes="192x192" href="icon-192x192.png">
<link rel="apple-touch-icon" sizes="192x192" href="/images/touch/icon-192x192.png">
<link rel="icon" sizes="256x256" href="/images/touch/icon-256x256.png">
<link rel="apple-touch-icon" sizes="256x256" href="/images/touch/icon-256x256.png">
<link rel="icon" sizes="384x384" href="/images/touch/icon-384x384.png">
<link rel="apple-touch-icon" sizes="384x384" href="/images/touch/icon-384x384.png">
<link rel="icon" sizes="512x512" href="/images/touch/icon-512x512.png">
<link rel="apple-touch-icon" sizes="512x512" href="/images/touch/icon-512x512.png">

Regresa al sitio. En la pestaña Aplicación de Herramientas para desarrolladores, selecciona la sección Borrar almacenamiento y haz clic en Borrar datos del sitio. Luego, actualiza la página. Ahora selecciona la sección Manifiesto. Deberías ver los íconos y las opciones de configuración que se establecieron en el archivo manifest.json. Si no ves los cambios, abre el sitio en una ventana de incógnito y vuelve a verificarlo.

Explicación

El archivo manifest.json le indica al navegador cómo aplicar estilo y formato a algunos de los aspectos progresivos de tu app, como el cromo del navegador, el ícono de la pantalla principal y la pantalla de inicio. También se puede usar para configurar tu app web para que se abra en modo standalone, como lo hace una app nativa (en otras palabras, fuera del navegador).

Al momento de escribir este artículo, aún se está desarrollando la compatibilidad con algunos navegadores, y las etiquetas <meta> configuran un subconjunto de estas funciones para ciertos navegadores que aún no tienen compatibilidad total.

Tuvimos que borrar los datos del sitio para quitar nuestra versión anterior almacenada en caché de index.html (ya que esa versión no tenía el vínculo del manifiesto). Intenta ejecutar otra auditoría de Lighthouse y observa cuánto mejoró la puntuación de la PWA.

Cómo activar el mensaje de instalación

El siguiente paso para instalar nuestra app es mostrar a los usuarios el mensaje de instalación. Chrome 67 les mostraba a los usuarios el mensaje de instalación automáticamente, pero a partir de Chrome 68, el mensaje de instalación se debe activar de forma programática en respuesta a un gesto del usuario.

Agrega un botón y un banner de "Instalar la app" en la parte superior de index.html (justo después de la etiqueta <main>) con el siguiente código:

<section id="installBanner" class="banner">
    <button id="installBtn">Install app</button>
</section>

Luego, agrega los siguientes estilos a styles/main.css para diseñar el banner:

.banner {
  align-content: center;
  display: none;
  justify-content: center;
  width: 100%;
}

Guarda el archivo. Por último, agrega la siguiente etiqueta de secuencia de comandos a index.html:

  <script>
    let deferredPrompt;
    window.addEventListener('beforeinstallprompt', event => {

      // Prevent Chrome 67 and earlier from automatically showing the prompt
      event.preventDefault();

      // Stash the event so it can be triggered later.
      deferredPrompt = event;

      // Attach the install prompt to a user gesture
      document.querySelector('#installBtn').addEventListener('click', event => {

        // Show the prompt
        deferredPrompt.prompt();

        // Wait for the user to respond to the prompt
        deferredPrompt.userChoice
          .then((choiceResult) => {
            if (choiceResult.outcome === 'accepted') {
              console.log('User accepted the A2HS prompt');
            } else {
              console.log('User dismissed the A2HS prompt');
            }
            deferredPrompt = null;
          });
      });

      // Update UI notify the user they can add to home screen
      document.querySelector('#installBanner').style.display = 'flex';
    });
  </script>

Guarda el archivo. Abre la app en Chrome en un dispositivo Android con la depuración remota. Cuando se cargue la página, deberías ver el botón "Instalar la app" (no lo verás en una computadora, así que asegúrate de realizar la prueba en un dispositivo móvil). Haz clic en el botón y debería aparecer el mensaje Agregar a la pantalla principal. Sigue los pasos para instalar la app en tu dispositivo. Después de la instalación, deberías poder abrir la app web en modo independiente (fuera del navegador) si presionas el ícono de la pantalla principal que se creó recientemente.

Explicación

El código HTML y CSS agrega un banner y un botón ocultos que podemos usar para permitir que los usuarios activen el mensaje de instalación.

Una vez que se activa el evento beforeinstallprompt, evitamos la experiencia predeterminada (en la que Chrome 67 y versiones anteriores solicitan automáticamente a los usuarios que instalen la app) y capturamos el beforeinstallevent en la variable global deferredPrompt. Luego, se configura el botón "Instalar la app" para que muestre el mensaje con el método prompt() de beforeinstallevent. Una vez que el usuario toma una decisión (instalar o no), la promesa userChoice se resuelve con la elección del usuario (outcome). Por último, mostramos el botón de instalación cuando todo está listo.

Aprendiste a auditar sitios con Lighthouse y a implementar los conceptos básicos de la funcionalidad sin conexión. Si completaste las secciones opcionales, también aprendiste a instalar apps web en la pantalla principal.

Más recursos

Lighthouse es de código abierto. Puedes bifurcarlo, agregar tus propias pruebas y registrar errores. Lighthouse también está disponible como una herramienta de línea de comandos para la integración con procesos de compilación.

Para ver todos los codelabs del curso de capacitación sobre APW, consulta el codelab de bienvenida del curso.