Mejora progresivamente tu app web progresiva

Compilación para navegadores modernos y mejora progresiva como en 2003

Publicado: 29 de junio de 2020

En marzo de 2003, Nick Finck y Steve Champeon sorprendieron al mundo del diseño web con el concepto de mejora progresiva, una estrategia de diseño web que enfatiza la carga del contenido principal de la página web primero y, luego, agrega progresivamente capas de presentación y funciones más matizadas y técnicamente rigurosas sobre el contenido. En 2003, la mejora progresiva se trataba de usar funciones CSS modernas (en ese momento), JavaScript discreto y hasta gráficos vectoriales escalables. La mejora progresiva en 2020 y en adelante se trata de usar capacidades modernas del navegador.

Diseño web inclusivo para el futuro con mejora progresiva. Diapositiva de título de la presentación original de Finck y Champeon.

JavaScript moderno

Hablando de JavaScript, la situación de compatibilidad del navegador con las funciones principales más recientes de ES 2015 JavaScript es excelente. El nuevo estándar incluye promesas, módulos, clases, literales de plantilla, funciones de flecha, let y const, parámetros predeterminados, generadores, asignación de desestructuración, rest y spread, Map/Set, WeakMap/WeakSet y muchos más. Todos son compatibles.

La tabla de compatibilidad de CanIUse para las funciones de ES6 que muestra la compatibilidad en todos los navegadores principales.
Tabla de compatibilidad del navegador con ECMAScript 2015 (ES6). (Fuente)

Las funciones async, una función de ES 2017 y una de mis favoritas personales, se pueden usar en todos los navegadores principales. Las palabras clave async y await permiten escribir un comportamiento asíncrono basado en promesas con un estilo más limpio, lo que evita la necesidad de configurar de forma explícita cadenas de promesas.

La tabla de compatibilidad de CanIUse para las funciones asíncronas que muestra la compatibilidad en todos los navegadores principales.
Tabla de compatibilidad del navegador con las funciones asíncronas. (Fuente)

Incluso las incorporaciones de lenguaje de ES 2020 muy recientes, como encadenamiento opcional y fusión de nulos, alcanzaron la compatibilidad muy rápido. En cuanto a las funciones principales de JavaScript, no hay mucho que mejorar.

Por ejemplo:

const adventurer = {
  name: 'Alice',
  cat: {
    name: 'Dinah',
  },
};
console.log(adventurer.dog?.name);
// Expected output: undefined
console.log(0 ?? 42);
// Expected output: 0
La icónica imagen de fondo de césped verde de Windows XP.
Todo está bien en cuanto a las funciones principales de JavaScript. (Captura de pantalla del producto de Microsoft, utilizada con permiso).

La app de ejemplo: Fugu Greetings

En este documento, trabajo con una PWA llamada Fugu Greetings (GitHub). El nombre de esta app es un guiño al Proyecto Fugu 🐡, un esfuerzo por brindar a la Web todas las capacidades de las aplicaciones para Android, iOS y computadoras de escritorio. Puedes obtener más información sobre el proyecto en su página de destino.

Fugu Greetings es una app de dibujo que te permite crear tarjetas de felicitación virtuales y enviárselas a tus seres queridos. Ejemplifica los conceptos básicos de las PWA. Es confiable y funciona sin conexión, por lo que puedes usarla incluso si no tienes red. También se puede instalar en la pantalla principal de un dispositivo y se integra sin problemas con el sistema operativo como una aplicación independiente.

AWP de Fugu Greetings con un dibujo que se asemeja al logotipo de la comunidad de AWP.
La app de ejemplo Fugu Greetings.

Mejora progresiva

Ahora que ya aclaramos esto, es momento de hablar sobre la mejora progresiva. El glosario de MDN Web Docs define el concepto de la siguiente manera:

