Mejora progresivamente tu app web progresiva

Crear para navegadores modernos y mejorar progresivamente como en el 2003

En marzo de 2003, Nick Finck y Steve Champeon sorprendieron al mundo del diseño web con el concepto de mejora progresiva, una estrategia para el diseño web que enfatiza la carga inicial del contenido principal de la página web y que, de manera progresiva, agrega capas más matizadas y técnicamente rigurosas sobre el contenido. Mientras que en 2003, la mejora progresiva consistía en utilizar, en ese momento, funciones de CSS modernas, JavaScript discreto e incluso solo gráficos vectoriales escalables. La mejora progresiva en 2020 y en el futuro se trata de usar capacidades modernas del navegador.

Diseño web inclusivo para el futuro con mejoras progresivas. Diapositiva de título de la presentación original de Finck y Champeon.
Diapositiva: Diseño web inclusivo para el futuro con mejoras progresivas. (Fuente)

JavaScript moderno

Hablando de JavaScript, la situación de compatibilidad del navegador con las últimas funciones principales de JavaScript de ES 2015 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, descanso y expansión, Map/Set, WeakMap/WeakSet y mucho más. Todos son compatibles.

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

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

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

Además, incluso las incorporaciones muy recientes del lenguaje ES 2020, como el encadenamiento opcional y la combinación nula, alcanzaron la compatibilidad con rapidez. Puedes ver una muestra de código a continuación. Cuando se trata de las funciones principales de JavaScript, el césped no podría ser mucho más verde de lo que es hoy.

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 verde hierba de Windows XP.
El césped es verde cuando se trata de las funciones principales de JavaScript. (Captura de pantalla del producto de Microsoft, que se usa con permiso).

La app de ejemplo: Fugu Greetings

Para este artículo, trabajo con una AWP simple, llamada Fugu Greetings (GitHub). El nombre de esta app es un sombrero de Project Fugu 🐡, una iniciativa para darle a la Web todos los poderes de las aplicaciones de 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 felicitaciones virtuales y enviárselas a tus seres queridos. Este documento ejemplifica los conceptos principales de AWP. Es confiable y está habilitada por completo sin conexión, por lo que puedes usarla incluso si no tienes una red. También es instalable en la pantalla principal de un dispositivo y se integra sin problemas en el sistema operativo como aplicación independiente.

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

Mejora progresiva

Ahora que tienes esto en mente, es momento de hablar sobre la mejora progresiva. El glosario de los documentos web de MDN define el concepto de la siguiente manera:

La mejora progresiva es una filosofía de diseño que proporciona un modelo de referencia de contenido y funcionalidad esenciales a tantos usuarios como sea posible, a la vez que proporciona 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 admitir funciones más modernas, mientras que polyfills a menudo se usan para agregar las funciones faltantes con JavaScript.

[…]

La mejora progresiva es una técnica útil que permite a los desarrolladores web enfocarse en desarrollar los mejores sitios web posibles mientras los hace funcionar con varios usuarios-agentes desconocidos. La degradación correcta está relacionada, pero no es lo mismo y, a menudo, se ve que va en la dirección opuesta a la mejora progresiva. En realidad, ambos enfoques son válidos y, a menudo, pueden complementarse entre sí.

Colaboradores de MDN

Iniciar cada tarjeta de felicitación desde cero puede ser realmente engorroso. Entonces, ¿por qué no contar con 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 que esto sucediera. Primero, debes crear el elemento, establecer su type como 'file' y agregar tipos de MIME a la propiedad accept. Luego, hacer clic en él de manera programática y detectar 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 haya una función de import, probablemente debería haber una función de import para que los usuarios puedan guardar sus tarjetas festivas de forma local. La forma tradicional de guardar archivos es crear un vínculo de anclaje con un atributo download y con una URL de BLOB como href. También puedes “hacer clic” de manera programática en él para activar la descarga y, para evitar fugas de memoria, recordar 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 permite elegir dónde colocar el archivo, el navegador descargó la tarjeta festiva directamente sin interacción del usuario y la colocó directamente en la carpeta Descargas. No es la mejor opción.

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

Entonces, ¿cómo puedo detectar atributos en una API? La API de File System Access expone un nuevo método window.chooseFileSystemEntries(). En consecuencia, debo cargar condicionalmente diferentes módulos de importación y exportación dependiendo de si este método está disponible o no. A continuación, te mostramos cómo hacerlo.

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í. Carga las secuencias de comandos heredadas en los navegadores que actualmente no son compatibles con la API de File System Access. A continuación, puedes ver las pestañas de red de Firefox y Safari.

