Mejora progresivamente tu app web progresiva

Crea contenido para navegadores modernos y mejora progresivamente como en 2003

En marzo de 2003, Nick Finck y Steve Champeon sorprendió al mundo del diseño web con el concepto de mejora progresiva, una estrategia de diseño web que hace hincapié en cargar primero el contenido central de la página web y que agrega progresivamente más matices y técnicamente rigurosas de presentación y funciones además del contenido. Mientras que en 2003, la mejora progresiva consistía en utilizar, en ese momento, funciones de CSS, JavaScript discreto y hasta solo Gráficos vectoriales escalables. La mejora progresiva en 2020 y en el futuro se trata de usar con las 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.
Diapositiva: Diseño web inclusivo para el futuro con mejoras progresivas. (Fuente)
.

JavaScript moderno

En cuanto a JavaScript, la situación de compatibilidad con el navegador para el último núcleo de JavaScript de ES 2015 las funciones es genial. El nuevo estándar incluye promesas, módulos, clases, literales de plantillas, funciones de flecha, let y const, los parámetros predeterminados, los generadores, la asignación de desestructuración, el resto y la dispersión, Map/Set, WeakMap/WeakSet y muchas más. Todos son compatibles.

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

async Functions, una función de ES 2017, y una de mis favoritas. están disponibles en todos los principales navegadores. Las palabras clave async y await habilitan un comportamiento asíncrono basado en promesas. se escriban en un estilo más limpio, lo que evita la necesidad de configurar explícitamente cadenas de promesas.

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

Incluso las incorporaciones de idiomas de ES 2020 más recientes, como encadenamiento opcional y coalescente nulo llegaron al soporte rápidamente. A continuación, puedes ver una muestra de código. Cuando se trata de las funciones principales de JavaScript, el césped no puede ser mucho más ecológico 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 de la hierba verde de Windows XP.
El césped es verde cuando se trata de las funciones principales de JavaScript. (Captura de pantalla de Microsoft, utilizada con permission).

La app de ejemplo: Fugu Greetings

Para este artículo, trabajo con una AWP simple, llamada Saludos de Fugu (GitHub). El nombre de esta app es una muestra del sombrero de Project Fugu 🐡, un esfuerzo por darle todo a la Web la potencia de las aplicaciones para Android/iOS/computadoras de escritorio. Puedes leer más 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 enviar a tus seres queridos. Es un ejemplo de Conceptos básicos de la AWP. Es son confiables y están completamente habilitados sin conexión. tienes una red, puedes seguir usándola. También es instalable. a la pantalla de inicio 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.
App de ejemplo Fugu Greetings
.

Mejora progresiva

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

La mejora progresiva es una filosofía de diseño que proporciona una base de contenido esencial y funciones para la mayor cantidad de usuarios posible, a la vez que ofreciendo la mejor experiencia posible solo a los usuarios de los usuarios que puedan ejecutar todo el código requerido.

Detección de funciones se usa para determinar si los navegadores pueden admitir una funcionalidad más moderna, mientras que polyfills se usan a menudo para agregar con JavaScript las funciones que faltan.

[…]

La mejora progresiva es una técnica útil que permite a los desarrolladores web centrarse en desarrollar los mejores sitios web posibles y, al mismo tiempo, hacer que esos sitios web funcionen en varios usuarios-agentes desconocidos. Degradación correcta está relacionado, pero no es lo mismo y a menudo se ve como que va en la dirección opuesta. hasta la mejora progresiva. En realidad, ambos enfoques son válidos y, a menudo, se pueden complementar entre sí.

Colaboradores de MDN

Empezar cada tarjeta desde cero puede ser muy engorroso. Entonces, ¿por qué no tener una función que les permita a los usuarios importar una imagen y comenzar desde allí? Con un enfoque tradicional, hubieras usado un <input type=file> para hacer que esto suceda. Primero, debes crear el elemento, establecer su type en 'file' y agregar tipos de MIME a la propiedad accept. y, luego, “haga clic” de manera programática y presten atención a 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 un atributo de importación, probablemente debería haber un atributo de exportación. para que los usuarios puedan guardar sus tarjetas de felicitaciones de manera local. La forma tradicional de guardar archivos es crear un vínculo de anclaje con un download y con una URL de BLOB como su href. También deberías “hacer clic” de manera programática para activar la descarga, y, para evitar fugas de memoria, 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 has "descargado" una tarjeta festiva, tienes "guardado" que la modifica. En lugar de mostrarte un mensaje de “guardar” que te permite elegir dónde colocar el archivo, El navegador descargó directamente la tarjeta festiva sin interacción del usuario. y lo colocará directamente en la carpeta Descargas. Esto no es lo mejor.

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

