Las aplicaciones web progresivas son una forma de llevar esa sensación de aplicación nativa a una aplicación web tradicional. Con PWAs podemos mejorar nuestro sitio web con funcionalidades de aplicaciones móviles, aumentando la usabilidad y ofreciendo una excelente experiencia de usuario.

En este artículo, vamos a crear una PWA desde cero con HTML, CSS y JavaScript. Estos son los temas que cubriremos:

Entonces, comencemos con una pregunta importante: ¿Qué diablos es una PWA?

¿Qué es una aplicación web progresiva?

Una Aplicación Web Progresiva (PWA por sus siglas en inglés) es una aplicación web que ofrece a los usuarios una experiencia similar a la de aplicaciones nativas mediante el uso de capacidades web modernas. Al final, es solo un sitio web común que se ejecuta en el navegador con algunas mejoras. Dándote la habilidad de:

  • Instalarla en una pantalla de inicio móvil
  • Acceder sin conexión
  • Acceder a la cámara
  • Recibir notificaciones push
  • Sincronizar en segundo plano

Y mucho más.

Sin embargo, para poder transformar nuestra aplicación web tradicional en una PWA, tenemos que ajustarla un poco, agregando un archivo de manifiesto de aplicación web y un service worker.

No te preocupes por estos nuevos términos – los cubriremos más adelante.

Primero, tenemos que construir nuestra aplicación web tradicional. Así que comencemos con el marcado.

Marcado básico

El archivo HTML es relativamente simple. Envolvemos todo en la etiqueta main.

  • En index.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <link rel="stylesheet" href="css/style.css" />
    <title>Dev'Coffee PWA</title>
  </head>
  <body>
    <main>
      <nav>
        <h1>Dev'Coffee</h1>
        <ul>
          <li>Home</li>
          <li>About</li>
          <li>Blog</li>
        </ul>
      </nav>
      <div class="container"></div>
    </main>
    <script src="js/app.js"></script>
  </body>
</html>

Se crea una barra de navegación con la etiqueta nav. Luego, el div con la clase .container guardará las tarjetas que agregaremos más tarde con JavaScript.

Una vez terminado el marcado básico, procedemos a darle estilo con CSS.

Estilizando

Aquí, como de costumbre, comenzamos importando las fuentes que necesitamos. Luego hacemos algunos restablecimientos para evitar el comportamiento predeterminado.

  • En css/style.css
@import url("https://fonts.googleapis.com/css?family=Nunito:400,700&display=swap");
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}
body {
  background: #fdfdfd;
  font-family: "Nunito", sans-serif;
  font-size: 1rem;
}
main {
  max-width: 900px;
  margin: auto;
  padding: 0.5rem;
  text-align: center;
}
nav {
  display: flex;
  justify-content: space-between;
  align-items: center;
}
ul {
  list-style: none;
  display: flex;
}

li {
  margin-right: 1rem;
}
h1 {
  color: #e74c3c;
  margin-bottom: 0.5rem;
}

Ahora, limitamos el ancho máximo del elemento main a 900px para que se vea bien en una pantalla grande.

Para la barra de navegación, quiero que el logo esté a la izquierda y los enlaces a la derecha. Por lo que en la etiqueta nav, después de convertirla en un contenedor flexible, usamos justify-content: space-between; para alinearlos.

  • En css/style.css
.container {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
  grid-gap: 1rem;
  justify-content: center;
  align-items: center;
  margin: auto;
  padding: 1rem 0;
}
.card {
  display: flex;
  align-items: center;
  flex-direction: column;
  width: 15rem auto;
  height: 15rem;
  background: #fff;
  box-shadow: 0 10px 20px rgba(0, 0, 0, 0.19), 0 6px 6px rgba(0, 0, 0, 0.23);
  border-radius: 10px;
  margin: auto;
  overflow: hidden;
}
.card--avatar {
  width: 100%;
  height: 10rem;
  object-fit: cover;
}
.card--title {
  color: #222;
  font-weight: 700;
  text-transform: capitalize;
  font-size: 1.1rem;
  margin-top: 0.5rem;
}
.card--link {
  text-decoration: none;
  background: #db4938;
  color: #fff;
  padding: 0.3rem 1rem;
  border-radius: 20px;
}