El Inspector web de Safari muestra la carga de los archivos heredados.
Pestaña de red del Inspector web de Safari.
Las Herramientas para desarrolladores de Firefox muestran la carga de los archivos heredados.
Pestaña de red de las Herramientas para desarrolladores de Firefox.

Sin embargo, en Chrome, un navegador compatible con la API, solo se cargan las nuevas secuencias de comandos. Esto es posible gracias al import() dinámico, que admiten todos los navegadores modernos. Como dije antes, el césped es bastante verde en 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 ya abordamos esto, es momento de analizar la implementación real en función de la API de File System Access. Para importar una imagen, llamo a window.chooseFileSystemEntries() y le paso una propiedad accepts en la que digo que quiero archivos de imagen. Se admiten las extensiones de archivo y los tipos de MIME. Esto da como resultado un controlador 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);
  }
};

Exportar una imagen es casi el mismo, pero esta vez debo pasar un parámetro de tipo de 'save-file' al método chooseFileSystemEntries(). 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. Configuré el parámetro accepts de manera similar a antes, pero esta vez se limitó solo a las imágenes PNG. De nuevo, obtengo un controlador de archivo, pero en lugar de obtenerlo, esta vez, llamo a createWritable() para crear una transmisión con capacidad de escritura. A continuación, escribo el BLOB, que es la imagen de mi tarjeta de felicitación, en el archivo. Finalmente, cierro la transmisión que admite escritura.

Todo puede fallar siempre: el disco se quedó sin espacio, podría haber un error de escritura o lectura, o tal vez el usuario simplemente haya cancelado el diálogo del archivo. Es por eso que siempre uno las llamadas en una sentencia 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);
  }
};

Mediante la mejora progresiva con la API de File System Access, puedo abrir un archivo como antes. El archivo importado se dibuja directamente en el lienzo. Puedo hacer ediciones y, finalmente, guardarlas con un cuadro de 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 conservarlo por siempre.

App de saludos de Fugu con un diálogo para abrir un archivo.
Diálogo para abrir el archivo.
Ahora la app de saludo de Fugu con una imagen importada
La imagen importada.
App de saludo de Fugu con la imagen modificada
Guardar la imagen modificada en un archivo nuevo

Las APIs de Web Share y Web Share Target

Además de almacenar por eternidad, quizás 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 de dispositivos móviles y de computadoras de escritorio incorporaron mecanismos de uso compartido. Por ejemplo, a continuación, se muestra la hoja compartida de Safari para computadoras de escritorio en macOS que se activa desde un artículo en mi blog. Cuando haces clic en el botón Share Article, puedes compartir un vínculo al artículo con un amigo, por ejemplo, mediante la app de Mensajes de macOS.

La hoja para compartir de Safari en macOS se activa desde el botón Compartir de un artículo.
La API de Web Share en Safari de escritorio con macOS.

El código para que esto suceda es bastante sencillo. Llamo a navigator.share() y le paso los valores 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 todavía no lo admite. La buena noticia es que el nivel 2 del uso compartido web agregó funciones de uso compartido de 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);
}

Déjame mostrarte cómo hacer que esto funcione con la aplicación de la tarjeta de felicitación de Fugu. Primero, debo preparar un objeto data con un array files que consta de un BLOB, 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 estoy intentando compartir. Si navigator.canShare() me indica que se pueden compartir los datos, tengo todo listo para llamar a navigator.share() como antes. Como todo puede fallar, de nuevo estoy usando 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, utilizo la mejora progresiva. Si tanto 'share' como 'canShare' existen en el objeto navigator, solo entonces puedo avanzar y cargar share.mjs a través de import() dinámico. En navegadores como Safari para dispositivos móviles que solo cumplen con una de las dos condiciones, no se carga la funcionalidad.

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

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

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

La API de Contact Picker

Ahora quiero hablar de los contactos, es decir, la libreta de direcciones de un dispositivo o la app de administración de contactos. Cuando escribes una tarjeta de felicitación, puede que no siempre sea fácil escribir correctamente el nombre de una persona. Por ejemplo, tengo un amigo Sergey que prefiere que su nombre se escriba con letras cirílicas. Estoy usando un teclado QWERTZ en alemán y no sé cómo escribir su nombre. Este es un problema que la API de Contact Picker puede resolver. Como tengo a mi amigo almacenado en la app de contactos de mi teléfono, con la API de Selector de contactos, puedo acceder a mis contactos desde la Web.

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 estar interesado en números de teléfono, correos electrónicos, íconos de avatar o direcciones físicas. A continuación, configuro un objeto options y configuré multiple como true para poder seleccionar más de una entrada. Por último, puedo llamar a navigator.contacts.select(), que muestra las propiedades deseadas para los contactos que selecciona 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);
  }
};