La mejora progresiva es una filosofía de diseño que proporciona una base de contenido y funcionalidad esenciales a la mayor cantidad posible de usuarios, a la vez que ofrece la mejor experiencia posible solo a los usuarios de los navegadores más modernos que pueden ejecutar todo el código requerido.

Por lo general, la detección de funciones se usa para determinar si los navegadores pueden controlar la funcionalidad más moderna, mientras que los polyfills se usan a menudo para agregar funciones faltantes con JavaScript.

[…]

La mejora progresiva es una técnica útil que permite a los desarrolladores web enfocarse en crear los mejores sitios web posibles y, al mismo tiempo, hacer que estos sitios funcionen en múltiples agentes de usuario desconocidos. La degradación elegante está relacionada, pero no es lo mismo y, a menudo, se considera que va en la dirección opuesta a la mejora progresiva. En realidad, ambos enfoques son válidos y, a menudo, se pueden complementar entre sí.

Colaboradores de MDN

Comenzar cada tarjeta de felicitación desde cero puede ser muy engorroso. Entonces, ¿por qué no tener una función que permita a los usuarios importar una imagen y comenzar desde allí? Con un enfoque tradicional, habrías usado un elemento <input type=file> para lograr esto. Primero, crearías el elemento, establecerías su type en 'file' y agregarías tipos de MIME a la propiedad accept. Luego, harías un "clic" de forma programática y escucharías los cambios. Cuando seleccionas una imagen, se importa directamente al lienzo.

const importImage = async () => {
  return new Promise((resolve) => {
    const input = document.createElement('input');
    input.type = 'file';
    input.accept = 'image/*';
    input.addEventListener('change', () => {
      resolve(input.files[0]);
    });
    input.click();
  });
};

Cuando hay una función de importación, probablemente debería haber una función de exportación para que los usuarios puedan guardar sus tarjetas de felicitación de forma local. La forma tradicional de guardar archivos es crear un vínculo de anclaje con un atributo download y una URL de BLOB como su href. También harías un "clic" de forma programática para activar la descarga y, para evitar fugas de memoria, esperamos que no olvides revocar la URL del objeto blob.

const exportImage = async (blob) => {
  const a = document.createElement('a');
  a.download = 'fugu-greeting.png';
  a.href = URL.createObjectURL(blob);
  a.addEventListener('click', (e) => {
    setTimeout(() => URL.revokeObjectURL(a.href), 30 * 1000);
  });
  a.click();
};

Pero espera un minuto. Mentalmente, no "descargaste" una tarjeta de felicitación, sino que la "guardaste". En lugar de mostrarte un diálogo de "guardar" que te permita elegir dónde colocar el archivo, el navegador descargó directamente la tarjeta de felicitación sin interacción del usuario y la colocó directamente en tu carpeta de Descargas. Esto no es bueno.

¿Y si hubiera una mejor manera? ¿Qué pasaría si pudieras abrir un archivo local, editarlo y, luego, guardar las modificaciones en un archivo nuevo o en el archivo original que abriste inicialmente? Resulta que sí. La API de File System Access te permite abrir y crear archivos y directorios, así como modificarlos y guardarlos .

Entonces, ¿cómo detecto las funciones de una API? La API de File System Access expone un nuevo método window.chooseFileSystemEntries(). Por lo tanto, necesito cargar de forma condicional diferentes módulos de importación y exportación según si este método está disponible.

const loadImportAndExport = () => {
  if ('chooseFileSystemEntries' in window) {
    Promise.all([
      import('./import_image.mjs'),
      import('./export_image.mjs'),
    ]);
  } else {
    Promise.all([
      import('./import_image_legacy.mjs'),
      import('./export_image_legacy.mjs'),
    ]);
  }
};

Pero antes de profundizar en los detalles de la API de File System Access, permítanme destacar rápidamente el patrón de mejora progresiva aquí. En los navegadores que no admiten la API de File System Access, cargo las secuencias de comandos heredadas.

