Aprimorar progressivamente seu Progressive Web App

Como criar para navegadores modernos e melhorar progressivamente como se fosse 2003

Em março de 2003, Nick Finck e Steve Champeon surpreenderam o mundo do design da Web com o conceito de aprimoramento progressivo, uma estratégia de design da Web que enfatiza o carregamento do conteúdo principal da página da Web primeiro e que, em seguida, adiciona progressivamente camadas de apresentação e recursos mais sutis e tecnicamente rigorosos sobre o conteúdo. Em 2003, o aprimoramento progressivo consistia em usar recursos CSS modernos, JavaScript discreto e até mesmo elementos gráficos vetoriais escaláveis. O aprimoramento progressivo em 2020 e além é sobre o uso de recursos modernos do navegador.

Design da Web inclusivo para o futuro com aprimoramento progressivo. Slide de título da apresentação original de Finck e Champeon.
Slide: Web design inclusivo para o futuro com aprimoramento progressivo. (Fonte)

JavaScript moderno

Falando em JavaScript, o suporte do navegador para os recursos principais mais recentes do JavaScript ES 2015 é ótimo. O novo padrão inclui promessas, módulos, classes, literais de modelo, funções de seta, let e const, parâmetros padrão, geradores, a atribuição de desestruturação, rest e spread, Map/Set, WeakMap/WeakSet e muito mais. Todos são aceitos.

Tabela de compatibilidade do CanIUse para recursos ES6 mostrando suporte em todos os principais navegadores.
A tabela de suporte do navegador ECMAScript 2015 (ES6). (Fonte)

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 e baseado em promessas seja escrito de um jeito mais limpo, evitando a necessidade de configurar explicitamente as cadeias de promessas.

Tabela de compatibilidade do CanIUse para funções assíncronas mostrando suporte em todos os principais navegadores.
A tabela de suporte do navegador para funções assíncronas. (Fonte)

E até mesmo as adições mais recentes da linguagem ES 2020, como cadeiação opcional e coalescência de nulos, receberam suporte muito rapidamente. Confira um exemplo de código abaixo. Quando se trata de recursos principais do JavaScript, não há muito o que melhorar hoje em dia.

const adventurer = {
  name: 'Alice',
  cat: {
    name: 'Dinah',
  },
};
console.log(adventurer.dog?.name);
// Expected output: undefined
console.log(0 ?? 42);
// Expected output: 0
A imagem de plano de fundo de grama verde do Windows XP.
Os recursos principais do JavaScript são ótimos. (Captura de tela do produto da Microsoft, usada com permissão.)

O app de exemplo: Fugu Greetings

Para este artigo, trabalho com uma PWA simples chamada Fugu Greetings (GitHub). O nome do app é uma homenagem ao Project Fugu 🐡, um esforço para dar à Web todos os poderes dos aplicativos Android/iOS/desktop. 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 quem você ama. Ele exemplifica os principais conceitos de PWA. Ele é confiável e totalmente compatível com o modo off-line. Assim, mesmo que você não tenha uma rede, ainda poderá usá-lo. Ele também pode ser instalado na tela inicial de um dispositivo e se integra perfeitamente ao sistema operacional como um aplicativo independente.

PWA da Fugu Greetings com um desenho que se parece com o logotipo da comunidade da PWA.
O app de exemplo Fugu Greetings.

Aprimoramento progressivo

Agora que isso foi resolvido, vamos falar sobre o aprimoramento progressivo. O glossário do MDN Web Docs define o conceito da seguinte maneira:

O aprimoramento progressivo é uma filosofia de design que fornece uma base de conteúdo e funcionalidade essenciais para o maior número possível de usuários, oferecendo 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 processar funcionalidades mais modernas, enquanto os polyfills são usados com frequência 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 em vários agentes de usuário desconhecidos. A degradação suave está relacionada, mas não é a mesma coisa e muitas vezes é vista como uma direção oposta ao aprimoramento progressivo. Na realidade, as duas abordagens são válidas e podem se complementar.

Colaboradores do MDN

