Criar para navegadores modernos e aprimorar progressivamente como se fosse 2003
Publicado em: 29 de junho de 2020
Em março de 2003, Nick Finck e Steve Champeon surpreenderam o mundo do web design com o conceito de melhoria progressiva, uma estratégia que enfatiza o carregamento do conteúdo principal da página da Web primeiro e, em seguida, adiciona progressivamente camadas mais sutis e tecnicamente rigorosas de apresentação e recursos sobre o conteúdo. Em 2003, o aprimoramento progressivo envolvia o uso de recursos modernos de CSS, JavaScript discreto e até mesmo elementos gráficos vetoriais escaláveis. O aprimoramento progressivo em 2020 e depois disso é sobre usar recursos modernos do navegador.
JavaScript moderno
Falando em JavaScript, a situação de suporte do navegador para os recursos mais recentes do
ES 2015 JavaScript é ótima. O novo padrão inclui promessas, módulos, classes, strings de modelo, funções de seta, let e const, parâmetros padrão, geradores, atribuição de desestruturação, rest e spread, Map/Set, WeakMap/WeakSet e muito mais.
Todos são aceitos.
As 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 o 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 de linguagem ES 2020 super recentes, como encadeamento opcional e junção nula receberam suporte muito rapidamente. Quando se trata de recursos principais do JavaScript, não há muito o que melhorar.
Exemplo:
const adventurer = {
name: 'Alice',
cat: {
name: 'Dinah',
},
};
console.log(adventurer.dog?.name);
// Expected output: undefined
console.log(0 ?? 42);
// Expected output: 0
O app de exemplo: Fugu Greetings
Neste documento, trabalho com um PWA chamado Fugu Greetings (GitHub). O nome deste app é uma homenagem ao Projeto Fugu 🐡, uma iniciativa para dar à Web todos os recursos dos aplicativos Android, iOS e para computador. Saiba mais sobre o projeto na página de destino.
O Fugu Greetings é um app de desenho que permite criar cartões virtuais e enviá-los para as pessoas que você ama. Ele exemplifica os principais conceitos de PWAs. Ele é confiável e totalmente ativado para uso off-line. Assim, mesmo que você não tenha uma rede, ainda poderá usar o app. Ele também é instalável na tela inicial de um dispositivo e se integra perfeitamente ao sistema operacional como um aplicativo independente.
Aprimoramento progressivo
Agora que já falamos sobre isso, é hora de falar sobre melhoria progressiva. O glossário do MDN Web Docs define o conceito da seguinte maneira:
O aprimoramento progressivo é uma filosofia de design que oferece uma 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 apenas para usuários dos navegadores mais modernos que podem executar todo o código necessário.
A detecção de recursos geralmente é 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 em criar os melhores sites possíveis, fazendo com que eles funcionem em vários user agents desconhecidos. A degradação gradual está relacionada, mas não é a mesma coisa e geralmente é vista como indo na direção oposta ao aprimoramento progressivo. Na realidade, as duas abordagens são válidas e podem se complementar.
Colaboradores da MDN
Começar cada cartão do zero pode ser muito complicado.
Então, por que não ter um recurso que permita aos usuários importar uma imagem e começar a partir dela?
Com uma abordagem tradicional, você teria usado um elemento
<input type=file>
para fazer isso.
Primeiro, crie o elemento, defina o type como 'file' e adicione tipos MIME
à propriedade accept. Depois, "clique" nele de forma programática e aguarde
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 importação, provavelmente também há um de exportação para que os usuários possam salvar os cartões de felicitações localmente.
A maneira tradicional de salvar arquivos é criar um link âncora
com um atributo download
e com um URL de blob como href.
Você também "clicaria" nele de forma programática para acionar o download e, para evitar vazamentos de memória, não se esqueceria de revogar o URL do 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();
};
Mas espere um pouco. Mentalmente, você não "baixou" um cartão de felicitações, mas o "salvou". Em vez de mostrar uma caixa de diálogo "Salvar" para você escolher onde colocar o arquivo, o navegador baixou diretamente o cartão sem interação do usuário e o colocou direto na pasta "Downloads". Isso não é nada 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 no arquivo original que você abriu inicialmente? A API File System Access permite abrir, criar, modificar e salvar arquivos e diretórios.
Então, como faço para detectar recursos de uma API?
A API File System Access expõe um novo método window.chooseFileSystemEntries().
Por isso, preciso carregar condicionalmente diferentes módulos de importação e exportação, dependendo da disponibilidade desse método.
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 entrar nos detalhes da API File System Access, quero destacar rapidamente o padrão de melhoria progressiva aqui. Em navegadores que não são compatíveis com a API File System Access, carrego os scripts legados.
No entanto, no Chrome, um navegador que oferece suporte à API, apenas os novos scripts são carregados.
Isso é possível graças ao
import() dinâmico, que todos os navegadores modernos
aceitam.
Como eu disse antes, a grama está bem verde hoje em dia.
API File System Access
Agora que abordei 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 em que digo que quero arquivos de imagem.
As extensões de arquivo e os tipos MIME são aceitos.
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);
}
};
Exportar uma imagem é quase a mesma coisa, mas desta vez
preciso transmitir um parâmetro de tipo de 'save-file' para o método chooseFileSystemEntries().
Assim, 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 maneira semelhante a antes, mas desta vez limitado apenas a imagens PNG.
De novo, recebo um identificador de arquivo, mas, em vez de receber o arquivo,
crio um fluxo gravável chamando createWritable().
Em seguida, escrevo o blob, que é a imagem do meu cartão de felicitações, no arquivo.
Por fim, fecho o stream gravável.
Tudo pode falhar: o disco pode ficar sem espaço, pode haver um erro de gravação ou leitura ou talvez o usuário simplesmente cancele a caixa de diálogo de arquivo.
Por isso, sempre encapsulo 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. Posso fazer minhas edições e finalmente salvá-las com uma caixa de diálogo de salvamento real, onde posso escolher o nome e o local de armazenamento do arquivo. Agora o arquivo está pronto para ser preservado para sempre.
APIs Web Share e Web Share Target