Tendremos varias tarjetas, por lo que el elemento container se mostrará como una cuadrícula. Con grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr)) podemos hacer que nuestras tarjetas sean responsivas, usando al menos 15rem de ancho si hay suficiente espacio (y 1fr si no).

Finalmente, para que estas luzcan mejor, duplicamos el efecto de sombra en la clase .card y usamos object-fit: cover en .card--avatar para evitar que la imagen se estire.

Con esto, nuestra aplicación web se ve mucho mejor – pero todavía no tenemos datos para mostrar.

Arreglemoslo en la siguiente sección.

Mostrar datos con JavaScript

Puedes descargar todos los recursos que usé para la creación de esta aplicación web aquí.

Observa que utilicé imágenes grandes que tardan algún tiempo en cargarse. Esto te mostrará de la mejor manera el poder de los service workers.

Como dije antes, la clase .container tendrá nuestras tarjetas. Por lo tanto, debemos seleccionarlo.

  • En js/app.js
const container = document.querySelector(".container")
const coffees = [
  { name: "Perspiciatis", image: "images/coffee1.jpg" },
  { name: "Voluptatem", image: "images/coffee2.jpg" },
  { name: "Explicabo", image: "images/coffee3.jpg" },
  { name: "Rchitecto", image: "images/coffee4.jpg" },
  { name: " Beatae", image: "images/coffee5.jpg" },
  { name: " Vitae", image: "images/coffee6.jpg" },
  { name: "Inventore", image: "images/coffee7.jpg" },
  { name: "Veritatis", image: "images/coffee8.jpg" },
  { name: "Accusantium", image: "images/coffee9.jpg" },
]

Luego, creamos un arreglo de tarjetas con nombres e imágenes.

  • En js/app.js
const showCoffees = () => {
  let output = ""
  coffees.forEach(
    ({ name, image }) =>
      (output += `
              <div class="card">
                <img class="card--avatar" src=${image} />
                <h1 class="card--title">${name}</h1>
                <a class="card--link" href="#">Taste</a>
              </div>
              `)
  )
  container.innerHTML = output
}

document.addEventListener("DOMContentLoaded", showCoffees)

El código anterior nos permite recorrer el arreglo y mostrar su contenido en el archivo HTML. Y para que todo funcione, esperamos hasta que el contenido del DOM (del inglés Document Object Model) termine de cargarse y así ejecutar el método showCoffees.

Hemos hecho mucho, pero por ahora solo tenemos una aplicación web tradicional. Así que cambiemos eso en la siguiente sección, presentando algunas funcionalidades de las PWAs.

super-excited

Manifiesto de Aplicación Web

El manifiesto de aplicación web es un archivo JSON simple que informa al navegador sobre tu aplicación web. Indica cómo debe comportarse cuando se instala en el dispositivo móvil o escritorio del usuario. Si queremos que se muestre el mensaje Agregar a la Pantalla de Inicio, requeriremos el manifiesto de aplicación web.

Ahora que sabemos qué es un manifiesto web, creemos un nuevo archivo llamado manifest.json (tienes que nombrarlo así) en el directorio raíz. Luego agrega el siguiente bloque de código.

  • En manifest.json
{
  "name": "Dev'Coffee",
  "short_name": "DevCoffee",
  "start_url": "index.html",
  "display": "standalone",
  "background_color": "#fdfdfd",
  "theme_color": "#db4938",
  "orientation": "portrait-primary",
  "icons": [
    {
      "src": "/images/icons/icon-72x72.png",
      "type": "image/png", "sizes": "72x72"
    },
    {
      "src": "/images/icons/icon-96x96.png",
      "type": "image/png", "sizes": "96x96"
    },
    {
      "src": "/images/icons/icon-128x128.png",
      "type": "image/png","sizes": "128x128"
    },
    {
      "src": "/images/icons/icon-144x144.png",
      "type": "image/png", "sizes": "144x144"
    },
    {
      "src": "/images/icons/icon-152x152.png",
      "type": "image/png", "sizes": "152x152"
    },
    {
      "src": "/images/icons/icon-192x192.png",
      "type": "image/png", "sizes": "192x192"
    },
    {
      "src": "/images/icons/icon-384x384.png",
      "type": "image/png", "sizes": "384x384"
    },
    {
      "src": "/images/icons/icon-512x512.png",
      "type": "image/png", "sizes": "512x512"
    }
  ]
}