Começar cada cartão de presente do zero pode ser muito trabalhoso. Por que não ter um recurso que permita aos usuários importar uma imagem e começar a partir daí? Com uma abordagem tradicional, você teria usado um elemento <input type=file> para fazer isso. Primeiro, você cria o elemento, define o type como 'file' e adiciona tipos MIME à propriedade accept. Em seguida, "clica" nele de forma programática e detecta 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 há um recurso de exportação para que os usuários possam salvar as cartões de felicitações localmente. A maneira tradicional de salvar arquivos é criar um link de âncora com um atributo download e com um URL de blob como href. Você também precisa "clicar" nele de forma programática para acionar o download. Para evitar vazamentos de memória, não se esqueça 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 minuto. Mentalmente, você não "fez o download" de um cartão, você "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 sem a 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 no arquivo original que você abriu inicialmente? Acontece que sim. 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 um recurso de 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 se esse método está disponível. Mostrei como fazer isso abaixo.

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 em detalhes sobre a API File System Access, vamos destacar rapidamente o padrão de melhoria progressiva. Em navegadores que não oferecem suporte à API File System Access, carrego os scripts legados. Confira abaixo as guias de rede do Firefox e do Safari.

Safari Web Inspector mostrando os arquivos legados sendo carregados.
Ficha de rede do Safari Web Inspector.
Ferramentas para desenvolvedores do Firefox mostrando os arquivos legados sendo carregados.
Guia de rede das Ferramentas para Desenvolvedores do Firefox.

No entanto, no Chrome, um navegador compatível com a API, apenas os novos scripts são carregados. Isso é possível graças ao import() dinâmico, que todos os navegadores modernos oferecem. Como eu disse antes, a grama está bem verde ultimamente.

Chrome DevTools mostrando os arquivos modernos sendo carregados.
Guia de rede do Chrome DevTools.

A API File System Access

Agora que já 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 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, em que posso acessar 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 dessa vez preciso transmitir um parâmetro de tipo 'save-file' para o método chooseFileSystemEntries(). Com isso, recebo uma caixa de diálogo de salvamento de arquivo. Com o arquivo aberto, isso não era necessário, já que 'open-file' é o padrão. Defino o parâmetro accepts de maneira semelhante à anterior, mas desta vez limitado apenas a imagens PNG. Recebo novamente um identificador de arquivo, mas, em vez de receber o arquivo, criei um stream gravável chamando createWritable(). Em seguida, eu escrevo o blob, que é a imagem do cartão, no arquivo. Por fim, fecho o stream gravável.

Tudo pode falhar: o disco pode estar 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 que sempre envolvo 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, em que posso escolher o nome e o local de armazenamento do arquivo. Agora o arquivo está pronto para ser preservado para sempre.

App Fugu Greetings com uma caixa de diálogo de abertura de arquivo.
A caixa de diálogo de abertura de arquivo.
App Fugu Greetings com uma imagem importada.
A imagem importada.
App Fugu Greetings com a imagem modificada.
Salvar a imagem modificada em um novo arquivo.

As 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 de segmentação compartilhada da Web me permitem fazer. Os sistemas operacionais para dispositivos móveis e, mais recentemente, para computadores ganharam mecanismos de compartilhamento integrados. Por exemplo, abaixo está a página de compartilhamento do Safari para computador no macOS acionada por um artigo no meu blog. Ao clicar no botão Compartilhar artigo, você pode compartilhar um link do artigo com um amigo, por exemplo, pelo app Mensagens do macOS.

Página de compartilhamento do Safari para computador no macOS acionada pelo botão &quot;Compartilhar&quot; de um artigo
API Share da Web no Safari para computador no macOS.

O código para fazer isso é bem simples. 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 nível 2 de 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 funcionar com o aplicativo de cartão de felicitações 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 informa se o objeto data que estou tentando compartilhar pode ser compartilhado tecnicamente pelo navegador. Se navigator.canShare() me informar que os dados podem ser compartilhados, estarei pronto para chamar navigator.share() como antes. Como tudo pode falhar, estou usando 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, só então vou carregar share.mjs por import() dinâmico. Em navegadores como o Safari para dispositivos móveis que atendem apenas a uma das duas condições, a funcionalidade não é carregada.

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. Posso, por exemplo, escolher o Gmail, e o widget de criação de e-mail aparece com a imagem anexada.

Página de compartilhamento no nível do SO mostrando vários apps para compartilhar a imagem.
Escolha um app para compartilhar o arquivo.
Widget de composição de e-mail do Gmail com a imagem anexada.
O arquivo é anexado a um novo e-mail no editor do Gmail.

A API Contact Picker

