Hoy voy a enseñarte cómo convertir cualquier aplicación web en una PWA o Progressive Web App.
Tú puedes programar en cualquier lenguaje y Framework, ya que las PWA no están atadas a algo más allá de JavaScript.
Así que mientras tu app conste de archivos del lado del cliente, podrás convertirla en PWA. Y con estos archivos me refiero a CSS, JS, HTML, imágenes, etcétera.
Te repito que para crear una PWA no necesitas usar un framework específico, así que puedes usar JavaScript puro, Angular, Vue, React, jQuery (bueno, jQuery no, ya es obsoleto) y cualquier otro que genere JavaScript al final.
¿Qué son las PWA?
Si quieres leer teoría, ve a la Wikipedia o busca en Google.
Yo uso las PWA porque se pueden instalar en el dispositivo como si fueran nativas, permiten agregar nuestros archivos al caché y básicamente nos permiten hacer apps “nativas” desde la web usando cualquier framework.
De este modo le das al usuario la posibilidad de instalar tu app y trabajar fuera de línea, así que él puede visitar tu app web una sola vez e instalar la app nativamente en Android, iOS o en la computadora con Windows, MacOS o Linux.
Limitaciones
Veo las PWA como un estándar, y no como una magia. Básicamente una Progressive Web App te permitirá, entre otras cosas, trabajar sin conexión e instalar tu webapp como si fuera nativa; pero no es magia.
Con esto me refiero a que si quieres trabajar sin conexión tú debes implementar esa lógica. Si usas un lenguaje del servidor o una API y el usuario no tiene internet, obviamente no va a poder usar tu app aunque la misma sea una PWA.
Las PWA solo guardan en caché los assets, scripts, estilos, etcétera; pero no guardan una porción de internet.
Además, no todas las apps usan conexión a internet o guardan datos, pero si quieres trabajar sin conexión puedes usar Firebase, PouchDB o localStorage.
Generando manifesto
Hasta este punto y para los siguientes voy a suponer que ya tienes tu app final que solo consta de archivos web del lado del cliente.
Bien, comenzamos creando el manifiesto y colocándolo junto a tus assets para luego importarlo en el HTML. Siéntete libre de modificarlo y adaptarlo a tus necesidades:
{
"name": "Deudas",
"short_name": "Deudas",
"theme_color": "#2196f3",
"background_color": "#2196f3",
"display": "fullscreen",
"orientation": "portrait",
"scope": "./",
"start_url": "./",
"icons": [
{
"src": "./images/icono-512.png",
"type": "image/png",
"sizes": "512x512"
}
]
}
En este caso icono-512
es la imagen del tamaño 512×512. Se recomienda que las imágenes sean cuadradas. Obviamente el src
puede cambiar dependiendo de tu proyecto. Puedes agregar varias imágenes para distintos tamaños.
Después importamos el manifiesto en el index.html
, obviamente si tú cambiaste la ruta entonces cambia lo necesario:
<link rel="manifest" href="./manifest.json">
Luego vamos a regresar al index.html
a registrar el Service Worker, pero primero debemos crearlo.
Instalando workbox
Workbox es una herramienta que te va a permitir generar el Service Worker automáticamente. Tú puedes generarlo a mano, pero yo recomiendo siempre usar herramientas de automatización.
No es nada difícil, ya lo verás. Solo necesitas NPM y Node. Una vez que cuentes con Node y NPM ejecuta lo siguiente para instalarlo:
npm install workbox-cli --global
Y espera a que se instale. Luego de eso, reinicia todas las terminales que tengas abiertas. Por cierto, para los siguientes pasos voy a ejecutar los comandos en la ubicación donde tengo mi proyecto.
Generando archivo de configuración para service worker
Ya tenemos workbox, ahora vamos a generar un archivo de configuración. Este archivo le dice a workbox cómo debe crear nuestro service worker. Para ello vamos a invocar un asistente.
En mi caso tengo mi proyecto con la CLI de Vue, y al compilar me genera el JS, CSS y HTML en la carpeta dist
, por ello es que en los siguientes pasos le diré al asistente que mi proyecto está en dist
.
Si tú tienes otra configuración o carpeta, entonces muévete a esa ruta antes de seguir los pasos. Pero bueno, comenzamos ejecutando:
workbox wizard
Elegimos el directorio, en mi caso es dist
. Luego marcamos los archivos que queremos cachear, podemos marcar y desmarcar con la tecla de espacio.
Yo dejé todas marcadas y presioné Enter. Cuando me preguntó en dónde guardar el archivo de service-worker
acepté la ruta, que fue en la carpeta dist/sw.js
.
Nota: una cosa es el archivo de configuración, y otra cosa es el service worker. Workbox lee el archivo de configuración y genera el service worker.
Por otro lado, las configuraciones las guardé en la carpeta padre de dist
, tal y como el asistente me lo sugirió.
Cuando me preguntó si quería eso de los parámetros de búsqueda le di N. A ti te puede preguntar otras cosas en el futuro, solo tienes que leer y responder. Luego me dijo:
To build your service worker, run
workbox generateSW workbox-config.js
as part of a build process. See https://goo.gl/fdTQBf for details.
You can further customize your service worker by making changes to workbox-config.js. See https://goo.gl/gVo87N for details.
Llegados a este punto ya podemos generar nuestro service worker.
Generando service worker
Tal como lo dijo la salida, ahora solo debemos ejecutar: workbox generateSW workbox-config.js
. A ti te puede dar otra salida obviamente.
Con esto le indicamos a workbox
que genere el service worker tomando en cuenta el archivo workbox-config.js
, que ya tiene los ajustes que pusimos anteriormente y que podemos modificar a nuestro gusto.
Nota: al ejecutarlo, me generó 4 archivos, pero los .map no son necesarios. Así que solo toma los 2 js, que son
sw.js
yworkbox-letras-y-números.js
Siempre que ejecutes el comando, se va a crear el service worker. Es recomendable ejecutarlo siempre que hagas cambios o cambies archivos.
En mi caso ejecuto npm run build
para compilar mi app, misma que me genera el directorio dist
. E inmediatamente después de eso ejecuto workbox generateSW workbox-config.js
para que genere el service worker.
Nota: cada que hagas un cambio debes generar el ServiceWorker de nuevo. Presta atención; dije Service Worker, y no archivo de configuración, pues el archivo de configuración se debe generar una vez por proyecto.
Volviendo al index.html
Ya tenemos nuestro ServiceWorker que en mi caso se ve así:
if(!self.define){let e,c={};const i=(i,n)=>(i=new URL(i+".js",n).href,c[i]||new Promise((c=>{if("document"in self){const e=document.createElement("script");e.src=i,e.onload=c,document.head.appendChild(e)}else e=i,importScripts(i),c()})).then((()=>{let e=c[i];if(!e)throw new Error(`Module ${i} didn’t register its module`);return e})));self.define=(n,s)=>{const o=e||("document"in self?document.currentScript.src:"")||location.href;if(c[o])return;let r={};const t=e=>i(e,o),f={module:{uri:o},exports:r,require:t};c[o]=Promise.all(n.map((e=>f[e]||t(e)))).then((e=>(s(...e),r)))}}define(["./workbox-ac6e2db0"],(function(e){"use strict";self.addEventListener("message",(e=>{e.data&&"SKIP_WAITING"===e.data.type&&self.skipWaiting()})),e.precacheAndRoute([{url:"css/chunk-vendors.0af22a31.css",revision:"e2e858b4d120eb644b8259240d7ec8d9"},{url:"favicon.ico",revision:"1dc6fa57e5d5fbca02aad225ca0f9e1c"},{url:"fonts/materialdesignicons-webfont.9cacdc87.eot",revision:"9cacdc876e2049988fcab540c21738d5"},{url:"fonts/materialdesignicons-webfont.9d243c16.woff2",revision:"9d243c168a4f1c2cb3cec74884344de7"},{url:"fonts/materialdesignicons-webfont.a0711490.woff",revision:"a0711490bcd581b647329230b3e915cf"},{url:"fonts/materialdesignicons-webfont.b62641af.ttf",revision:"b62641afc9ab487008e996a5c5865e56"},{url:"images/icono-512.png",revision:"785dd4631c1bcc23a9bc4c253531a688"},{url:"index.html",revision:"e739386a271e6fa79521075247284d79"},{url:"js/app.47c9b442.js",revision:"1e44598bc22c04452f367b89706f60ee"},{url:"js/chunk-vendors.fc19b828.js",revision:"2fdc8172c8f00b6ba891a822bd55cd6d"},{url:"manifest.json",revision:"062998d4565fc1a85f585d673c08aeea"}],{ignoreURLParametersMatching:[/^utm_/,/^fbclid$/]})}));
//# sourceMappingURL=sw.js.map
También es importante copiar el archivo workbox-letras-y-números.js
que Workbox generó, y este archivo debe estar en el mismo directorio que el sw.js
.
Vamos al index.html
, en el head
registramos el service worker (recuerda que igualmente ahí debemos importar el manifiesto):
<script type="text/javascript">
if ("serviceWorker" in navigator) {
navigator.serviceWorker.register("sw.js");
}
</script>
Obviamente sw.js
está en la misma carpeta que mi index.html
, así que por eso lo registro de esa manera. Si el tuyo está en otra ubicación o tiene otro nombre cambia lo que sea necesario.
Verificando PWA
Entonces ya tenemos toda nuestra app, el manifiesto y el service worker. En mi caso se ve así:
Sí, ya sé que se me pasaron los .map
pero en un momento los eliminaré. El punto es que esa es mi webapp completa, ya lista para enviarla a producción.
Es momento de subirla a un servidor HTTPS, y si no tienes ninguno puedes usar las Github pages, ya que al final solo estamos usando archivos del lado del cliente.
Una vez subida navega hasta tu app web recién publicada, abre la consola de depuración y ve a Application > Manifest, verás que tu manifiesto es válido y se debe ver algo así:
Luego en Service Workers también podrás ver que el archivo generador por workbox ha funcionado:
Llegados a este punto ya puedes instalar tu web app progresiva como si fuera nativa, ya sea en la computadora o en un móvil. Así se ve en mi teléfono con Android, lista para ser instalada:
Además, si el usuario visita nuestra app progresiva varias veces, aparece la sugerencia para instalar en la parte inferior. Y así es como podemos crear una PWA con nuestros archivos web, sin importar el framework o tecnologías usadas.
Posible error
En julio de 2023 intenté usar estos comandos y me aparecía el siguiente error:
Please pass in a valid CommonJS module that exports your configuration.
require() of ES Module C:\Users\parzibyte\Documents\desarrollo\svelte\notas_sqlite_svelte\workbox-config.js from C:\Users\parzibyte\AppData\Roaming\npm\node_modules\workbox-cli\build\lib\read-config.js not supported.
workbox-config.js is treated as an ES module file as it is a .js file whose nearest parent package.json contains “type”: “module” which declares all .js files in that package scope as ES modules.Instead rename workbox-config.js to end in .cjs, change the requiring code to use dynamic import() which is available in all CommonJS modules, or change “type”: “module” to “type”: “commonjs” in C:\Users\parzibyte\Documents\desarrollo\svelte\notas_sqlite_svelte\package.json to treat all .js files as CommonJS (using .mjs for all ES modules instead).
Me imagino que es debido a la versión de Node y NPM que tengo. Por si sirve, mis versiones son:
- Node: v18.16.0
- npm: 9.6.7
Lo solucioné renombrando el workbox-config.js
a workbox-config.cjs
.
Bonus: creando PWA con Vue 3
En estos días he retomado este tutorial y lo he ajustado para enseñarte cómo convertir una webapp de Vue 3 a PWA:
Estoy realizando una pwa de ejemplo para aprender como funcionan.
He seguido todos los pasos pero no me reconoce el service worker en el navegador.
Se ha generado correctamente dentro de mis directorios, pero a la hora de ver si mi web se ha convertido en pwa solo reconoce el manifest, estando el sw registrado en la cabecera de mis archivos .html, alguna idea de que puede fallar?
Hola. Gracias por sus comentarios. Si tiene alguna consulta, solicitud de creación de un programa o solicitud de cambio de software estoy para servirle en https://parzibyte.me/#contacto
Saludos!