Além de armazenar para sempre, talvez eu queira compartilhar meu cartão de felicitações. Isso é algo que a API Web Share e a API Web Share Target permitem fazer. Os sistemas operacionais para dispositivos móveis e, mais recentemente, para computadores ganharam mecanismos de compartilhamento integrados.
Por exemplo, a folha de compartilhamento do Safari para computador no macOS é acionada quando um usuário clica em Compartilhar artigo no meu blog. Você pode compartilhar um link do artigo com um amigo usando o app Mensagens do macOS.
Para fazer isso, 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 oferece suporte a isso.
A boa notícia é que o Web Share Level 2 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 funcionar com o aplicativo de cartão de felicitações do Fugu.
Primeiro, preciso preparar um objeto data com uma matriz files que consiste em um blob e, em seguida, 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 ser compartilhado tecnicamente pelo navegador.
Se navigator.canShare() me disser que os dados podem ser compartilhados, vou ligar para navigator.share() como antes.
Como tudo pode falhar, estou usando um bloco try...catch novamente.
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, vou continuar e
carregar share.mjs usando import() dinâmico.
Em navegadores como o Safari para dispositivos móveis, que atendem apenas a uma das duas condições, não carrego a funcionalidade.
const loadShare = () => {
if ('share' in navigator && 'canShare' in navigator) {
import('./share.mjs');
}
};
No Fugu Greetings, 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. Por exemplo, posso escolher o Gmail, e o widget de criação de e-mail aparece com a imagem anexada.
API Contact Picker
Agora, quero falar sobre contatos, ou seja, a agenda de endereços de um dispositivo ou o app gerenciador de contatos. Ao escrever um cartão de felicitações, nem sempre é fácil escrever o nome de alguém corretamente. Por exemplo, tenho um amigo chamado Sergey que prefere que o nome dele seja escrito em letras cirílicas. Estou usando um teclado QWERTZ alemão e não sei como digitar o nome dele. Esse é um problema que a API Contact Picker pode resolver. Como tenho meu amigo armazenado no app de contatos do smartphone, usando a API Contacts Picker, posso acessar meus contatos na Web.
Primeiro, preciso especificar a lista de propriedades que quero acessar.
Nesse caso, quero apenas 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 poder selecionar mais de uma entrada.
Por fim, posso chamar navigator.contacts.select(), que retorna as propriedades ideais 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);
}
};
E agora você provavelmente já aprendeu o padrão: só carrego o arquivo quando a API é realmente compatível.
if ('contacts' in navigator) {
import('./contacts.mjs');
}
No Fugu Greeting, quando toco no botão Contatos e seleciono meus dois melhores amigos, Сергей Михайлович Брин e 劳伦斯·爱德华·"拉里"·佩奇, é possível ver como o seletor de contatos é limitado a mostrar apenas os nomes, mas não os endereços de e-mail ou outras informações, como os números de telefone. Os nomes são desenhados no meu cartão de felicitações.
A API Async Clipboard
Em seguida, vamos falar sobre copiar e colar. Uma das nossas operações favoritas como desenvolvedores de software é copiar e colar. Como autora de cartões de felicitações, às vezes, eu também quero fazer isso. Posso querer colar uma imagem em um cartão de felicitações em que estou trabalhando ou copiar o cartão para continuar editando em outro lugar. A API Async Clipboard é compatível com texto e imagens. Vou explicar como adicionei suporte para copiar e colar ao app Fugu Greetings.
Para copiar algo na área de transferência do sistema, preciso gravar nela.
O método navigator.clipboard.write() usa uma matriz de itens da área de transferência como um
parâmetro.
Cada item da área de transferência é essencialmente um objeto com um blob como valor e o tipo do blob como 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 fazer um loop nos itens da área de transferência que obtenho chamando
navigator.clipboard.read().
Isso acontece porque vários itens podem estar na área de transferência em
representações diferentes.
Cada item da área de transferência tem um campo types que me informa os tipos MIME dos recursos disponíveis.
Chamo o método getType() do item da área de transferência, transmitindo o
tipo MIME que obtive 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 agora. Só faço isso em navegadores compatíveis.
if ('clipboard' in navigator && 'write' in navigator.clipboard) {
import('./clipboard.mjs');
}
Como isso funciona na prática? Tenho uma imagem aberta no app Pré-visualização do macOS e a copio para a área de transferência. Quando clico em Colar, o app Fugu Greetings me pergunta se quero permitir que ele acesse texto e 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. Deixe-me copiar um cartão de felicitações para a área de transferência. Quando abro o Preview e clico em Arquivo e depois em Novo da área de transferência, o cartão é colado em uma nova imagem sem título.
API Badging
Outra API útil é a API Badging.
Como um PWA instalável, o Fugu Greetings tem um ícone de app
que os usuários podem colocar no dock de apps ou na tela inicial.
Uma maneira divertida de demonstrar a API é usá-la no Fugu Greetings,
como um contador de traços 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 ícone atualizado.
Sempre que a tela é limpa, o contador é redefinido e o selo é removido.
let strokes = 0;
canvas.addEventListener('pointerdown', () => {
navigator.setAppBadge(++strokes);
});
clearButton.addEventListener('click', () => {
strokes = 0;
navigator.setAppBadge(strokes);
});
Esse recurso é um aprimoramento progressivo, então a lógica de carregamento é a mesma de sempre.
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 indicadores no ícone agora está em sete.
A API Periodic Background Sync
Quer começar cada dia com algo novo? Um recurso interessante do app Fugu Greetings é que ele pode inspirar você todas as manhãs com uma nova imagem de plano de fundo para começar seu cartão de felicitações. O app usa a API Periodic Background Sync para fazer isso.
A primeira etapa é registrar um evento de sincronização periódica no registro
do service worker. Ele fica atento a uma tag de sincronização chamada 'image-of-the-day'
e tem um intervalo mínimo de um dia,
para que o usuário receba 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 com a função getImageOfTheDay(),
e o resultado será propagado para todos os clientes, para que eles possam atualizar as telas e
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, esse é um aprimoramento progressivo. Portanto, o código só é carregado quando
a API é compatível com o navegador.
Isso se aplica ao código do cliente e do service worker.
Em navegadores sem suporte, nenhum deles é carregado.
Observe que, no service worker, em vez de um import() dinâmico (que ainda não é compatível em um contexto de service worker), uso o importScripts() clássico.
// 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, ao pressionar o botão Papel de parede, a imagem do cartão de saudação do dia é atualizada todos os dias com a API Periodic Background Sync.
API Notification Triggers
Às vezes, mesmo com muita inspiração, você precisa de uma ajuda para terminar um cartão de saudação. Esse recurso é ativado pela API Notification Triggers. Como usuário, posso inserir um horário em que quero receber um lembrete para terminar meu cartão de felicitações. Quando chegar a hora, vou receber uma notificação de que meu cartão está esperando.
Depois de solicitar o horário de destino,
o aplicativo agenda a notificação com um showTrigger.
Pode ser um TimestampTrigger com a data de destino selecionada anteriormente.
A notificação de lembrete será acionada localmente. Não é necessário ter rede ou 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 tudo o que mostrei até agora, esse é um aprimoramento progressivo. Portanto, o código só é carregado condicionalmente.
if ('Notification' in window && 'showTrigger' in Notification.prototype) {
import('./notification_triggers.mjs');
}
Quando marco a caixa de seleção Lembrete em Fugu Greetings, uma solicitação pergunta quando quero receber um lembrete para terminar meu cartão de felicitações.
Quando uma notificação programada é acionada no Fugu Greetings, ela é mostrada como qualquer outra notificação, mas, como escrevi antes, não exigiu uma conexão de rede.
API Wake Lock
Também quero incluir a API Wake Lock. Às vezes, basta olhar para a tela por tempo suficiente até que a inspiração chegue. O pior que pode acontecer é a tela desligar. A API Wake Lock pode evitar isso.
A primeira etapa é conseguir um bloqueio de despertar com o navigator.wakelock.request method().
Eu transmito a string 'screen' para conseguir um wake lock de tela.
Em seguida, adiciono um listener de eventos para ser informado quando o bloqueio de despertar é liberado.
Isso pode acontecer, por exemplo, quando a visibilidade da guia muda.
Se isso acontecer, quando a guia ficar visível novamente, posso recuperar o bloqueio de despertar.
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. Portanto, 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');
}
No Fugu Greetings, há uma caixa de seleção Insomnia que, quando marcada, mantém a tela ativa.
A API Idle Detection
Às vezes, mesmo que você fique olhando para a tela por horas, não adianta, e você não consegue ter a menor ideia do que fazer com o cartão. 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 limpará a tela. Essa API é protegida pela permissão de notificações, já que muitos casos de uso de detecção de inatividade em produção estão relacionados a notificações, por exemplo, para enviar uma notificação apenas a um dispositivo que o usuário está usando ativamente.
Depois de verificar se a permissão de notificações foi concedida, instancio o detector de inatividade. Registro um listener de eventos que detecta mudanças de inatividade, incluindo o usuário e o estado da tela. O usuário pode estar ativo ou inativo, e a tela pode estar desbloqueada ou bloqueada. Se o usuário ficar inativo, a tela será limpa. Dou ao detector de inatividade 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,
});
E, como sempre, só carrego esse código quando o navegador é compatível com ele.
if ('IdleDetector' in window) {
import('./idle_detection.mjs');
}
No app Fugu Greetings, a tela é limpa quando a caixa de seleção Ephemeral está marcada e o usuário fica inativo por muito tempo.
Encerramento
Ufa, que viagem. Tantas APIs em apenas um app de exemplo. E lembre-se: nunca faço o usuário pagar o custo de download de um recurso que o navegador dele não oferece suporte. Ao usar o aprimoramento progressivo, garanto que apenas o código relevante seja carregado. Como no HTTP/2 as solicitações são baratas, esse padrão funciona bem para muitos aplicativos, mas talvez seja melhor considerar um bundler para apps muito grandes.
O app pode parecer um pouco diferente em cada navegador, já que nem todas as plataformas são compatíveis com todos os recursos, mas a funcionalidade principal está sempre presente, sendo aprimorada progressivamente de acordo com os recursos específicos do navegador. Esses recursos podem mudar até mesmo no mesmo navegador, dependendo se o app está sendo executado como um app instalado ou em uma guia do navegador.
Você pode bifurcar o Fugu no GitHub.
A equipe do Chromium está trabalhando muito para melhorar as APIs Fugu avançadas. Ao aplicar o aprimoramento progressivo ao criar meu app, garanto que todos tenham uma experiência básica boa e sólida, mas que as pessoas que usam navegadores compatíveis com mais APIs da plataforma Web tenham uma experiência ainda melhor. Não vejo a hora de saber o que você vai fazer com o aprimoramento progressivo nos seus apps.
Agradecimentos
Sou grato a Christian Liebel e Hemanth HM, que contribuíram para o Fugu Greetings.
Este documento foi revisado por Joe Medley e
Kayce Basques.
Jake Archibald me ajudou a descobrir a situação
com import() dinâmico em um contexto de service worker.