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.

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.

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.

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

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.


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.

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.



Las APIs de Web Share y Web Share Target
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.


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.


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.

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


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.

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.

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

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.

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.

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.



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.