5. PWA básica

Versión para imprimir.

A. Introduccion

B. Hazlo funcionar

  1. Prueba e instala, de preferencia con Chrome, el sitio https://pwab.rf.gd/ y en https://gilpgpwa.github.io/.

  2. Descarga el archivo /src/pwab.zip y descompáctalo.

  3. Crea tu proyecto en GitHub pages:

    1. Crea una cuenta de email con el nombre de tu sitio, por ejemplo, miapp@google.com

    2. Crea una cuenta de GitHub usando el email anterior y selecciona el nombre de usuario unsando la parte inicial del correo electrónico, por ejemplo miapp.

    3. Crea un repositorio nuevo. En la página principal de GitHub cliquea 📘 New.

    4. En la página Create a new repository introduce los siguientes datos:

      • Proporciona el nombre de tu repositorio debajo de donde dice Repository name *. Debes usar el nombre de tu cuenta seguido por .github.io; por ejemplo miapp.github.io

      • Mantén la selección Public.

      • Verifica la casilla Add a README file. En este archivo se muestra información sobre tu proyecto.

      • Cliquea License: None. y selecciona la licencia que consideres más adecuada para tu proyecto.

      • Cliquea Create repository.

    5. Entra al repositorio y selecciona ⚙ Settings, luego selecciona 📁 Pages y en la sección Branches selecciona la carpeta donde se ubicará la carpeta. De preferencia selecciona / (root) para que coloques la página en la raíz del proyecto.

  4. Importa el proyecto en GitHub:

    1. En la página principal de tu proyecto en GitHub, en la pestaña < > Code, cliquea < > Code y en la sección Branches y copia la dirección que está en HTTPS, debajo de Clone.

    2. En Visual Studio Code, usa el botón de la izquierda para Source Control.

      Imagen de Source Control
    3. Cliquea el botón Clone Repository.

    4. Pega la url que copiaste anteriormente hasta arriba, donde dice algo como Provide repository URL y presiona la teclea Intro.

    5. Selecciona la carpeta donde se guardará la carpeta del proyecto.

    6. Abre la carpeta del proyecto importado.

    7. Añade el contenido de la carpeta descompactada que contiene el código del ejemplo, excepto el archivo .htaccess.

  5. Edita los archivos que desees.

  6. Crea los íconos del proyecto con https://www.photopea.com/. Este paso se hizo com Microsoft Edge (versión Chromium), pero no funcionó con Google Chrome.

  7. Crea los íconos enmascarables con https://maskable.app/ a partir del archivo «icono2048.png».

  8. Coloca el archivo favicon.ico en la raíz del proyecto.

  9. Coloca los otros íconos en la carpeta img y asegúrate de que estén declarados en el archivo site.webmanifest.

  10. El archivo sw.js tiene una lista de los archivos que se instalan. El archivo instruccionesListadoSw.txt te indica como generarla usando Visual Studio Code.

  11. Cada vez que modifiques los archivos, debes modificar el valor de VERSION en el archivo sw.js para poder ver los cambios en el navegador.

  12. Si tu proyecto no usa backend, haz clic derecho en index.html, selecciona Open with Live Server y se abre el navegador para que puedas probar localmente el ejemplo.

  13. Si tu proyecto usa PHP, haz clic derecho en index.html, selecciona PHP Server: serve project y se abre el navegador para que puedas probar localmente el ejemplo.

  14. Haz al menos una captura de la ventana de tu aplicación con orientación vertical y otra con orientación horizontal, todas ellas de 320 px como mínimo y 3,840 px como máximo.

  15. Coloca las capturas en la carpeta img y asegúrate de que estén declaradas en el archivo site.webmanifest.

  16. Cuando desarrolles, es incómodo modificar la versión cada que realizas cambios; en ves de ello desinstala la app:

    1. Abre las herramientas de depuración haciendo clic derecho en la página y selecciona Inspeccionar (o Inspect si aparece en inglés).

    2. En la Pestaña Aplicación (o Application en inglés) selecciona Almacenamoento (o Storage en inglés). Cliquea Borrar datos del sitio.

    3. Recarga la app, de preferencia haciendo clic derecho en el ícono de volver a cargar la página Ïmagen del ícono de recarga y seleccionando vaciar caché y volver a cargar de manera forzada (o algo parecido). Si no aparece un menú emergente, simplemente cliquea volver a cargar la página Ïmagen del ícono de recarga. Revisa que no aparezca ningún error ni en la pestañas Consola, ni en Red.

    4. Tanbién puedes usar la combinación de teclas Ctrl+Mayúsculas+r para forzar que se actualice temporalmente el navegador en caso de que no se vean los cambios.

    5. En la Pestaña Aplicación (o Application en inglés) selecciona Archivo de manifiesto (o Manifest file en inglés). Esta herramienta analiza la estructura del archivo de manifiesto y te indica si hay un error. InfinityFree bloquea el análisis de las imágenes.

    6. En la Pestaña Aplicación (o Application en inglés) selecciona Almacenamiento en caché (o Cache storage en inglés). Aquí puedes revisar si el caché de la aplicación se llenó correctamente. En caso de que esté vacío, es que hubo algún error durante la carga y la app se ejecuta más lenta.

  17. Cuando usas GitHub pages, antes de subir los archivos, no debes modificar el valor de VERSION en el archivo sw.js.

  18. Para subir el código a GitHub, en la sección de SOURCE CONTROL, en Message introduce un mensaje sobre los cambios que hiciste, por ejemplo index.html corregido, selecciona v y luego Commit & Push.

    Imagen de Commit & Push
  19. Si usas GitHub pages:

    1. Entra a la página de tu repositorio y abajo a la derecha, selecciona el enlace github-pages.

    2. Se muestran los despliegues realizados. Recarga la página hasta que apareca el mensaje de tu último push con una palomita dentro de un círculo verde.

    3. A partir de este momento, espera al menos 11 minutos para modificar el valor de VERSION en el archivo sw.js y volver a subir el proyecto.

  20. Si no usas GitHub pages:

    1. Sube el proyecto al hosting que elijas sin incluir el archivo .htaccess. En algunos casos puedes usar filezilla (https://filezilla-project.org/)

    2. En algunos host como InfinityFree, tienes que configurar el certificado SSL.

    3. En algunos host, como InfinityFree, debes subir el archivo .htaccess cuando el certificado SSL se ha creado e instalado. Sirve para forzar el uso de https, para eliminar el control de cache, pues ahora lo lleva el service worker y para asignar el mime type correcto para el archivo de manifest.

    4. Abre un navegador y prueba el proyecto en tu hosting.

    5. En el hosting InfinityFree, la primera ves que corres la página, puede marcar un mensaje de error, pero al recargar funciona correctamente. Puedes evitar este problema usando un dominio propio.

  21. Instala y usa tu PWA en Windows. Aunque en este video se recomienda usar Edge, al momento de actualizar el contenido, la opción más recomendada es Chrome para que te muestre las descripciones y las capturas de pantalla.

  22. Instala y usa tu PWA en Android. Al momento de actualizar las notas, tal vez no te aparezca el botón para instalar y tengas que seleccionar la acción de instalar que aparece en el menú de extensión de Chrome.

  23. Instala y usa tu PWA en iOS (iPhone y iPad).

C. Archivos

Haz clic en los triángulos para expandir las carpetas

D. index.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7
8 <title>PWA Básica</title>
9
10 <!-- Resumen para los motores de búsqueda. -->
11 <meta name="description" content="Ejemplo de PWA">
12
13 <script src="js/registraServiceWorker.js"></script>
14
15 <meta name="viewport" content="width=device-width">
16
17 <!-- Color de la barra de navegación de Chrome en dispositivos móviles. -->
18 <meta name="theme-color" content="#cbc693">
19
20 <!-- Ícono para la página web, que normalmente se pone en la raíz del sitio.
21 Puede ser diferente para cada página. -->
22 <link rel="icon" sizes="32x32" href="favicon.ico">
23
24 <!-- Configuración de la PWA para Chrome, Edge y Safari.
25 Debe ponerse en todas las páginas. -->
26 <link rel="manifest" href="site.webmanifest">
27
28 <link rel="stylesheet" href="css/estilos.css">
29
30</head>
31
32<body>
33
34 <h1>PWA Básica</h1>
35
36 <p>Hola mundo.</p>
37
38</body>
39
40</html>

E. Carpeta « css »

Versión para imprimir.

A. css / estilos.css

1html {
2 color-scheme: light dark;
3 font-family: sans-serif;
4}

F. Carpeta « js »

Versión para imprimir.

A. js / registraServiceWorker.js

1"use strict" // usa JavaScript en modo estricto.
2
3const nombreDeServiceWorker = "sw.js"
4
5try {
6 navigator.serviceWorker.register(nombreDeServiceWorker)
7 .then(registro => {
8 console.log(nombreDeServiceWorker, "registrado.")
9 console.log(registro)
10 })
11 .catch(error => console.log(error))
12} catch (error) {
13 console.log(error)
14}

G. site.webmanifest

  • Este archivo sirve para configurar los instaladores de la aplicación.

Explicación de las propiedades

short_name

Nombre corto. Normalmente se despliega en dispositivos móviles. Máximo 20 caracteres.

name

Nombre largo. Normalmente se despliega en computadoras de escritorio. Máximo 30 caracteres.

id

Identificador del archivo de instalación. Normalmente es la ruta del archivo inicial de la app.

start_url

Ruta del archivo inicial de la app.

display

Forma de mostrar la app. El término standalone significa que no se muestra la barra de navegación del navegador web.

theme_color

Color de la barra de estado (en dispositivos móviles) o de título (en computadoras de escritorio) de la app.

background_color

Color de fondo de la pantalla desplah en dispositivos móviles.

description

Describe el propósito de la aplicación. Aparece en el cuadro de diálogo que muestra el navegador al instalar la app.

screenshots

Listado de máximo 8 capturas de pantalla. Aparecen en el cuadro de diálogo que muestra el navegador al instalar la app. Debes incluir al menos una con "form_factor": "wide" y otra con "form_factor": "narrow".

  • El ancho y la altura deben ser de 320 px como mínimo y 3,840 px como máximo.

  • La dimensión máxima no puede ser más de 2.3 veces mayor que la dimensión mínima.

  • Todas las capturas de pantalla con el valor del mismo factor de forma deben tener relaciones de aspecto idénticas.

  • Solo se admiten los formatos de imagen JPEG y PNG.

  • Solo se mostrarán ocho capturas de pantalla. Si se agregan más, el usuario-agente simplemente los ignora.

  • https://web.dev/patterns/web-apps/richer-install-ui?hl=es-419

icons

Listado de íconos en distintas resoluciones para los instaladores de la app. Se selecciona el que se vea mejor según las característocas del dispositivo.

src

Url de la imagen dentro de la app.

sizes

Dimensiones en pixeles de la imagen, anchoxalto.

type

Tipo mime de la imagen.

purpose

Forma en que se usa la imagen.

maskable

La imagen puede recortarse de forma segura para tomar distintas formas, como círculos, gotas, cuadrados con esquinas redondeadas, etc. Normalmente se usa para dispositivos móviles.

any

No se puede asegurar nada sobre la imagen. Normalmente se usa para dispositivos de escritorio.

Normalmente debe proporcionarse un juego de íconos con purpose any y otro juego de íconos con purpose maskable.

form_factor

Orientación de una screenshot.

wide

La screenshot tiene una orientación horizontal. Normalmente la creenshot se usa para dispositivos de escritorio.

narrow

La screenshot tiene una orientación vertical. Normalmente la creenshot se usa para dispositivos móviles.

Debes incluir al menos una screenshot con "form_factor": "wide" y otra con "form_factor": "narrow".

label

Descripción de una screenshot. Aparece en el cuadro de diálogo que muestra el navegador al instalar la app.

1{
2 "short_name": "PWA",
3 "name": "Ejemplo de PWA",
4 "id": "/index.html",
5 "start_url": "/index.html",
6 "display": "standalone",
7 "theme_color": "#cbc693",
8 "background_color": "#ffffff",
9 "description": "Ejemplos básico de PWA.",
10 "screenshots": [
11 {
12 "src": "/img/screenshot_horizontal.png",
13 "sizes": "1507x777",
14 "type": "image/png",
15 "form_factor": "wide",
16 "label": "PWA Básica"
17 },
18 {
19 "src": "/img/screenshot_vertical.png",
20 "sizes": "591x980",
21 "type": "image/png",
22 "form_factor": "narrow",
23 "label": "PWA Básica (2)"
24 }
25 ],
26 "icons": [
27 {
28 "src": "/img/maskable_icon_x48.png",
29 "sizes": "48x48",
30 "type": "image/png",
31 "purpose": "any"
32 },
33 {
34 "src": "/img/maskable_icon_x72.png",
35 "sizes": "72x72",
36 "type": "image/png",
37 "purpose": "any"
38 },
39 {
40 "src": "/img/maskable_icon_x96.png",
41 "sizes": "96x96",
42 "type": "image/png",
43 "purpose": "any"
44 },
45 {
46 "src": "/img/maskable_icon_x128.png",
47 "sizes": "128x128",
48 "type": "image/png",
49 "purpose": "any"
50 },
51 {
52 "src": "/img/maskable_icon_x192.png",
53 "sizes": "192x192",
54 "type": "image/png",
55 "purpose": "any"
56 },
57 {
58 "src": "/img/maskable_icon_x384.png",
59 "sizes": "384x384",
60 "type": "image/png",
61 "purpose": "any"
62 },
63 {
64 "src": "/img/maskable_icon_x512.png",
65 "sizes": "512x512",
66 "type": "image/png",
67 "purpose": "any"
68 },
69 {
70 "src": "/img/maskable_icon.png",
71 "sizes": "2730x2730",
72 "type": "image/png",
73 "purpose": "any"
74 },
75 {
76 "src": "/img/icono2048.png",
77 "sizes": "2048x2048",
78 "type": "image/png",
79 "purpose": "any"
80 },
81 {
82 "src": "/img/maskable_icon_x48.png",
83 "sizes": "48x48",
84 "type": "image/png",
85 "purpose": "maskable"
86 },
87 {
88 "src": "/img/maskable_icon_x72.png",
89 "sizes": "72x72",
90 "type": "image/png",
91 "purpose": "maskable"
92 },
93 {
94 "src": "/img/maskable_icon_x96.png",
95 "sizes": "96x96",
96 "type": "image/png",
97 "purpose": "maskable"
98 },
99 {
100 "src": "/img/maskable_icon_x128.png",
101 "sizes": "128x128",
102 "type": "image/png",
103 "purpose": "maskable"
104 },
105 {
106 "src": "/img/maskable_icon_x192.png",
107 "sizes": "192x192",
108 "type": "image/png",
109 "purpose": "maskable"
110 },
111 {
112 "src": "/img/maskable_icon_x384.png",
113 "sizes": "384x384",
114 "type": "image/png",
115 "purpose": "maskable"
116 },
117 {
118 "src": "/img/maskable_icon_x512.png",
119 "sizes": "512x512",
120 "type": "image/png",
121 "purpose": "maskable"
122 },
123 {
124 "src": "/img/maskable_icon.png",
125 "sizes": "2730x2730",
126 "type": "image/png",
127 "purpose": "maskable"
128 }
129 ]
130}

H. instruccionesListadoSw.txt

1Generar el listado de archivos del sw.js desde Visual Studio Code.
21. Abrir una terminal desde el menú
3 Terminal > New Terminal
4
52. Desde la terminal introducir la orden:
6 Get-ChildItem -path . -Recurse | Select Directory,Name | Out-File archivos.txt
7
83. Abrir el archivo generado, que se llama
9 archivos.txt
10 y sobre este, realizar los pasos que siguen:
11
124. Quita del archivo archivos.txt:
13 * el encabezado,
14 * todas las carpetas,
15 * el archivo .vscode/settings.json,
16 * el archivo .htaccess,
17 * el archivo archivos.txt,
18 * este archivo (instruccionesListadoSw.txt),
19 * el archivo jsconfig.json,
20 * el archivo sw.js,
21 * el archivo de la base de datos, que termina en ".bd" y
22 está en la carpeta srv,
23 * todos los archivos de php y
24 * las líneas en blanco del final
25
265. Cambia los \ por / desde Visual Studio Code con las siguientes
27 combinaciones de teclas:
28
29 Ctrl+H En el diálogo que aparece introduce lo siguiente:
30 Find:\
31 Replace:/
32
33 Clic en el icono Reemplaza todo o Replace All y luego teclea ESC
34
356. Coloca las comillas y coma del final de cada línea desde Visual
36 Studio Code con las siguientes combinaciones de teclas:
37
38 Ctrl+H En el diálogo que aparece, selecciona el botón
39 ".*"
40 e introduce lo siguiente:
41 Find:\s*$
42 Replace:",
43
44 Clic en el icono Reemplaza todo o Replace All y luego teclea ESC
45
467. Marca la carpeta inicial, presiona la combinación de teclas:
47
48 Shift+Ctrl+L
49
50 borra la selección, teclea " y luego ESC
51
528. Cambia las secuencias de espacios por / con las siguientes
53 combinaciones de teclas:
54
55 Ctrl+H En el diálogo que aparece, selecciona el botón
56 ".*"
57 e introduce lo siguiente:
58 Find:\s+
59 Replace:/
60
61 Clic en el icono Reemplaza todo o Replace All y luego teclea ESC
62
639. Cambia las "/ por " con las siguientes combinaciones de teclas:
64
65 Ctrl+H En el diálogo que aparece, quita la selección del botón
66 ".*"
67 e introduce lo siguiente:
68 Find:"/
69 Replace:"
70
71 Clic en el icono Reemplaza todo o Replace All y luego teclea ESC
72
7310. Copia el texto al archivo
74 sw.js
75 en el contenido del arreglo llamado ARCHIVOS, pero recuerda
76 mantener el último elemento, que dice:
77 "/"

I. archivos.txt

1"favicon.ico",
2"index.html",
3"site.webmanifest",
4"css/estilos.css",
5"img/icono2048.png",
6"img/maskable_icon.png",
7"img/maskable_icon_x128.png",
8"img/maskable_icon_x192.png",
9"img/maskable_icon_x384.png",
10"img/maskable_icon_x48.png",
11"img/maskable_icon_x512.png",
12"img/maskable_icon_x72.png",
13"img/maskable_icon_x96.png",
14"img/screenshot_horizontal.png",
15"img/screenshot_vertical.png",
16"js/registraServiceWorker.js",

J. sw.js

1/* Este archivo debe estar colocado en la carpeta raíz del sitio.
2 *
3 * Cualquier cambio en el contenido de este archivo hace que el service
4 * worker se reinstale. */
5
6/**
7 * Cambia el número de la versión cuando cambia el contenido de los
8 * archivos.
9 *
10 * El número a la izquierda del punto (.), en este caso <q>1</q>, se
11 * conoce como número mayor y se cambia cuando se realizan
12 * modificaciones grandes o importantes.
13 *
14 * El número a la derecha del punto (.), en este caso <q>00</q>, se
15 * conoce como número menor y se cambia cuando se realizan
16 * modificaciones menores.
17 */
18const VERSION = "1.00"
19
20/** Nombre del archivo de cache. */
21const CACHE = "ejemploPWA"
22
23/**
24 * Archivos requeridos para que la aplicación funcione fuera de
25 * línea.
26 */
27const ARCHIVOS = [
28 "favicon.ico",
29 "index.html",
30 "site.webmanifest",
31 "css/estilos.css",
32 "img/icono2048.png",
33 "img/maskable_icon.png",
34 "img/maskable_icon_x128.png",
35 "img/maskable_icon_x192.png",
36 "img/maskable_icon_x384.png",
37 "img/maskable_icon_x48.png",
38 "img/maskable_icon_x512.png",
39 "img/maskable_icon_x72.png",
40 "img/maskable_icon_x96.png",
41 "img/screenshot_horizontal.png",
42 "img/screenshot_vertical.png",
43 "js/registraServiceWorker.js",
44 "/"
45]
46
47// Verifica si el código corre dentro de un service worker.
48if (self instanceof ServiceWorkerGlobalScope) {
49 // Evento al empezar a instalar el servide worker,
50 self.addEventListener("install",
51 (/** @type {ExtendableEvent} */ evt) => {
52 console.log("El service worker se está instalando.")
53 evt.waitUntil(llenaElCache())
54 })
55
56 // Evento al solicitar información a la red.
57 self.addEventListener("fetch", (/** @type {FetchEvent} */ evt) => {
58 if (evt.request.method === "GET") {
59 evt.respondWith(buscaLaRespuestaEnElCache(evt))
60 }
61 })
62
63 // Evento cuando el service worker se vuelve activo.
64 self.addEventListener("activate",
65 () => console.log("El service worker está activo."))
66}
67
68async function llenaElCache() {
69 console.log("Intentando cargar caché:", CACHE)
70 // Borra todos los cachés.
71 const keys = await caches.keys()
72 for (const key of keys) {
73 await caches.delete(key)
74 }
75 // Abre el caché de este service worker.
76 const cache = await caches.open(CACHE)
77 // Carga el listado de ARCHIVOS.
78 await cache.addAll(ARCHIVOS)
79 console.log("Cache cargado:", CACHE)
80 console.log("Versión:", VERSION)
81}
82
83/** @param {FetchEvent} evt */
84async function buscaLaRespuestaEnElCache(evt) {
85 // Abre el caché.
86 const cache = await caches.open(CACHE)
87 const request = evt.request
88 /* Busca la respuesta a la solicitud en el contenido del caché, sin
89 * tomar en cuenta la parte después del símbolo "?" en la URL. */
90 const response = await cache.match(request, { ignoreSearch: true })
91 if (response === undefined) {
92 /* Si no la encuentra, empieza a descargar de la red y devuelve
93 * la promesa. */
94 return fetch(request)
95 } else {
96 // Si la encuentra, devuelve la respuesta encontrada en el caché.
97 return response
98 }
99}

K. favicon.ico

favicon.ico

L. Carpeta « img »

Versión para imprimir.

A. img / icono2048.png

icono2048.png

B. img / maskable_icon.png

maskable_icon.png

C. img / maskable_icon_x128.png

maskable_icon_x128.png

D. img / maskable_icon_x192.png

maskable_icon_x192.png

E. img / maskable_icon_x384.png

maskable_icon_x384.png

F. img / maskable_icon_x48.png

maskable_icon_x48.png

G. img / maskable_icon_x512.png

maskable_icon_x512.png

H. img / maskable_icon_x72.png

maskable_icon_x72.png

I. img / maskable_icon_x96.png

maskable_icon_x96.png

J. img / screenshot_horizontal.png

screenshot_horizontal.png

K. img / screenshot_vertical.png

screenshot_vertical.png

M. .htaccess

1AddType application/manifest+json .webmanifest
2
3ExpiresActive On
4
5Header set Cache-Control "max-age=1, must-revalidate"
6
7RewriteEngine On
8
9RewriteCond %{HTTP:X-Forwarded-Proto} !https
10RewriteCond %{HTTPS} off
11RewriteCond %{HTTP:CF-Visitor} !{"scheme":"https"}
12RewriteRule (.*) https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]
13

N. jsconfig.json

1{
2 "compilerOptions": {
3 "checkJs": true,
4 "strictNullChecks": true,
5 "target": "ES6",
6 "module": "Node16",
7 "moduleResolution": "Node16",
8 "lib": [
9 "ES2017",
10 "WebWorker",
11 "DOM"
12 ]
13 }
14}

O. Resumen