¿Cómo puedo detectar una API? La API de File System Access expone un método window.chooseFileSystemEntries() nuevo. En consecuencia, debo cargar condicionalmente diferentes módulos de importación y exportación en función 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. Carga las secuencias de comandos heredadas en navegadores que actualmente no admiten 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 cómo se cargan los archivos heredados.
Pestaña de red del Inspector web de Safari.
Herramientas para desarrolladores de Firefox que muestran cómo se cargan los archivos heredados.
. En la 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 secuencias de comandos nuevas. Esto es posible gracias a import() dinámico, que todos los navegadores modernos asistencia. Como dije antes, el pasto es bastante verde estos días.

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

La API de File System Access

Ahora que resolvimos este problema, es momento de ver la implementación real basada en la API de File System Access. Para importar una imagen, llamo a window.chooseFileSystemEntries(). y pasarle una propiedad accepts donde supongo que quiero archivos de imagen. Se admiten extensiones de archivo y tipos de MIME. Esto da como resultado un controlador de archivos, 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 Necesito pasar un parámetro de tipo de 'save-file' al método chooseFileSystemEntries(). Desde aquí, aparece un diálogo para guardar el archivo. Con el archivo abierto, esto no era necesario porque 'open-file' es la configuración predeterminada. Configuré el parámetro accepts de manera similar a antes, pero esta vez limitado a imágenes PNG. Aparece el controlador de archivos, pero en lugar de obtenerlo Esta vez, creo una transmisión que admite 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 la transmisión con capacidad de escritura.

Todo puede fallar siempre: el disco podría estar sin espacio podría haber un error de escritura o lectura, o tal vez el usuario cancele el diálogo del archivo. Por eso, 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);
  }
};

El uso de 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 mis ediciones y, finalmente, guardarlas en un cuadro de diálogo de guardado real. donde puedo elegir el nombre y la ubicación de almacenamiento del archivo. Ahora el archivo está listo para conservarlo por siempre.

La app de saludos de Fugu con un diálogo de archivo abierto.
El diálogo para abrir el archivo.
La app de saludos de Fugu ahora con una imagen importada.
. Es la imagen importada.
App de saludos 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 almacenarlo para siempre, quizá quiera compartir mi tarjeta de felicitación. Esto es algo que la API de Web Share y la API de Web Share Target me permiten hacer lo siguiente. Los dispositivos móviles y, más recientemente, los sistemas operativos para computadoras de escritorio ahora tienen funciones de uso compartido integradas. con mecanismos de control de acceso clave. Por ejemplo, a continuación se muestra la hoja para compartir de Safari de escritorio en macOS activada a partir de un artículo sobre mi blog Cuando haces clic en el botón Compartir artículo, puedes compartir un vínculo al artículo con un amigo, por ejemplo, a través de la app de Mensajes de macOS.

Hoja para compartir de Safari para computadoras en macOS que se activa desde el botón Compartir de un artículo
API de Web Share en Safari para computadoras de escritorio y macOS.

El código para hacerlo es bastante sencillo. Llamo a navigator.share() y y pasarle title, text y url opcionales en un objeto ¿Qué sucede si quiero adjuntar una imagen? El nivel 1 de la API de Web Share aún no lo admite. La buena noticia es que Web Share nivel 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 solicitud de tarjeta de felicitación de Fugu. Primero, debo preparar un objeto data con un array files que consista en un BLOB y, luego, un title y un text. A continuación, como práctica recomendada, usaré el nuevo método navigator.canShare(), que hace lo siguiente: lo que su nombre sugiere: Me indica si, técnicamente, el navegador puede compartir el objeto data que intento compartir. Si navigator.canShare() me dice que los datos se pueden compartir, estoy listo para llama 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, utilizo la mejora progresiva. Si 'share' y 'canShare' existen en el objeto navigator, entonces, avanzaré y carga share.mjs mediante import() dinámico. No cargo en navegadores, como Safari para dispositivos móviles, que solo cumplen una de las dos condiciones. la funcionalidad.

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

En los saludos de Fugu, si presiono el botón Compartir en un navegador compatible, como Chrome en Android, se abrirá la hoja incorporada para compartir. Por ejemplo, puedo elegir Gmail y el widget de redacción de correo electrónico aparece con la imagen adjunta.

Hoja para compartir a nivel del SO que muestra varias apps con las que se puede 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 adjuntará a un nuevo correo electrónico en el compositor de Gmail.