Es probable que ya hayas aprendido el patrón: solo carga 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, (Нерелеpodría same лайлови optimizan Бр únicos y 劳伦斯·爱德华"拉里"··佩奇, como sus números de teléfono, solo se muestra el selector, pero no se limitan a las direcciones de correo electrónico. Sus nombres se dibujan en mi tarjeta de felicitación.

Selector de contactos que muestra los nombres de dos contactos de la libreta de direcciones.
Seleccionar dos nombres con el selector de contactos de la libreta de direcciones
Los nombres de los dos contactos elegidos anteriormente dibujados en la tarjeta de felicitación.
Los dos nombres se dibujarán en la tarjeta de felicitación.

La API de Aasync Clipboard

Lo siguiente es copiar y pegar. Una de nuestras operaciones favoritas como desarrolladores de software es copiar y pegar. Como autor de tarjetas festivas, a veces, es posible que quiera hacer lo mismo. Es posible que quiera pegar una imagen en una tarjeta festiva en la que esté trabajando o copiar mi tarjeta festiva para poder seguir editándola desde otro. La API de Async Clipboard admite imágenes y texto. Te explicaré cómo agregué la compatibilidad de copiar y pegar a la app de saludos de Fugu.

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 de 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, debo hacer un bucle sobre los elementos del portapapeles que obtengo llamando a navigator.clipboard.read(). Esto se debe a que varios elementos del portapapeles pueden estar en él en 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 pasé 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 es casi innecesario decirlo a esta altura. Solo lo hago en navegadores compatibles.

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

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

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

Por último, después de aceptar el permiso, la imagen se pega en la aplicación. Lo opuesto también funciona. Voy a copiar una tarjeta festiva en el portapapeles. Cuando abro la vista previa y hago clic en File y, luego, en New from Clipboard, la tarjeta de felicitaciones se pega en una imagen nueva sin título.

La app de la vista previa de macOS con una imagen sin título y pegada
Una imagen pegada en la app de la versión preliminar de macOS.

La API de Badging

Otra API útil es la API de Badging. Como es una AWP instalable, Fugu Greetings tiene un ícono de la app que los usuarios pueden colocar en el conector de apps o en la pantalla principal. Una forma divertida y fácil de demostrar la API es (ab) usarla en Fugu Greetings como un contador de trazos de pluma. Agregué un objeto de escucha de eventos que aumenta el contador de trazos del lápiz cada vez que se produce el evento pointerdown y, luego, establece la insignia del ícono actualizada. Cuando se borra el lienzo, se restablece el contador 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 trazo de lápiz por número. El contador de insignias del ícono ahora está en 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 del lápiz en forma de insignia del ícono de la app

La API de Periodic Background Sync

¿Quieres comenzar cada día fresco con algo nuevo? Una función genial de la app de saludos de Fugu es que puede inspirarte cada mañana con una nueva imagen de fondo para que empieces tu tarjeta de felicitación. Para lograrlo, la app usa la API de Periodic Background Sync.

El primer paso es register un evento de sincronización periódico en el registro del service worker. Detecta 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 service worker. Si la etiqueta del evento es 'image-of-the-day', es decir, la que se registró antes, la imagen del día se recupera a través de 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,
          });
        });
      })()
    );
  }
});

De nuevo, se trata de una mejora progresiva, por lo que el código solo se carga cuando el navegador admite la API. Esto se aplica al código de cliente y al código del service worker. En navegadores que no son compatibles, no se carga ninguno de ellos. Observa cómo, en el service worker, en lugar de en un import() dinámico (que todavía no se admite en el contexto de un service worker), uso la versión clásica de 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 Wallpaper, se muestra la imagen de la tarjeta de felicitación del día que se actualiza todos los días a través de la API de Periodic Background Sync.

La app de saludos de Fugu con una nueva imagen de tarjeta de felicitación del día.
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 iniciada. Esta es una función que habilita la API de activadores de notificaciones. Como usuario, puedo ingresar la hora en la que quiero que se me guíe para terminar mi tarjeta de felicitación. Cuando llegue ese momento, recibiré una notificación en la que se indica que mi tarjeta está esperando.

Después de solicitar la hora objetivo, la aplicación programa la notificación con un showTrigger. Puede ser una TimestampTrigger con la fecha objetivo seleccionada con anterioridad. La notificación de recordatorio se activará de manera local, no es necesaria ninguna 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),
  });
}

Como con todo lo demás 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 Greetings de Fugu, un mensaje me pregunta cuándo quiero que termine mi tarjeta de felicitación.

La app de Fugu Greetings con un mensaje que le pregunta al usuario cuándo quiere recibir un recordatorio para terminar su tarjeta de felicitación.
Programa una notificación local para que se te recuerde terminar una tarjeta de felicitación.

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

