Cloud, DevOps e IoT en español

Parte 15 de 22 de la serie Domótica con ESP32 y AWS desde cero

Convertir un panel web en PWA con Vite (y que se actualice)

9 de octubre de 2026 · Steven Carvajal · Tutoriales

Código de este artículo en GitHub →

Nadie quiere abrir el navegador, escribir una dirección e iniciar sesión cada vez que quiere encender una luz. El panel web de la parte 10 tiene que sentirse como una app del celular: un ícono en la pantalla de inicio, que se abre sin la barra del navegador y con la sesión guardada.

Eso es una PWA (Progressive Web App), y con Vite se consigue con un plugin. En esta parte vemos la configuración, dónde publicarla sin pagar y cómo evitar el problema más común: usuarios atrapados en una versión vieja.

Qué necesita una PWA

Para que el navegador ofrezca "instalar" la app y la abra como una app independiente hacen falta tres cosas:

  1. Un manifiesto web con el nombre, los colores, los íconos y el modo de visualización.
  2. Un service worker, un script que el navegador ejecuta aparte de la página y que puede guardar los archivos en caché.
  3. HTTPS.

vite-plugin-pwa genera las dos primeras a partir de la configuración de Vite, usando Workbox por debajo.

La configuración

import react from "@vitejs/plugin-react";
import { defineConfig } from "vite";
import { VitePWA } from "vite-plugin-pwa";

export default defineConfig({
  plugins: [
    react(),
    VitePWA({
      registerType: "autoUpdate",
      injectRegister: false,  // el registro se hace a mano, ver más abajo
      includeAssets: ["favicon-32x32.png", "apple-touch-icon.png"],
      workbox: {
        globIgnores: ["**/descargas/**"],  // no precachear archivos grandes que casi nadie usa
      },
      manifest: {
        name: "Casa Demo",
        short_name: "Casa",
        description: "Panel de control de la casa",
        lang: "es",
        theme_color: "#0f766e",
        background_color: "#ffffff",
        display: "standalone",  // se abre sin la barra del navegador
        start_url: "/",
        scope: "/",
        icons: [
          { src: "pwa-192x192.png", sizes: "192x192", type: "image/png" },
          { src: "pwa-512x512.png", sizes: "512x512", type: "image/png" },
          { src: "pwa-512x512.png", sizes: "512x512", type: "image/png", purpose: "maskable" },
        ],
      },
    }),
  ],
});

Algunos detalles:

El iPhone necesita sus propias etiquetas

En iOS no hay un botón de "instalar": se usa Safari, Compartir → Agregar a pantalla de inicio. Para que se abra en modo app y con el ícono correcto, conviene incluir estas etiquetas en el index.html:

<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
<meta name="theme-color" content="#0f766e" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
<meta name="apple-mobile-web-app-title" content="Casa" />

El apple-touch-icon es un PNG de 180 × 180 píxeles.

Dónde publicarla

Una PWA es un sitio estático: archivos HTML, CSS y JavaScript. Se puede publicar gratis en Netlify, Cloudflare Pages o similares, o en S3 con CloudFront por centavos. Las diferencias, incluidas las condiciones de uso comercial de cada plan gratuito, están en dónde alojar una web app estática.

Antes de elegir, comprueba que nada ata la app a un dominio:

Y no olvides la regla de una SPA: cualquier ruta que no sea un archivo debe devolver index.html (en Netlify, /* /index.html 200 en el archivo _redirects).

Por qué una pestaña abierta no se actualiza

Con registerType: "autoUpdate" parece que la app se actualizará sola al publicar una versión nueva. Pero hay un detalle: el navegador solo busca un service worker nuevo cuando navega a la página (o, como mucho, cada 24 horas). Una pestaña abierta durante días, o una PWA que el celular mantiene en segundo plano, puede seguir ejecutando el JavaScript viejo que tiene en memoria.

El service worker nuevo puede estar listo para tomar el control (skipWaiting() y clientsClaim()), pero si nadie pregunta si hay uno nuevo, no llega.

La solución: registrar el service worker a mano y preguntar

Desactiva el registro automático (injectRegister: false, como en la configuración de arriba) y registra con el hook del propio plugin, agregando una comprobación periódica:

import { useRegisterSW } from "virtual:pwa-register/react";

const BUSCAR_ACTUALIZACION_MS = 30 * 60 * 1000;  // cada 30 minutos

export default function App() {
  useRegisterSW({
    onRegisteredSW(_swUrl, registration) {
      if (!registration) return;
      setInterval(() => registration.update(), BUSCAR_ACTUALIZACION_MS);
    },
  });

  // ...
}

Con registerType: "autoUpdate", el hook recarga la página en cuanto detecta un service worker nuevo. Lo que faltaba era volver a preguntar mientras la app sigue abierta.

Para que TypeScript reconozca el módulo virtual, agrega sus tipos en tsconfig.app.json:

{
  "compilerOptions": {
    "types": ["vite-plugin-pwa/client"]
  }
}

Si prefieres no recargar sin avisar (por ejemplo, si el usuario podría estar escribiendo algo), usa registerType: "prompt" y muestra un botón de "Hay una versión nueva" con el valor needRefresh del mismo hook.

Comprobar que todo funciona

Preguntas frecuentes

¿La app funciona sin internet?

Abre, porque los archivos están en caché, pero no puede controlar nada: el control pasa por la nube. Para una app de domótica en la nube, el beneficio real de la PWA es la instalación y la apertura rápida, no el modo sin conexión.

¿Por qué no publicar en la App Store?

Porque una PWA se instala sin revisión de Apple ni cuenta de desarrollador, y se actualiza al instante. A cambio, en iOS tiene limitaciones, como las notificaciones, que solo funcionan con la app instalada en la pantalla de inicio.

¿Cada cuánto conviene buscar actualizaciones?

Depende de cuánto te importe que todos usen la última versión. Treinta minutos es un buen equilibrio: casi no genera tráfico y ninguna pestaña queda desactualizada por días.

Sigue leyendo