Em seguida, quero falar sobre contatos, ou seja, a agenda de um dispositivo ou o app de gerenciamento de contatos. Quando você escreve uma mensagem em uma cartão, nem sempre é fácil escrever o nome de alguém corretamente. Por exemplo, tenho um amigo chamado Sergey que prefere que seu nome seja escrito em letras cirílicas. Estou usando um teclado QWERTZ alemão e não tenho ideia de como digitar o nome. Esse é um problema que a API Contact Picker pode resolver. Como tenho meu amigo armazenado no app Contatos do meu smartphone, usando a API Contacts Picker, posso acessar meus contatos na 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, configurei um objeto options e defini multiple como true para 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);
  }
};

E agora você provavelmente já aprendeu o padrão: só carrego o arquivo quando a API tem suporte.

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

No Fugu Greeting, quando eu toco no botão Contatos e seleciono meus dois melhores amigos, Сергей Михайлович Брин e 劳伦斯·爱德华·"拉里"·佩奇, você pode ver como o seletor de contatos é limitado para mostrar apenas os nomes, mas não os endereços de e-mail ou outras informações, como números de telefone. Os nomes deles são desenhados no meu cartão de felicitações.

Seletor de contatos mostrando os nomes de dois contatos na agenda de endereços.
Seleção de dois nomes com o seletor de contatos na agenda.
Os nomes dos dois contatos escolhidos anteriormente desenhados no cartão de felicitações.
Os dois nomes são desenhados no cartão de felicitações.

API Asynchronous Clipboard

A próxima etapa é copiar e colar. Uma das nossas operações favoritas como desenvolvedores de software é copiar e colar. Como autor de cartões comemorativos, às vezes, eu quero fazer o mesmo. Posso colar uma imagem em um cartão em que estou trabalhando ou copiar o cartão para continuar editando em outro lugar. A API Async Clipboard oferece suporte a texto e imagens. Vou mostrar como adicionei o suporte a 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 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 sobre os itens da área de transferência que recebo chamando navigator.clipboard.read(). Isso ocorre porque vários itens da área de transferência podem estar nela em representações diferentes. Cada item da área de transferência tem um campo types que 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 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 não é preciso dizer mais nada. 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 Visualização do macOS e a copiei 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.

App Fugu Greetings mostrando a solicitação de permissão da área de transferência.
A solicitação de permissão da á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 para a área de transferência. Quando abro o app Visualização e clico em Arquivo e em Novo da área de transferência, o cartão de felicitações é colado em uma nova imagem sem título.

O app Visualização do macOS com uma imagem sem título que acabou de ser colada.
Uma imagem colada no app Prévia do macOS.

API Badging

Outra API útil é a API Badging. Como um PWA instalável, o Fugu Greetings tem um ícone que os usuários podem colocar na tela inicial ou no dock de apps. Uma maneira divertida e fácil de demonstrar a API é (ab)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 selos no ícone agora está em sete.

Os números de um a sete desenhados no cartão de felicitações, cada um com apenas um traço de caneta.
Desenhando os números de 1 a 7, usando sete traços de caneta.
Ícone de selo no app Fugu Greetings mostrando o número 7.
Contador de traços de caneta na forma de selo do ícone do app.

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

Mais uma vez, esse é um aprimoramento progressivo, então o código só é carregado quando a API é compatível com o navegador. Isso se aplica ao código do cliente e ao código do service worker. Em navegadores sem suporte, nenhum deles é carregado. Observe como, no service worker, em vez de um import() dinâmico (que ainda não tem suporte no contexto de um 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 Wallpaper, a imagem do cartão do dia é mostrada. Ela é atualizada todos os dias pela API Periodic Background Sync.

App Fugu Greetings com uma nova imagem de cartão do dia.
Ao tocar no botão Wallpaper, a imagem do dia é exibida.

API Notification Triggers

Às vezes, mesmo com muita inspiração, você precisa de um empurrãozinho para terminar um cartão de cumprimento. Esse é um recurso ativado pela API Notification Triggers. Como usuário, posso inserir um horário em que quero receber uma mensagem para terminar meu cartão de felicitações. Quando chegar a hora, vou receber uma notificação de que meu cartão está aguardando.

Depois de solicitar o horário desejado, o aplicativo programa 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, sem necessidade de 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),
  });
}

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 eu marco a caixa de seleção Lembrete no Fugu Greetings, um prompt me pergunta quando eu quero receber um lembrete para terminar meu cartão.