El Inspector web de Safari muestra que se están cargando los archivos heredados.
Las Herramientas para desarrolladores de Firefox muestran que se están cargando los archivos heredados.

Sin embargo, en Chrome, un navegador que admite la API, solo se cargan los nuevos secuencias de comandos. Esto es posible de forma elegante gracias a import() dinámico, que admiten todos los navegadores modernos. Como dije antes, el césped está bastante verde estos días.

Herramientas para desarrolladores de Chrome que muestran la carga de los archivos modernos.
Pestaña de red de las Herramientas para desarrolladores de Chrome.

La API de File System Access

Ahora que abordé este tema, es hora de analizar la implementación real basada en la API de File System Access. Para importar una imagen, llamo a window.chooseFileSystemEntries() y le paso una propiedad accepts en la que indico que quiero archivos de imagen. Se admiten tanto las extensiones de archivo como los tipos de MIME. Esto da como resultado un identificador de archivo, desde el cual puedo obtener el archivo real llamando a getFile().

const importImage = async () => {
  try {
    const handle = await window.chooseFileSystemEntries({
      accepts: [
        {
          description: 'Image files',
          mimeTypes: ['image/*'],
          extensions: ['jpg', 'jpeg', 'png', 'webp', 'svg'],
        },
      ],
    });
    return handle.getFile();
  } catch (err) {
    console.error(err.name, err.message);
  }
};

La exportación de una imagen es casi igual, pero esta vez necesito pasar un parámetro de tipo 'save-file' al método chooseFileSystemEntries(). Desde aquí, obtengo un diálogo para guardar el archivo. Con el archivo abierto, esto no era necesario, ya que 'open-file' es el valor predeterminado. Establecí el parámetro accepts de manera similar a antes, pero esta vez solo para imágenes PNG. Nuevamente, obtengo un identificador de archivo, pero, en lugar de obtener el archivo, esta vez creo una transmisión de escritura llamando a createWritable(). A continuación, escribo el BLOB, que es la imagen de mi tarjeta de felicitaciones, en el archivo. Por último, cierro el flujo de escritura.

Todo puede fallar en cualquier momento: el disco podría quedarse sin espacio, podría haber un error de lectura o escritura, o tal vez el usuario simplemente cancele el diálogo de archivos. Por eso, siempre encierro las llamadas en una instrucción try...catch.

const exportImage = async (blob) => {
  try {
    const handle = await window.chooseFileSystemEntries({
      type: 'save-file',
      accepts: [
        {
          description: 'Image file',
          extensions: ['png'],
          mimeTypes: ['image/png'],
        },
      ],
    });
    const writable = await handle.createWritable();
    await writable.write(blob);
    await writable.close();
  } catch (err) {
    console.error(err.name, err.message);
  }
};

Con la mejora progresiva de la API de File System Access, puedo abrir un archivo como antes. El archivo importado se dibuja directamente en el lienzo. Puedo realizar mis ediciones y, finalmente, guardarlas con un diálogo de guardado real, en el que puedo elegir el nombre y la ubicación de almacenamiento del archivo. Ahora el archivo está listo para conservarse por la eternidad.

App de Fugu Greetings con un diálogo de apertura de archivos.
Es el diálogo de apertura de archivos.
La app de Fugu Greetings ahora incluye una imagen importada.
La imagen importada.
App de Fugu Greetings con la imagen modificada.
Guarda la imagen modificada en un archivo nuevo.

Las APIs de Web Share y Web Share Target

attempt-right

Además de almacenarla para siempre, tal vez quiera compartir mi tarjeta de felicitaciones. Esto es algo que me permiten hacer la API de Web Share y la API de Web Share Target. Los sistemas operativos para dispositivos móviles y, más recientemente, para computadoras de escritorio, incorporaron mecanismos de uso compartido.