Al final, es solo un archivo JSON con algunas propiedades obligatorias y otras opcionales.

name (nombre): Cuando el navegador inicie la pantalla de bienvenida, será el nombre que se muestre en la pantalla.

short_name (nombre corto): Será el nombre que se muestre debajo del acceso directo de la aplicación en la pantalla de inicio.

start_url (url de inicio): Será la página que se muestre al usuario una vez abierta tu aplicación.

display: Le dice al navegador cómo mostrar la aplicación. Hay varios modos como minimal-ui, fullscreen, browser, etc. Aquí, utilizamos el modo standalone para ocultar todo lo relacionado con el navegador.

background_color (color de fondo): Cuando el navegador inicie la pantalla de bienvenida, será el fondo pantalla.

theme_color (color de tema): Será el color de fondo de la barra de estado cuando abramos la aplicación.

orientation (orientación): Le dice al navegador la orientación que debe tener al mostrar la aplicación.

icons (iconos): Cuando el navegador inicie la pantalla de bienvenida, será el icono que se muestre en la pantalla. Aquí utilicé todos los tamaños para tener compatibilidad con el icono preferido de cualquier dispositivo. Pero puedes usar uno o dos. Tú decides.

Ahora que tenemos un manifiesto de aplicación web, lo agregamos al archivo HTML.

  • En index.html (etiqueta head)
<link rel="manifest" href="manifest.json" />
<!-- Soporte para iOS -->
<link rel="apple-touch-icon" href="images/icons/icon-72x72.png" />
<link rel="apple-touch-icon" href="images/icons/icon-96x96.png" />
<link rel="apple-touch-icon" href="images/icons/icon-128x128.png" />
<link rel="apple-touch-icon" href="images/icons/icon-144x144.png" />
<link rel="apple-touch-icon" href="images/icons/icon-152x152.png" />
<link rel="apple-touch-icon" href="images/icons/icon-192x192.png" />
<link rel="apple-touch-icon" href="images/icons/icon-384x384.png" />
<link rel="apple-touch-icon" href="images/icons/icon-512x512.png" />
<meta name="apple-mobile-web-app-status-bar" content="#db4938" />
<meta name="theme-color" content="#db4938" />

Como puedes ver, vinculamos nuestro archivo manifest.json a la etiqueta head. Y agregamos algunos otros enlaces que manejan el soporte de iOS para mostrar los íconos y colorear la barra de estado con nuestro color de tema.

Ahora podemos pasar a la parte final e introducir el service worker.

¿Qué es un Service Worker?

Ten en cuenta que las PWA se ejecutan solo en https porque el service workers puede acceder a la solicitud y manejarla. Por lo tanto, se requiere seguridad.

Un service worker es un script que tu navegador ejecuta en segundo plano en un hilo separado. Eso significa que se ejecuta en un lugar diferente y está completamente separado de tu página web. Esa es la razón por la que no puede manipular elementos en el DOM.

Sin embargo, es superpoderoso. El service worker puede interceptar y manejar solicitudes de red, administrar el caché para habilitar el soporte fuera de línea o enviar notificaciones push a tus usuarios.

wow

Entonces, creemos nuestro primer service worker en la carpeta raíz nombrándolo serviceWorker.js (el nombre depende de ti). Pero tienes que ponerlo en la raíz para no limitar su alcance a una sola carpeta.

Caché de recursos

  • En serviceWorker.js
const staticDevCoffee = "dev-coffee-site-v1"
const assets = [
  "/",
  "/index.html",
  "/css/style.css",
  "/js/app.js",
  "/images/coffee1.jpg",
  "/images/coffee2.jpg",
  "/images/coffee3.jpg",
  "/images/coffee4.jpg",
  "/images/coffee5.jpg",
  "/images/coffee6.jpg",
  "/images/coffee7.jpg",
  "/images/coffee8.jpg",
  "/images/coffee9.jpg",
]

self.addEventListener("install", installEvent => {
  installEvent.waitUntil(
    caches.open(staticDevCoffee).then(cache => {
      cache.addAll(assets)
    })
  )
})

Este código parece intimidante al principio, pero es solo JavaScript (así que no te preocupes).

Declaramos el nombre de nuestro caché staticDevCoffee y los recursos (assets) para almacenar en el mismo. Para realizar esta acción, necesitamos adjuntar un event listener a self.