App Fugu Greetings com uma solicitação que pergunta ao usuário quando ele quer receber um lembrete para terminar o cartão.
Programando uma notificação local para receber um lembrete de terminar um cartão de felicitações.

Quando uma notificação programada é acionada no Fugu Greetings, ela é mostrada como qualquer outra notificação, mas, como mencionei antes, ela não exigia uma conexão de rede.

Central de notificações do macOS mostrando uma notificação acionada pelo Fugu Greetings.
A notificação acionada aparece na Central de notificações do macOS.

A API Wake Lock

Também quero incluir a API Wake Lock. Às vezes, basta olhar para a tela até que a inspiração surja. O pior que pode acontecer é a tela desligar. A API Wake Lock pode impedir que isso aconteça.

A primeira etapa é conseguir um bloqueio de ativação com o navigator.wakelock.request method(). Eu transmito a string 'screen' para receber um wake lock de tela. Em seguida, adiciono um listener de eventos para ser informado quando o bloqueio de ativação for liberado. Isso pode acontecer, por exemplo, quando a visibilidade da guia muda. Se isso acontecer, quando a guia ficar visível novamente, posso recuperar a trava de ativação.

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 fazer o carregamento quando o navegador oferecer suporte à 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 caixa de seleção &quot;Insônia&quot;, se marcada, mantém a tela ativada.
A caixa de seleção Insomnia mantém o app ativo.

A API Idle Detection

Às vezes, mesmo que você olhe para a tela por horas, ela é inútil 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 a tela será limpa. No momento, essa API está bloqueada pela permissão de notificações, já que muitos casos de uso de produção da detecção de inatividade estão relacionados a notificações, por exemplo, para enviar apenas uma notificação a um dispositivo que o usuário está usando ativamente.

Depois de verificar se a permissão de notificações foi concedida, instanciei o detector de inatividade. Registrei 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. Eu 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 oferece suporte a 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.

App Fugu Greetings com uma tela limpa depois que o usuário ficou inativo por muito tempo.
Quando a caixa de seleção Ephemeral está marcada e o usuário fica inativo por muito tempo, a tela é limpa.

Encerramento

Ufa, que viagem. Tantas APIs em apenas um app de exemplo. E lembre-se: nunca faço o usuário pagar o custo do download de um recurso que o navegador não oferece suporte. Ao usar o aprimoramento progressivo, garanto que apenas o código relevante seja carregado. E, como as solicitações são baratas com o HTTP/2, esse padrão deve funcionar bem para muitos aplicativos, embora seja recomendável usar um agrupador para apps muito grandes.

Painel &quot;Network&quot; do Chrome DevTools mostrando apenas solicitações de arquivos com código compatível com o navegador atual.
Guia "Rede" do Chrome DevTools mostrando apenas solicitações de arquivos com código compatível com o navegador atual.

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 presente, sendo aprimorada progressivamente de acordo com os recursos do navegador específico. Esses recursos podem mudar até mesmo em um mesmo navegador, dependendo se o app está sendo executado como um app instalado ou em uma guia do navegador.

Fugu Greetings em execução no Chrome para Android, mostrando muitos recursos disponíveis.
Fugu Greetings em execução no Chrome para Android.
Fugu Greetings em execução no Safari para computador, mostrando menos recursos disponíveis.
Fugu Greetings em execução no Safari para computador.
Fugu Greetings em execução no Chrome para computador, mostrando muitos recursos disponíveis.
Fugu Greetings em execução no Chrome para computador.

Se você tiver interesse no app Fugu Greetings, encontre e crie um fork no GitHub.

Repositório Fugu Greetings no GitHub.
App Fugu Greetings no GitHub.

A equipe do Chromium está trabalhando muito para melhorar as APIs avançadas do Project Fugu. Ao aplicar o aprimoramento progressivo no desenvolvimento do meu app, tenho certeza de que todos terão uma experiência de referência boa e sólida, mas as pessoas que usam navegadores com suporte a mais APIs da plataforma da Web terão uma experiência ainda melhor. Estou ansioso para saber o que você vai fazer com o aprimoramento progressivo nos seus apps.

Agradecimentos

Agradeço 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 import() dinâmico em um contexto de worker de serviço.