Por ejemplo, la hoja para compartir de Safari para computadoras en macOS se activa cuando un usuario hace clic en Compartir artículo en mi blog. Podrías compartir un vínculo al artículo con un amigo a través de la app de Mensajes de macOS.

Para que esto suceda, llamo a navigator.share() y le paso un title, text y url opcionales en un objeto. Pero, ¿qué sucede si quiero adjuntar una imagen? El nivel 1 de la API de Web Share aún no admite esta función. La buena noticia es que Web Share Level 2 agregó funciones para compartir archivos.

try {
  await navigator.share({
    title: 'Check out this article:',
    text: `"${document.title}" by @tomayac:`,
    url: document.querySelector('link[rel=canonical]').href,
  });
} catch (err) {
  console.warn(err.name, err.message);
}

Te mostraré cómo hacer que esto funcione con la aplicación de tarjetas de saludo de Fugu. Primero, debo preparar un objeto data con un array files que contenga un solo BLOB y, luego, un title y un text. A continuación, como práctica recomendada, uso el nuevo método navigator.canShare(), que hace lo que su nombre sugiere: me indica si el navegador puede compartir técnicamente el objeto data que intento compartir. Si navigator.canShare() me dice que los datos se pueden compartir, puedo llamar a navigator.share() como antes. Como todo puede fallar, vuelvo a usar un bloque try...catch.

const share = async (title, text, blob) => {
  const data = {
    files: [
      new File([blob], 'fugu-greeting.png', {
        type: blob.type,
      }),
    ],
    title: title,
    text: text,
  };
  try {
    if (!(navigator.canShare(data))) {
      throw new Error("Can't share data.", data);
    }
    await navigator.share(data);
  } catch (err) {
    console.error(err.name, err.message);
  }
};

Como antes, uso la mejora progresiva. Si 'share' y 'canShare' existen en el objeto navigator, solo entonces cargo share.mjs con import() dinámico. En navegadores como Safari para dispositivos móviles, que solo cumplen una de las dos condiciones, no cargo la funcionalidad.

const loadShare = () => {
  if ('share' in navigator && 'canShare' in navigator) {
    import('./share.mjs');
  }
};

En Fugu Greetings, si presiono el botón Compartir en un navegador compatible, como Chrome en Android, se abre la hoja para compartir integrada. Por ejemplo, puedo elegir Gmail, y aparecerá el widget de redacción de correos electrónicos con la imagen adjunta.

Hoja para compartir a nivel del SO que muestra varias apps para compartir la imagen.
Elegir una app para compartir el archivo
Widget de redacción de correos electrónicos de Gmail con la imagen adjunta.
El archivo se adjunta a un correo electrónico nuevo en el redactor de Gmail.

La API de Contact Picker

A continuación, quiero hablar sobre los contactos, es decir, la libreta de direcciones o la app de administración de contactos de un dispositivo. Cuando escribes una tarjeta de felicitaciones, no siempre es fácil escribir correctamente el nombre de alguien. Por ejemplo, tengo un amigo llamado Sergey que prefiere que su nombre se escriba con letras cirílicas. Uso un teclado QWERTZ alemán y no sé cómo escribir su nombre. Este es un problema que puede resolver la API de Contact Picker. Como tengo a mi amigo guardado en la app de Contactos del teléfono, puedo acceder a mis contactos desde la Web con la API de Contacts Picker.

Primero, debo especificar la lista de propiedades a las que quiero acceder. En este caso, solo quiero los nombres, pero para otros casos de uso, podría interesarme obtener números de teléfono, correos electrónicos, íconos de avatar o direcciones físicas. A continuación, configuro un objeto options y establezco multiple en true para poder seleccionar más de una entrada. Por último, puedo llamar a navigator.contacts.select(), que devuelve las propiedades ideales para los contactos seleccionados por el usuario.

const getContacts = async () => {
  const properties = ['name'];
  const options = { multiple: true };
  try {
    return await navigator.contacts.select(properties, options);
  } catch (err) {
    console.error(err.name, err.message);
  }
};