API del selector de contactos

Ahora, quiero hablar de los contactos, es decir, la libreta de direcciones de un dispositivo o una app de administración de contactos. Cuando escribes una tarjeta festiva, puede que no siempre sea fácil escribirlo correctamente el nombre de una persona. Por ejemplo, tengo un amigo Sergey que prefiere que su nombre se escriba con letras cirílicas. Soy con un teclado QWERTZ alemán y no sabes cómo escribir su nombre. Este es un problema que puede resolver la API de Contact Picker. Como tengo a mi amigo almacenado en la app de Contactos del teléfono, con la API del 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 interesarme números de teléfono, correos electrónicos, avatares iconos o direcciones físicas. A continuación, configuro un objeto options y configuro multiple como true para poder seleccionar más de una entrada. Por último, puedo llamar a navigator.contacts.select(), que devuelve las propiedades deseadas. 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, ahora, es probable que ya hayas aprendido el patrón: Solo puedo cargar el archivo cuando la API es realmente compatible.

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

En Fugu Greeting, cuando presiono el botón Contactos y selecciono a mis dos mejores amigos, ерей майловиmur Бри Recuerda y 劳伦斯·爱德华·"拉里"·佩奇, puedes ver cómo el selector de contactos está limitado a mostrar solo sus nombres, pero no sus direcciones de correo electrónico u otra información, como sus números de teléfono. Luego, sus nombres aparecen dibujados en mi tarjeta festiva.

Selector de contactos que muestra los nombres de dos contactos de la libreta de direcciones.
Selección de dos nombres con el selector de contactos de la libreta de direcciones.
Los nombres de los dos contactos seleccionados anteriormente dibujados en la tarjeta de felicitaciones
. Los dos nombres se dibujan en la tarjeta de felicitación.

La API de Aasync 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 felicitaciones, a veces es posible que quiera hacer lo mismo. Me gustaría pegar una imagen en una tarjeta en la que estoy trabajando o copiar mi tarjeta festiva para poder seguir editándola desde en otro lugar. La API de Async Clipboard, admite texto e imágenes. Te explicaré cómo agregué la compatibilidad con la función de copiar y pegar a Fugu. App de saludos.

Para copiar algo en el portapapeles del sistema, debo escribir en él. El método navigator.clipboard.write() toma un array de elementos del portapapeles como una parámetro. En esencia, cada elemento del portapapeles es un objeto con un BLOB como valor y del tipo de BLOB. como la clave.

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

Para pegarlo, debo repetir indefinidamente los elementos del portapapeles que obtengo llamando a navigator.clipboard.read() Esto se debe a que varios elementos del portapapeles podrían estar en el portapapeles en representaciones diferentes. Cada elemento del portapapeles tiene un campo types que me indica los tipos de MIME de los de Google Cloud. Llamo al método getType() del elemento del portapapeles y pasé el Es 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 casi no hace falta decirlo por ahora. Solo hago esto 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 versión preliminar de macOS y cópialo en el portapapeles. Cuando hago clic en Pegar, la app de saludos de Fugu me pregunta. si quiero permitir que la app vea texto e imágenes en el portapapeles.

La app de saludos de Fugu muestra el mensaje de permiso del portapapeles.
El mensaje de permiso del portapapeles.

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

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

API de Badging

Otra API útil es la API de insignias. Como AWP instalable, Fugu Greetings tiene un ícono de la app que los usuarios pueden colocar en el acoplamiento de apps o en la pantalla principal. Una forma divertida y sencilla de demostrar la API es (ab) usarla en Fugu Greetings como un contador de trazos de lápiz. Agregué un objeto de escucha de eventos que aumenta el contador de trazos del lápiz cada vez que ocurre el evento pointerdown y, luego, establece la insignia del ícono actualizada. Cuando 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 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 festiva, cada uno con solo un 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 saludos de Fugu que muestra el número 7
. El contador de trazos de lápiz en la forma de la insignia del ícono de la app

La API de Periodic Background Sync

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

El primer paso es registrar 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, para que el usuario obtenga 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 detectar 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 propagó a todos los clientes, por lo que pueden 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,
          });
        });
      })()
    );
  }
});