self es el propio service worker. Nos permite escuchar los eventos del ciclo de vida y hacer algo a cambio.

El service worker tiene varios ciclos de vida y uno de ellos es el evento install. Se ejecuta cuando se instala el service worker. Se activa tan pronto se ejecuta y solo es llamado una vez por cada service worker.

Cuando se dispara el evento install, ejecutamos el callback que nos da acceso al objecto event.

Almacenar cosas en la caché del navegador puede tardar un tiempo en finalizar porque es asíncrono.

Entonces, para manejarlo necesitamos usar el método waitUntil(), el cual espera a que termine la acción.

Una vez que la API de caché este lista, podemos ejecutar el método open() y crear nuestra caché pasando su nombre como argumento a caches.open(staticDevCoffee).

Luego esta devuelve una promesa, que nos ayuda a almacenar nuestros recursos en la caché con cache.addAll(assets).

image-cache

Con suerte, todavía estás conmigo.

desesperate

Ahora, hemos almacenado con éxito nuestros recursos en el caché del navegador. Y la próxima vez que carguemos la página, el service worker manejará la solicitud y buscará el caché si estamos fuera de línea.

Por lo que toca recuperar nuestro caché.

Fetch de recursos

  • En serviceWorker.js
self.addEventListener("fetch", fetchEvent => {
  fetchEvent.respondWith(
    caches.match(fetchEvent.request).then(res => {
      return res || fetch(fetchEvent.request)
    })
  )
})

Aquí usamos el evento fetch para recuperar nuestros datos. El callback nos da acceso a fetchEvent. Luego le adjuntamos respondWith() para evitar la respuesta predeterminada del navegador. En su lugar devuelve una promesa, ya que la acción de recuperación puede tardar un tiempo en completarse.

Y una vez listo el caché, aplicamos el método caches.match(fetchEvent.request). Este verificará si algo en el caché coincide con fetchEvent.request. Por cierto, fetchEvent.request es solo nuestro arreglo de recursos.

Luego, este devuelve una promesa. Y finalmente, podemos devolver el resultado si existe o el fetch inicial si no.

Ahora, nuestros recursos pueden ser almacenados en caché y recuperados por el service worker, lo que aumenta bastante el tiempo de carga de nuestras imágenes.

Y lo más importante, hace que nuestra aplicación esté disponible en modo fuera de línea.

Pero un service worker por si solo no puede hacer el trabajo. Necesitamos registrarlo en nuestro proyecto.

let-s-do-it

Registrando el Service Worker

  • En js/app.js
if ("serviceWorker" in navigator) {
  window.addEventListener("load", function() {
    navigator.serviceWorker
      .register("/serviceWorker.js")
      .then(res => console.log("service worker registered"))
      .catch(err => console.log("service worker not registered", err))
  })
}

Aquí, comenzamos verificando si serviceWorker es compatible con el navegador actual (ya que todavía no es compatible con todos los navegadores).

Luego, escuchamos el evento de carga de la página para registrar nuestro service worker pasando el nombre de nuestro archivo serviceWorker.js a navigator.serviceWorker.register() como parámetro para registrar nuestro worker.

Con esta actualización, ahora hemos transformado nuestra aplicación web habitual en una PWA.

we-did-it

Pensamientos finales

A lo largo de este artículo, hemos visto lo increíbles que pueden ser las PWAs. Al agregar un archivo de manifiesto de aplicación web y un service worker, se obtiene una mejora importante de la experiencia de usuario en nuestra aplicación web tradicional. Esto se debe a que las PWAs son rápidas, seguras, confiables y –  lo más importante –  habilitan el modo fuera de línea.

Muchos frameworks ahora vienen con un archivo de service worker ya configurado para nosotros. Pero saber cómo implementarlo puramente con JavaScript puede ayudarte a comprender mejor las PWAs.

Y puedes ir aún más lejos con los service workers, almacenando en caché los recursos de forma dinámica, limitando el tamaño de dicha caché, y mucho más.

Gracias por leer este artículo.

Puedes ver un demo de lo que creamos aquí y el código fuente acá.

Lee más de mis artículos en mi blog.

Traducido del artículo de Ibrahima Ndaw - How to build a PWA from scratch with HTML, CSS, and JavaScript