Y, a esta altura, probablemente ya hayas aprendido el patrón: solo cargo el archivo cuando la API es compatible.

if ('contacts' in navigator) {
  import('./contacts.mjs');
}

En Fugu Greeting, cuando presiono el botón Contacts y selecciono a mis dos mejores amigos, Сергей Михайлович Брин y 劳伦斯·爱德华·"拉里"·佩奇, puedes ver cómo el selector de contactos se limita a mostrar solo sus nombres, pero no sus direcciones de correo electrónico ni otra información, como sus números de teléfono. Luego, sus nombres se dibujan en mi tarjeta de saludo.

Selector de contactos que muestra los nombres de dos contactos en la libreta de direcciones.
Seleccionar dos nombres con el selector de contactos de la libreta de direcciones
Los nombres de los dos contactos elegidos previamente que se dibujaron en la tarjeta de felicitación.
Luego, los dos nombres se dibujan en la tarjeta de saludo.

La API de Async Clipboard

A continuación, veremos cómo copiar y pegar. Una de nuestras operaciones favoritas como desarrolladores de software es copiar y pegar. Como autor de tarjetas de felicitación, a veces, es posible que quiera hacer lo mismo. Es posible que quiera pegar una imagen en una tarjeta de felicitación en la que estoy trabajando o copiar mi tarjeta de felicitación para poder seguir editándola desde otro lugar. La API de Async Clipboard admite texto e imágenes. Te explicaré cómo agregué la compatibilidad con copiar y pegar a la app de Fugu Greetings.

Para copiar algo en el portapapeles del sistema, necesito escribir en él. El método navigator.clipboard.write() toma un array de elementos del portapapeles como parámetro. Cada elemento del portapapeles es, básicamente, un objeto con un blob como valor y el tipo del blob como clave.

const copy = async (blob) => {
  try {
    await navigator.clipboard.write([
      new ClipboardItem({
        [blob.type]: blob,
      }),
    ]);
  } catch (err) {
    console.error(err.name, err.message);
  }
};

Para pegar, necesito iterar los elementos del portapapeles que obtengo llamando a navigator.clipboard.read(). El motivo es que puede haber varios elementos en el portapapeles con diferentes representaciones. Cada elemento del portapapeles tiene un campo types que me indica los tipos de MIME de los recursos disponibles. Llamo al método getType() del elemento del portapapeles y paso el tipo de MIME que obtuve antes.

const paste = async () => {
  try {
    const clipboardItems = await navigator.clipboard.read();
    for (const clipboardItem of clipboardItems) {
      try {
        for (const type of clipboardItem.types) {
          const blob = await clipboardItem.getType(type);
          return blob;
        }
      } catch (err) {
        console.error(err.name, err.message);
      }
    }
  } catch (err) {
    console.error(err.name, err.message);
  }
};

Y ya casi no hace falta decirlo. Solo lo hago en los navegadores compatibles.

if ('clipboard' in navigator && 'write' in navigator.clipboard) {
  import('./clipboard.mjs');
}

Entonces, ¿cómo funciona esto en la práctica? Tengo una imagen abierta en la app de Vista previa de macOS y la copio en el portapapeles. Cuando hago clic en Pegar, la app de Fugu Greetings me pregunta si quiero permitir que vea el texto y las imágenes del portapapeles.

La app de Fugu Greetings muestra la solicitud de permiso del portapapeles.
Es el mensaje de permiso del portapapeles.

Por último, después de aceptar el permiso, la imagen se pega en la aplicación. También funciona al revés. Déjame copiar una tarjeta de felicitación en el portapapeles. Cuando abro Vista previa y hago clic en Archivo y, luego, en Nuevo desde el portapapeles, la tarjeta de felicitación se pega en una nueva imagen sin título.

