Desenvolvendo para navegadores modernos e melhorando progressivamente como é 2003
Em março de 2003, Nick Finck e Steve Champeon impressionaram o mundo do Web design com o conceito de aprimoramento progressivo, uma estratégia de web design que enfatiza o carregamento do conteúdo principal da página da Web primeiro, e que depois adiciona progressivamente camadas de apresentação e recursos mais sutis e tecnicamente rigorosas ao conteúdo. Em 2003, o aprimoramento progressivo tratava do uso, na época, de recursos modernos do CSS, JavaScript discretos e até mesmo gráficos vetoriais escaláveis. O aprimoramento progressivo em 2020 e além envolve o uso de recursos modernos do navegador.
JavaScript moderno
Em relação ao JavaScript, a situação do suporte aos navegadores para os principais recursos principais de JavaScript do ES 2015 é ótima.
O novo padrão inclui promessas, módulos, classes, literais de modelo, funções de seta, let
e const
,
parâmetros padrão, geradores, atribuição de desestruturação, repouso e propagação, Map
/Set
,
WeakMap
/WeakSet
e muito mais.
Todas são aceitas.
Funções assíncronas, um recurso do ES 2017 e um dos meus favoritos pessoais,
podem ser usadas
em todos os principais navegadores.
As palavras-chave async
e await
permitem que um comportamento assíncrono baseado em promessas seja escrito em um estilo mais limpo, evitando a necessidade de configurar explicitamente cadeias de promessas.
E até mesmo adições recentes à linguagem ES 2020, como encadeamento opcional e coalescência anulada, chegaram à compatibilidade rapidamente. Confira um exemplo de código abaixo. Quando se trata dos principais recursos de JavaScript, o gramado não poderia ser muito mais ecológico do que hoje.
const adventurer = {
name: 'Alice',
cat: {
name: 'Dinah',
},
};
console.log(adventurer.dog?.name);
// Expected output: undefined
console.log(0 ?? 42);
// Expected output: 0
App de exemplo: Fugu Greetings
Neste artigo, trabalho com um PWA simples chamado Fugu Greetings (GitHub). O nome desse app é uma referência ao Projeto Fugu 🐡, uma iniciativa para dar à Web todo o poder dos apps Android/iOS/desktop. Leia mais sobre o projeto na página de destino dele.
O Fugu Greetings é um app de desenho que permite criar cartões comemorativos virtuais e enviá-los para quem você ama. Ele exemplifica os principais conceitos do PWA. Ele é confiável e totalmente ativado off-line, portanto, mesmo que você não tenha uma rede, ainda poderá usá-la. Ele também é instalável na tela inicial de um dispositivo e se integra perfeitamente ao sistema operacional como um aplicativo independente.
Aprimoramento progressivo
Com isso em mente, é hora de falar sobre o aprimoramento progressivo. O glossário de documentos da Web do MDN define o conceito da seguinte maneira:
O aprimoramento progressivo é uma filosofia de design que oferece uma linha de base de conteúdo e funcionalidade essenciais para o maior número possível de usuários, ao mesmo tempo em que oferece a melhor experiência possível somente aos usuários dos navegadores mais modernos que podem executar todo o código necessário.
Geralmente, a detecção de recursos é usada para determinar se os navegadores podem lidar com funcionalidades mais modernas, enquanto os polyfills são usados para adicionar recursos ausentes com JavaScript.
[…]
O aprimoramento progressivo é uma técnica útil que permite que os desenvolvedores da Web se concentrem no desenvolvimento dos melhores sites possíveis, fazendo com que eles funcionem com vários user agents desconhecidos. A degradação suave está relacionada, mas não é a mesma coisa, e geralmente é vista como um passo na direção oposta do aprimoramento progressivo. Na realidade, ambas as abordagens são válidas e muitas vezes podem se complementar.
Colaboradores do MDN
Iniciar cada cartão comemorativo do zero pode ser muito complicado.
Então, por que não criar um recurso que permita aos usuários importar uma imagem e começar daí?
Com uma abordagem tradicional, você usaria um elemento
<input type=file>
para fazer isso acontecer.
Primeiro, você precisa criar o elemento, definir a type
como 'file'
e adicionar tipos MIME à propriedade accept
.
Em seguida, "clique" nele de maneira programática e detecte as mudanças.
Quando você seleciona uma imagem, ela é importada diretamente para a tela.
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();
});
};
Quando há um recurso de import, provavelmente há um recurso de import para que os usuários possam salvar os cartões comemorativos localmente.
A maneira tradicional de salvar arquivos é criar um link fixo
com um atributo download
e com um URL de blob como href
.
Você também clicaria nele de maneira programática para acionar o download e, para evitar vazamentos de memória, não se esqueça de revogar o URL do objeto de 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();
};
Mas espere um minuto. Mentalmente, você não "fez o download" de um cartão comemorativo, mas o "salvou". Em vez de mostrar uma caixa de diálogo "salvar" que permite escolher onde colocar o arquivo, o navegador fez o download direto do cartão comemorativo, sem interação do usuário, e o colocou diretamente na pasta "Downloads". Isso não é bom.
E se houvesse uma maneira melhor? E se você pudesse abrir um arquivo local, editá-lo e salvar as modificações, em um novo arquivo ou de volta no arquivo original aberto? Acontece que existe. A API File System Access permite abrir e criar arquivos e diretórios, além de modificá-los e salvá-los .
Como faço para detectar uma API?
A API File System Access expõe um novo método window.chooseFileSystemEntries()
.
Consequentemente, preciso carregar condicionalmente diferentes módulos de importação e exportação dependendo da disponibilidade desse método. Abaixo mostramos como fazer isso.
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'),
]);
}
};
Mas antes de conferir os detalhes da API File System Access, vou destacar rapidamente o padrão de aprimoramento progressivo. Em navegadores que não são compatíveis com a API File System Access, carrego os scripts legados. Abaixo você encontra as guias de rede do Firefox e do Safari.
No entanto, no Chrome, um navegador que oferece suporte à API, somente os novos scripts são carregados.
Isso é possível graças ao
import()
dinâmico, que é
compatível com todos os navegadores mais recentes.
Como eu disse antes, a grama é muito verde hoje em dia.
A API File System Access
Agora que abordamos isso, é hora de analisar a implementação real com base na API File System Access.
Para importar uma imagem, chamo window.chooseFileSystemEntries()
e transmito uma propriedade accepts
para informar que quero arquivos de imagem.
Tanto as extensões de arquivo quanto os tipos MIME são compatíveis.
Isso resulta em um identificador de arquivo, de onde posso receber o arquivo real chamando 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);
}
};
A exportação de uma imagem é quase a mesma, mas desta vez
preciso transmitir um parâmetro de tipo 'save-file'
para o método chooseFileSystemEntries()
.
A partir disso, recebo uma caixa de diálogo para salvar o arquivo.
Com o arquivo aberto, isso não era necessário porque 'open-file'
é o padrão.
Defini o parâmetro accepts
de forma semelhante ao anterior, mas desta vez limitado apenas a imagens PNG.
Novamente, obtenho um identificador de arquivo, mas, em vez de obter o arquivo,
desta vez, crio um stream gravável chamando createWritable()
.
Em seguida, escrevo o blob, que é a imagem do meu cartão comemorativo, no arquivo.
Finalmente, fecho o fluxo gravável.
Tudo pode sempre falhar: o disco pode estar sem espaço,
pode haver um erro de gravação ou leitura ou talvez simplesmente o usuário cancele a caixa de diálogo do arquivo.
É por isso que sempre uno as chamadas em uma instrução 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);
}
};
Usando o aprimoramento progressivo com a API File System Access, posso abrir um arquivo como antes. O arquivo importado é desenhado diretamente na tela. Consigo fazer minhas edições e, por fim, salvá-las com uma caixa de diálogo de salvamento real em que posso escolher o nome e o local de armazenamento do arquivo. Agora o arquivo está pronto para ser preservado para sempre.
As APIs Web Share e Web Share Target
Além de armazenar para sempre, talvez eu queira compartilhar meu cartão comemorativo. Isso é algo que eu posso fazer com a API Web Share e a API Web Share Target. Os sistemas operacionais para dispositivos móveis e, mais recentemente, os de computador ganharam mecanismos de compartilhamento integrados. Por exemplo, confira abaixo a planilha de compartilhamento do Safari para computador no macOS acionada a partir de um artigo no meu blog. Ao clicar no botão Share Article, você pode compartilhar um link para o artigo com um amigo, por exemplo, pelo app macOS Messages.
O código para fazer isso acontecer é bem simples. Eu chamo navigator.share()
e
transmito um title
, text
e url
opcionais em um objeto.
Mas e se eu quiser anexar uma imagem? O nível 1 da API Web Share ainda não é compatível com isso.
A boa notícia é que o nível 2 do compartilhamento da Web adicionou recursos de compartilhamento de arquivos.
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);
}
Vou mostrar como fazer isso com o aplicativo de Saudação do Fugu.
Primeiro, precisamos preparar um objeto data
com uma matriz files
que consiste em um blob, depois
um title
e um text
. Em seguida, como prática recomendada, uso o novo método navigator.canShare()
, que faz
o que o nome sugere:
Ele me diz se o objeto data
que estou tentando compartilhar pode tecnicamente ser compartilhado pelo navegador.
Se navigator.canShare()
disser que os dados podem ser compartilhados, posso chamar navigator.share()
como antes.
Como tudo pode falhar, vou usar novamente um bloco 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 o aprimoramento progressivo.
Se 'share'
e 'canShare'
existirem no objeto navigator
, somente então vou avançar e
carregar share.mjs
via import()
dinâmico.
Em navegadores como o Safari para dispositivos móveis, que atendem a apenas uma das duas condições, não carrego
a funcionalidade.
const loadShare = () => {
if ('share' in navigator && 'canShare' in navigator) {
import('./share.mjs');
}
};
No Fugu Saudações, se eu tocar no botão Compartilhar em um navegador compatível, como o Chrome no Android, a página de compartilhamento integrada será aberta. Posso, por exemplo, escolher o Gmail e o widget do Editor de e-mail aparecerá com a imagem anexada.
API Contact Picker
Em seguida, quero falar sobre contatos, ou seja, a agenda ou o aplicativo de gerenciamento de contatos de um dispositivo. Quando você escreve um cartão comemorativo, nem sempre é fácil escrever corretamente o nome de alguém. Por exemplo, tenho um amigo Sergey que prefere que o nome dele seja escrito em letras cirílicas. Estou usando um teclado QWERTZ alemão e não tenho ideia de como digitar o nome dele. Esse é um problema que a API Contact Picker pode resolver. Como tenho um amigo armazenado no app de contatos do meu smartphone, com a API Contatos Picker, posso tocar nos meus contatos da Web.
Primeiro, preciso especificar a lista de propriedades que quero acessar.
Nesse caso, só quero os nomes, mas, para outros casos de uso, posso ter interesse em números de telefone, e-mails, ícones de avatar ou endereços físicos.
Em seguida, configuro um objeto options
e defino multiple
como true
, para que eu possa selecionar mais
de uma entrada.
Por fim, posso chamar navigator.contacts.select()
, que retorna as propriedades desejadas para os contatos selecionados pelo usuário.
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);
}
};
Provavelmente, você já aprendeu o padrão: só carrego o arquivo quando a API é realmente compatível.
if ('contacts' in navigator) {
import('./contacts.mjs');
}
Em "Saudação do Fugu", quando toco no botão Contatos e seleciono meus dois melhores amigos, "></ергей وи eficiênciaайловия Брин e 劳伦斯·爱德华·"拉里"·佩奇, mostra apenas os endereços de e-mail ou os endereços de e-mail deles. Seus nomes são então desenhados no meu cartão comemorativo.
API Async Clipboard
A seguir, vamos copiar e colar. Uma das nossas operações favoritas como desenvolvedores de software é copiar e colar. Como autor de cartões comemorativos, às vezes, quero fazer o mesmo. Posso colar uma imagem em um cartão comemorativo em que estou trabalhando ou copiar meu cartão comemorativo para continuar editando de outro lugar. A API Async Clipboard, oferece suporte a texto e imagens. Vou mostrar como adicionei o suporte de copiar e colar ao app Fugu Greetings.
Para copiar algo para a área de transferência do sistema, preciso escrever o código.
O método navigator.clipboard.write()
usa uma matriz de itens da área de transferência como
parâmetro.
Cada item da área de transferência é essencialmente um objeto com um blob como valor e o tipo do blob
como a chave.
const copy = async (blob) => {
try {
await navigator.clipboard.write([
new ClipboardItem({
[blob.type]: blob,
}),
]);
} catch (err) {
console.error(err.name, err.message);
}
};
Para colar, preciso repetir os itens da área de transferência recebidos chamando
navigator.clipboard.read()
.
O motivo é que vários itens da área de transferência podem estar na área de transferência em
representações diferentes.
Cada item da área de transferência tem um campo types
que informa os tipos MIME dos recursos
disponíveis.
Eu chamo o método getType()
do item da área de transferência, passando o tipo MIME que recebi 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);
}
};
E é quase desnecessário dizer a essa altura. Isso só é feito em navegadores compatíveis.
if ('clipboard' in navigator && 'write' in navigator.clipboard) {
import('./clipboard.mjs');
}
Então, como isso funciona na prática? Tenho uma imagem aberta no app macOS Preview e copia para a área de transferência. Quando clico em Colar, o app Fugu Greetings pergunta se eu quero permitir que ele veja os textos e as imagens na área de transferência.
Por fim, depois de aceitar a permissão, a imagem é colada no aplicativo. O contrário também funciona. Vou copiar um cartão comemorativo para a área de transferência. Quando abro a visualização, clico em Arquivo e depois em Novo na área de transferência, o cartão é colado em uma nova imagem sem título.
API Badging
Outra API útil é a Badging API.
Como um PWA instalável, o Fugu Greetings tem um ícone de app
que os usuários podem colocar na base do app ou na tela inicial.
Uma maneira fácil e divertida de demonstrar a API é usá-la no Fugu Greetings como um contador de traçados de caneta.
Adicionei um listener de eventos que incrementa o contador de traços de caneta sempre que o evento pointerdown
ocorre
e define o selo de ícone atualizado.
Sempre que a tela for apagada, o contador será redefinido, e o selo será removido.
let strokes = 0;
canvas.addEventListener('pointerdown', () => {
navigator.setAppBadge(++strokes);
});
clearButton.addEventListener('click', () => {
strokes = 0;
navigator.setAppBadge(strokes);
});
Esse recurso é um aprimoramento progressivo, de modo que a lógica de carregamento é normal.
if ('setAppBadge' in navigator) {
import('./badge.mjs');
}
Neste exemplo, desenhei os números de um a sete, usando um traço de caneta por número. O contador de selos no ícone agora está em sete.
API Periodic Background Sync
Quer começar cada dia do zero? Um recurso legal do app Fugu Greetings é que ele pode inspirar você todas as manhãs com uma nova imagem de plano de fundo para iniciar seu cartão comemorativo. O app usa a API Periodic Background Sync para fazer isso.
A primeira etapa é register um evento de sincronização periódica no registro do service worker.
Ele detecta uma tag de sincronização chamada 'image-of-the-day'
e tem um intervalo mínimo de um dia,
para que o usuário possa receber uma nova imagem de plano de fundo a 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);
}
};
A segunda etapa é detectar o evento periodicsync
no service worker.
Se a tag de evento for 'image-of-the-day'
, ou seja, a que foi registrada antes, a imagem do dia será recuperada pela função getImageOfTheDay()
e o resultado será propagado para todos os clientes, para que eles possam atualizar as telas e os caches.
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,
});
});
})()
);
}
});
Novamente, essa é uma melhoria progressiva, de modo que o código só é carregado quando a
API tem suporte do navegador.
Isso se aplica ao código do cliente e ao código do service worker.
Em navegadores não compatíveis, nenhum deles é carregado.
No service worker, em vez de um import()
dinâmico
(que ainda não tem suporte em um contexto de service worker
ainda),
uso a
importScripts()
clássica.
// 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');
}
No Fugu Greetings, pressionar o botão Papel de parede revela a imagem do cartão comemorativo do dia, que é atualizada todos os dias pela API Periodic Background Sync.
API Notification Triggers
Às vezes, mesmo com muita inspiração, você precisa de um empurrão para terminar um cartão de boas-vindas. Esse é um recurso ativado pela API Notification Triggers. Como usuário, posso inserir o horário em que quero receber um alerta para concluir meu cartão comemorativo. Quando chegar esse momento, vou receber uma notificação de que meu cartão comemorativo está esperando.
Depois de solicitar o horário de destino,
o aplicativo programa a notificação com um showTrigger
.
Pode ser um TimestampTrigger
com a data desejada selecionada anteriormente.
A notificação de lembrete será acionada localmente, não é necessário ter uma rede ou lado do 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),
});
}
Assim como tudo o mais que mostrei até agora, esse é um aprimoramento progressivo. Portanto, o código é carregado apenas condicionalmente.
if ('Notification' in window && 'showTrigger' in Notification.prototype) {
import('./notification_triggers.mjs');
}
Quando eu marco a caixa de seleção Lembrete em Fugu Saudações, um aviso me pergunta quando quero ser lembrado de terminar meu cartão comemorativo.
Quando uma notificação programada é acionada no Fugu Greetings, ela é exibida como qualquer outra notificação, mas, como escrevi antes, ela não exige uma conexão de rede.
A API Wake Lock
Também quero incluir a API Wake Lock. Às vezes, você só precisa olhar para a tela por tempo suficiente até que a inspiração te beije. O pior que pode acontecer é a tela ser desligada. A API Wake Lock pode impedir que isso aconteça.
A primeira etapa é conseguir um wake lock com o navigator.wakelock.request method()
.
Eu passo a string 'screen'
para receber um wake lock de tela.
Em seguida, adiciono um listener de eventos para ser informado quando o wake lock é liberado.
Isso pode acontecer, por exemplo, quando a visibilidade da guia muda.
Se isso acontecer, quando a guia ficar visível novamente, vou conseguir o wake lock.
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);
Sim, esse é um aprimoramento progressivo, então só preciso carregá-lo quando o navegador for compatível com a API.
if ('wakeLock' in navigator && 'request' in navigator.wakeLock) {
import('./wake_lock.mjs');
}
Em Fugu Greetings, há uma caixa de seleção Insomnia que, quando marcada, mantém a tela ligada.
API Idle Detection
Às vezes, mesmo que você fique olhando para a tela por horas, ela é inútil e você não consegue ter a menor ideia do que fazer com seu cartão comemorativo. A API Idle Detection permite que o app detecte o tempo de inatividade do usuário. Se o usuário ficar inativo por muito tempo, o app será redefinido para o estado inicial e a tela será limpa. Atualmente, essa API está protegida pela permissão de notificações, já que muitos casos de uso de produção de detecção de inatividade estão relacionados a notificações, por exemplo, para enviar uma notificação apenas a um dispositivo que o usuário esteja usando no momento.
Depois de confirmar que a permissão de notificações foi concedida, instancio o detector inativo. Registrei um listener de eventos que detecta mudanças inativas, o que inclui o usuário e o estado da tela. O usuário pode estar ativo ou inativo, e a tela pode ser desbloqueada ou bloqueada. Se o usuário estiver inativo, a tela será limpa. O detector de inatividade tem um limite 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,
});
Como sempre, só carrego esse código quando o navegador é compatível.
if ('IdleDetector' in window) {
import('./idle_detection.mjs');
}
No app Fugu Greetings, a tela apaga quando a caixa de seleção Ephemeral é marcada e o usuário fica inativo por muito tempo.
Encerramento
Uau, que bonitinho. Há tantas APIs em apenas um app de exemplo. E, lembre-se, eu nunca faço o usuário pagar o custo de download por um recurso que o navegador não suporta. Usando o aprimoramento progressivo, eu me certifico de que apenas o código relevante seja carregado. Como as solicitações do HTTP/2 são baratas, esse padrão funcionará bem para muitos aplicativos, embora você possa querer considerar um bundler para apps muito grandes.
O app pode parecer um pouco diferente em cada navegador, já que nem todas as plataformas oferecem suporte a todos os recursos, mas a funcionalidade principal está sempre lá, e ela é aprimorada progressivamente de acordo com as capacidades de cada navegador. Esses recursos podem mudar até no mesmo navegador, dependendo se o app está sendo executado como instalado ou em uma guia do navegador.
Se você tiver interesse pelo app Fugu Greetings, acesse e bifurque o app no GitHub (link em inglês).
A equipe do Chromium está trabalhando duro para tornar a grama mais ecológica quando se trata de APIs avançadas do Fugu. Ao aplicar o aprimoramento progressivo no desenvolvimento do meu app, garanto que todos tenham uma experiência básica boa e sólida, mas que as pessoas que usam navegadores com suporte a mais APIs de plataforma da Web tenham uma experiência ainda melhor. Estou ansioso para ver o que você fará com o aprimoramento progressivo nos seus apps.
Agradecimentos
Sou grato a Christian Liebel e
Hemanth HM, que contribuíram para o Fugu Greetings.
Este artigo foi revisado por Joe Medley e
Kayce Basques.
Jake Archibald me ajudou a descobrir a situação
com a import()
dinâmica em um contexto de service worker.