El Centro de notificaciones de macOS muestra una notificación activada de Fugu Greetings.
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 durante todo el tiempo hasta que la inspiración te da un beso. Lo peor que puede suceder es que se apague la pantalla. La API de Wake Lock puede evitar que esto suceda.

El primer paso es obtener un bloqueo de activación con el 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 una notificación cuando se libere el bloqueo de activación. Esto puede ocurrir, por ejemplo, cuando cambia la visibilidad de la pestaña. Si esto sucede, cuando la pestaña vuelva a estar visible, puedo volver a obtener el bloqueo de activación.

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 admite 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 marcas la casilla de verificación sobre insomnio, la pantalla se mantiene activa.
La casilla de verificación Insomnio mantiene activa la app.

La API de Idle Detection

A veces, incluso si miras la pantalla durante horas, es inútil y no se te ocurre la menor idea de qué hacer con tu tarjeta. La API de Idle Detection permite que la app detecte el tiempo de inactividad del usuario. Si el usuario está inactivo durante demasiado tiempo, la app se restablece al estado inicial y borra el lienzo. Actualmente, esta API se encuentra restringida detrás del permiso de notificaciones, ya que muchos de los casos de uso de producción de detección de inactividad están relacionados con notificaciones; por ejemplo, para enviar solo una notificación a un dispositivo que el usuario está utilizando de forma activa en ese momento.

Después de asegurarme de que se otorguen los permisos de notificaciones, creo una instancia del detector de inactividad. Registre un objeto de escucha de eventos que escucha los cambios inactivos, lo que incluye el usuario y el estado de la pantalla. El usuario puede estar activo o inactivo, y la pantalla se puede desbloquear o bloquear. Si el usuario está inactivo, el lienzo se borra. Le otorgo un umbral de 60 segundos al detector inactivo.

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,
});

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

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

En la app de saludos de Fugu, el lienzo se borra cuando se marca la casilla de verificación Efímera y el usuario permanece inactivo durante mucho tiempo.

La app de saludo de Fugu con un lienzo borrado después de que el usuario estuvo inactivo durante demasiado tiempo
Cuando se marque la casilla de verificación Efímera y el usuario haya estado inactivo durante demasiado tiempo, se borrará el lienzo.

Closing

¡Qué paseo! Hay tantas APIs en una sola app de ejemplo. Recuerda que nunca hago que el usuario pague el costo de descarga de una función que su navegador no admite. Con la mejora progresiva, me aseguro de que se cargue solo el código relevante. Debido a que con HTTP/2, las solicitudes son económicas, este patrón debería funcionar bien en muchas aplicaciones, aunque es posible que desees considerar un agrupador para apps realmente grandes.

El panel Network de las Herramientas para desarrolladores de Chrome muestra solo solicitudes de archivos con código compatible con el navegador actual.
La pestaña Red de las Herramientas para desarrolladores de Chrome que solo muestra solicitudes de archivos con código compatible con el navegador actual.

Es posible que la app se vea un poco diferente en cada navegador, ya que no todas las plataformas admiten todas las funciones, pero la funcionalidad principal siempre está ahí y se mejora de forma progresiva según las capacidades del navegador en particular. Ten en cuenta que estas funciones pueden cambiar incluso en un solo navegador, en función de si la app se ejecuta como una app instalada o en una pestaña del navegador.

Saludos de Fugu en Chrome para Android, que muestra muchas funciones disponibles.
Saludos de Fugu en Chrome para Android.
Fugu Greetings que se ejecuta en Safari para computadoras de escritorio, que muestra menos funciones disponibles.
Fugu Greetings que se ejecuta en Safari para computadoras.
Se están ejecutando los saludos de Fugu en la versión de Chrome para computadoras. Se muestran muchas funciones disponibles.
Saludos de Fugu en Chrome para computadoras de escritorio.

Si te interesa la app de Fugu Greetings, búscala y bifurcala en GitHub.

Repositorio de saludos de Fugu en GitHub.
App de Fugu Greetings en GitHub.

El equipo de Chromium trabaja arduamente para que el césped sea más verde cuando se trata de las APIs avanzadas de Fugu. Cuando aplico la mejora progresiva en el desarrollo de mi app, me aseguro de que todos tengan una experiencia de referencia positiva y sólida, pero que las personas que usan navegadores compatibles con más APIs de plataformas web obtengan una experiencia aún mejor. Espero ver qué lograrás con la mejora progresiva en tus aplicaciones.

Agradecimientos

Agradezco a Christian Liebel y Hemanth HM, quienes contribuyeron con Fugu Greetings. Joe Medley y Kayce Basques revisaron este artículo. Jake Archibald me ayudó a descubrir la situación con import() dinámicos en un contexto de service worker.