La app de Vista previa de macOS con una imagen sin título que se acaba de pegar.
Una imagen pegada en la app de Vista previa de macOS.

La API de Badging

Otra API útil es la API de Badging. Como PWA instalable, Fugu Greetings, por supuesto, tiene un ícono de app que los usuarios pueden colocar en el dock de apps o en la pantalla principal. Una forma divertida de demostrar la API es usarla en Fugu Greetings, como contador de trazos de lápiz. Agregué un objeto de escucha de eventos que incrementa el contador de trazos de lápiz cada vez que se produce el evento pointerdown y, luego, establece la insignia de ícono actualizada. Cada vez que se borra el lienzo, el contador se restablece y se quita la insignia.

let strokes = 0;

canvas.addEventListener('pointerdown', () => {
  navigator.setAppBadge(++strokes);
});

clearButton.addEventListener('click', () => {
  strokes = 0;
  navigator.setAppBadge(strokes);
});

Esta función es una mejora progresiva, por lo que la lógica de carga es la habitual.

if ('setAppBadge' in navigator) {
  import('./badge.mjs');
}

En este ejemplo, dibujé los números del uno al siete con un solo trazo de lápiz por número. El contador de insignias del ícono ahora es de siete.

Los números del uno al siete dibujados en la tarjeta de felicitación, cada uno con un solo trazo de lápiz.
Dibujar los números del 1 al 7 con siete trazos de lápiz
Ícono de insignia en la app de Fugu Greetings que muestra el número 7.
El contador de trazos de lápiz en forma de insignia del ícono de la app.

La API de Periodic Background Sync

¿Quieres comenzar cada día con algo nuevo? Una función interesante de la app de Fugu Greetings es que puede inspirarte cada mañana con una nueva imagen de fondo para comenzar tu tarjeta de saludo. La app usa la API de Periodic Background Sync para lograr esto.

El primer paso es registrar un evento de sincronización periódica en el registro del trabajador de servicio. Espera una etiqueta de sincronización llamada 'image-of-the-day' y tiene un intervalo mínimo de un día, por lo que el usuario puede obtener una nueva imagen de fondo cada 24 horas.

const registerPeriodicBackgroundSync = async () => {
  const registration = await navigator.serviceWorker.ready;
  try {
    registration.periodicSync.register('image-of-the-day-sync', {
      // An interval of one day.
      minInterval: 24 * 60 * 60 * 1000,
    });
  } catch (err) {
    console.error(err.name, err.message);
  }
};

El segundo paso es escuchar el evento periodicsync en el trabajador de servicio. Si la etiqueta del evento es 'image-of-the-day', es decir, la que se registró antes, la imagen del día se recupera con la función getImageOfTheDay() y el resultado se propaga a todos los clientes para que puedan actualizar sus lienzos y cachés.

self.addEventListener('periodicsync', (syncEvent) => {
  if (syncEvent.tag === 'image-of-the-day-sync') {
    syncEvent.waitUntil(
      (async () => {
        const blob = await getImageOfTheDay();
        const clients = await self.clients.matchAll();
        clients.forEach((client) => {
          client.postMessage({
            image: blob,
          });
        });
      })()
    );
  }
});

Nuevamente, se trata de una mejora progresiva, por lo que el código solo se carga cuando el navegador admite la API. Esto se aplica tanto al código del cliente como al código del Service Worker. En los navegadores que no admiten esta función, no se carga ninguno de los dos. Observa cómo, en el service worker, en lugar de un import() dinámico (que aún no se admite en un contexto de service worker), uso el clásico importScripts().

// In the client:
const registration = await navigator.serviceWorker.ready;
if (registration && 'periodicSync' in registration) {
  import('./periodic_background_sync.mjs');
}
// In the service worker:
if ('periodicSync' in self.registration) {
  importScripts('./image_of_the_day.mjs');
}

