D. Carpeta « public »

Versión para imprimir.

A. public / .htaccess

1
AddType application/manifest+json .webmanifest
2
3
ExpiresActive On
4
5
Header set Cache-Control "max-age=1, must-revalidate"
6

B. public / 404.html

1
<!DOCTYPE html>
2
<html>
3
  <head>
4
    <meta charset="utf-8">
5
    <meta name="viewport" content="width=device-width, initial-scale=1">
6
    <title>Page Not Found</title>
7
8
    <style media="screen">
9
      body { background: #ECEFF1; color: rgba(0,0,0,0.87); font-family: Roboto, Helvetica, Arial, sans-serif; margin: 0; padding: 0; }
10
      #message { background: white; max-width: 360px; margin: 100px auto 16px; padding: 32px 24px 16px; border-radius: 3px; }
11
      #message h3 { color: #888; font-weight: normal; font-size: 16px; margin: 16px 0 12px; }
12
      #message h2 { color: #ffa100; font-weight: bold; font-size: 16px; margin: 0 0 8px; }
13
      #message h1 { font-size: 22px; font-weight: 300; color: rgba(0,0,0,0.6); margin: 0 0 16px;}
14
      #message p { line-height: 140%; margin: 16px 0 24px; font-size: 14px; }
15
      #message a { display: block; text-align: center; background: #039be5; text-transform: uppercase; text-decoration: none; color: white; padding: 16px; border-radius: 4px; }
16
      #message, #message a { box-shadow: 0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.24); }
17
      #load { color: rgba(0,0,0,0.4); text-align: center; font-size: 13px; }
18
      @media (max-width: 600px) {
19
        body, #message { margin-top: 0; background: white; box-shadow: none; }
20
        body { border-top: 16px solid #ffa100; }
21
      }
22
    </style>
23
  </head>
24
  <body>
25
    <div id="message">
26
      <h2>404</h2>
27
      <h1>Page Not Found</h1>
28
      <p>The specified file was not found on this website. Please check the URL for mistakes and try again.</p>
29
      <h3>Why am I seeing this?</h3>
30
      <p>This page was generated by the Firebase Command-Line Interface. To modify it, edit the <code>404.html</code> file in your project's configured <code>public</code> directory.</p>
31
    </div>
32
  </body>
33
</html>

C. public / favicon.ico

favicon.ico

D. public / 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 type="module" 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. public / 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".

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
}

F. public / 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
 */
18
const VERSION = "1.00"
19
20
/** Nombre del archivo de cache. */
21
const CACHE = "ejemploPWA"
22
23
/**
24
 * Archivos requeridos para que la aplicación funcione fuera de
25
 * línea.
26
 */
27
const 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.
48
if (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
68
async 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 */
84
async 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
}

G. Carpeta « public / css »

Versión para imprimir.

1. public / css / estilos.css

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

H. 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

I. Carpeta « public / js »

Versión para imprimir.

1. public / js / registraServiceWorker.js

1
const nombreDeServiceWorker = "sw.js"
2
3
try {
4
 navigator.serviceWorker.register(nombreDeServiceWorker)
5
  .then(registro => {
6
   console.log(nombreDeServiceWorker, "registrado.")
7
   console.log(registro)
8
  })
9
  .catch(error => console.log(error))
10
} catch (error) {
11
 console.log(error)
12
}