Una vez más, esto es una mejora verdaderamente progresiva, por lo que el código solo se carga cuando la La API es compatible con el navegador. Esto se aplica al código del cliente y al código del service worker. En los navegadores no compatibles, no se cargará ninguno de ellos. Observa cómo en el service worker, en lugar de en un import() dinámico (esto no se admite en un contexto de service worker todavía), Utilizo la versión clásica 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 los saludos de Fugu, cuando presionas el botón Fondo de pantalla se revela 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 felicitaciones del día.
Cuando 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 codazo para terminar un saludo ya iniciado tarjeta. Esta es una función que habilita la API de activadores de notificaciones. Como usuario, puedo ingresar una hora en la que quiero que se me sugiera que termine mi tarjeta de felicitación. Cuando llegue ese momento, recibiré una notificación en la que se indicará que mi tarjeta festiva está esperando.

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 del recordatorio se activará de manera local, no es necesario tener acceso a la red ni al 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, para que el código solo se cargue de forma condicional.

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

Cuando marco la casilla de verificación Recordatorio en los saludos de Fugu, aparece un mensaje en el que se pregunta cuando quiero que me recuerden terminar mi tarjeta de felicitación.

La app de saludos de Fugu con un mensaje en el que se le pregunta al usuario cuándo quiere que se le recuerde que debe terminar la tarjeta de felicitación.
Se está programando una notificación local para que se te recuerde que debes terminar una tarjeta festiva.

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

Centro de notificaciones de macOS en el que se muestra una notificación activada de Fugu Greetings.
La notificación activada aparecerá en el Centro de notificaciones de macOS.

API de Wake Lock

También quiero incluir la API de Wake Lock. A veces, solo tienes que mirar la pantalla el tiempo suficiente hasta inspirarte te beso. 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 que me informe cuando se suelta el bloqueo de activación. Esto puede ocurrir, por ejemplo, cuando cambia la visibilidad de la pestaña. En ese caso, 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, así que solo necesito cargarla cuando el navegador es compatible con la API.

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

En los saludos de Fugu, hay una casilla de verificación de Insomnio que, si se marca, mantiene pantalla activa.

Si se marca la casilla de verificación de 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 de felicitación. La API de Idle Detection permite que la app detecte el tiempo de inactividad de los usuarios. Si el usuario permanece inactivo durante mucho tiempo, la app se restablece al estado inicial. y borra el lienzo. Actualmente, esta API se encuentra protegida por permiso de notificaciones, ya que muchos 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á usando en ese momento.

Después de asegurarme de que se otorgó el permiso de notificaciones, creo una instancia del de inactividad del detector de inactividad. Registro de un objeto de escucha de eventos que escucha cambios inactivos, lo que incluye al usuario 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 de inactividad.

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 saludo de Fugu, el lienzo se borra cuando la casilla de verificación Efímera aparece y el usuario permanece inactivo durante demasiado tiempo.

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

Closing

¡Qué paseo! Muchas APIs en solo una app de ejemplo. Recuerda que nunca hago que el usuario pague el costo de descarga por una función que su navegador no admite. Al usar la mejora progresiva, me aseguro de que solo se cargue el código relevante. Y dado que con HTTP/2 las solicitudes son económicas, este patrón debería funcionar bien aplicaciones, aunque deberías considerar un agrupador para apps muy grandes.

El panel Network de Chrome DevTools muestra solo solicitudes de archivos con código compatible con el navegador actual.
La pestaña Network de Chrome DevTools muestra solo solicitudes de archivos con código compatible con el navegador actual.

La aplicación puede verse un poco diferente en cada navegador, ya que no todas las plataformas son compatibles con todas las funciones, pero la funcionalidad principal siempre está ahí; mejora progresivamente de acuerdo con las capacidades de cada navegador. Ten en cuenta que estas funciones pueden cambiar incluso en un solo navegador, dependiendo de si la app se ejecuta como una app instalada o en una pestaña del navegador.

Saludo de Fugu en Android Chrome, que muestra muchas funciones disponibles.
Saludos de Fugu que se ejecuta en Android Chrome.
Saludo de Fugu en Safari de escritorio y muestra menos funciones disponibles.
. Saludos de Fugu en Safari para computadoras de escritorio.
Saludo de Fugu en Chrome para computadoras de escritorio que muestra muchas funciones disponibles.
. Saludos de Fugu en Chrome para computadoras de escritorio.

Si te interesa la app de Fugu Greetings, haz lo siguiente: ve a buscarlo y bifurcarlo en GitHub.

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

El equipo de Chromium está trabajando arduamente para que el césped sea más ecológico cuando se trata de las APIs avanzadas de Fugu. Al aplicar una mejora progresiva en el desarrollo de mi app, Me aseguro de que todos tengan una buena experiencia de referencia, pero que quienes usan navegadores compatibles con más APIs de plataformas web obtienen una experiencia aun mejor. Espero ver lo que lograrás con la mejora progresiva en tus apps.

Agradecimientos

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