En Fugu Greetings, si presionas el botón Fondo de pantalla, se muestra la imagen de la tarjeta de saludo del día, que se actualiza todos los días con la API de Periodic Background Sync.

Si presionas el botón Fondo de pantalla, se muestra la imagen del día.

API de Notification Triggers

A veces, incluso con mucha inspiración, necesitas un empujón para terminar una tarjeta de felicitación que comenzaste. Esta es una función que habilita la API de Notification Triggers. Como usuario, puedo ingresar la hora en la que quiero que se me recuerde que debo terminar mi tarjeta de felicitación. Cuando llegue ese momento, recibiré una notificación de que mi tarjeta de felicitación está lista.

Después de solicitar la hora objetivo, la aplicación programa la notificación con un showTrigger. Puede ser un TimestampTrigger con la fecha objetivo seleccionada anteriormente. La notificación de recordatorio se activará de forma local, sin necesidad de red ni servidor.

const targetDate = promptTargetDate();
if (targetDate) {
  const registration = await navigator.serviceWorker.ready;
  registration.showNotification('Reminder', {
    tag: 'reminder',
    body: "It's time to finish your greeting card!",
    showTrigger: new TimestampTrigger(targetDate),
  });
}

Al igual que con todo lo que mostré hasta ahora, esta es una mejora progresiva, por lo que el código solo se carga de forma condicional.

if ('Notification' in window && 'showTrigger' in Notification.prototype) {
  import('./notification_triggers.mjs');
}

Cuando marco la casilla de verificación Recordatorio en Fugu Greetings, aparece un mensaje que me pregunta cuándo quiero que se me recuerde que debo terminar mi tarjeta de saludo.

App de Fugu Greetings con un mensaje que le pregunta al usuario cuándo quiere que se le recuerde que termine su tarjeta de saludo.
Cómo programar una notificación local para que se recuerde terminar una tarjeta de felicitación.

Cuando se activa una notificación programada en Fugu Greetings, se muestra como cualquier otra notificación, pero, como escribí antes, no requería una conexión de red.

La notificación activada aparece en el Centro de notificaciones de macOS.

La API de Wake Lock

También quiero incluir la API de Wake Lock. A veces, solo necesitas mirar la pantalla el tiempo suficiente hasta que la inspiración te bese. Lo peor que puede suceder es que la pantalla se apague. La API de Wake Lock puede evitar que esto suceda.

El primer paso es obtener un bloqueo de activación con navigator.wakelock.request method(). Le paso la cadena 'screen' para obtener un bloqueo de activación de pantalla. Luego, agrego un objeto de escucha de eventos para recibir información cuando se libera el bloqueo de activación. Esto puede ocurrir, por ejemplo, cuando cambia la visibilidad de la pestaña. Si esto sucede, puedo volver a obtener el bloqueo de activación cuando la pestaña vuelva a estar visible.

let wakeLock = null;
const requestWakeLock = async () => {
  wakeLock = await navigator.wakeLock.request('screen');
  wakeLock.addEventListener('release', () => {
    console.log('Wake Lock was released');
  });
  console.log('Wake Lock is active');
};

const handleVisibilityChange = () => {
  if (wakeLock !== null && document.visibilityState === 'visible') {
    requestWakeLock();
  }
};

document.addEventListener('visibilitychange', handleVisibilityChange);
document.addEventListener('fullscreenchange', handleVisibilityChange);

Sí, esta es una mejora progresiva, por lo que solo necesito cargarla cuando el navegador admita la API.

if ('wakeLock' in navigator && 'request' in navigator.wakeLock) {
  import('./wake_lock.mjs');
}

En Fugu Greetings, hay una casilla de verificación Insomnia que, cuando se marca, mantiene la pantalla activa.

Si se marca la casilla de verificación de Insomnio, la pantalla permanecerá activa.
La casilla de verificación Insomnia mantiene la app activa.

La API de Idle Detection

A veces, incluso si miras la pantalla durante horas, no sirve de nada y no se te ocurre la más mínima idea de qué hacer con tu tarjeta de felicitación. La API de Idle Detection permite que la app detecte el tiempo de inactividad del usuario. Si el usuario permanece inactivo durante demasiado tiempo, la app se restablece al estado inicial y borra el lienzo. Esta API está protegida por el permiso de notificaciones, ya que muchos casos de uso de producción de la detección de inactividad están relacionados con las notificaciones, por ejemplo, para enviar una notificación solo a un dispositivo que el usuario esté usando de forma activa.

Después de asegurarme de que se otorgó el permiso de notificaciones, instancio el detector de inactividad. Registro un objeto de escucha de eventos que detecta los cambios de inactividad, lo que incluye el usuario y el estado de la pantalla. El usuario puede estar activo o inactivo, y la pantalla puede estar desbloqueada o bloqueada. Si el usuario está inactivo, el lienzo se borra. Le doy al detector de inactividad un umbral de 60 segundos.

const idleDetector = new IdleDetector();
idleDetector.addEventListener('change', () => {
  const userState = idleDetector.userState;
  const screenState = idleDetector.screenState;
  console.log(`Idle change: ${userState}, ${screenState}.`);
  if (userState === 'idle') {
    clearCanvas();
  }
});

await idleDetector.start({
  threshold: 60000,
  signal,
});

Y, como siempre, solo cargo este código cuando el navegador lo admite.

if ('IdleDetector' in window) {
  import('./idle_detection.mjs');
}

En la app de Fugu Greetings, el lienzo se borra cuando se marca la casilla de verificación Ephemeral y el usuario está inactivo durante demasiado tiempo.

App de Fugu Greetings con un lienzo borrado después de que el usuario estuvo inactivo durante demasiado tiempo.
Cuando se marca la casilla de verificación Ephemeral y el usuario está inactivo durante demasiado tiempo, se borra el lienzo.

Closing

Uf, qué viaje. Hay muchas APIs en una sola app de ejemplo. Y recuerda que nunca le hago pagar al usuario el costo de descarga por una función que su navegador no admite. Con la mejora progresiva, me aseguro de que solo se cargue el código pertinente. Y, dado que con HTTP/2 las solicitudes son económicas, este patrón debería funcionar bien para muchas aplicaciones, aunque es posible que desees considerar un bundler para las aplicaciones realmente grandes.

Pestaña Network de las Herramientas para desarrolladores de Chrome que muestra solo las solicitudes de archivos con código que admite el navegador.

La app puede verse un poco diferente en cada navegador, ya que no todas las plataformas admiten todas las funciones, pero la funcionalidad principal siempre está presente y se mejora de forma progresiva según las capacidades del navegador en particular. Estas capacidades pueden cambiar incluso en el mismo navegador, según si la app se ejecuta como una app instalada o en una pestaña del navegador.

Fugu Greetings ejecutándose en Chrome para Android, que muestra muchas funciones disponibles.
Fugu Greetings ejecutándose en Safari para computadoras, que muestra menos funciones disponibles.
Fugu Greetings ejecutándose en Chrome para computadoras, mostrando muchas funciones disponibles.

Puedes bifurcar Fugu en GitHub.

El equipo de Chromium está trabajando arduamente para mejorar las APIs avanzadas de Fugu. Si aplico la mejora progresiva cuando creo mi app, me aseguro de que todos obtengan una experiencia básica sólida y buena, pero que las personas que usan navegadores que admiten más APIs de la plataforma web obtengan una experiencia aún mejor. Esperamos ver lo que harás con la mejora progresiva en tus apps.

Agradecimientos

Agradezco a Christian Liebel y a Hemanth HM, quienes contribuyeron a Fugu Greetings. Joe Medley y Kayce Basques revisaron este documento. Jake Archibald me ayudó a comprender la situación con import() dinámico en un contexto de Service Worker.