Aprimorar progressivamente seu Progressive Web App

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.

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

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.

A tabela de suporte CanIUse para recursos ES6 mostrando compatibilidade com todos os principais navegadores.
A tabela de suporte a navegadores ECMAScript 2015 (ES6). (Fonte)

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.

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

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
A icônica imagem de plano de fundo do gramado verde do Windows XP.
O gramado é verde quando se trata dos principais recursos de JavaScript. Captura de tela do produto da Microsoft, usada com permissão.

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.

PWA do Fugu Greetings com um desenho parecido com o logotipo da comunidade do PWA.
O app de exemplo Fugu Greetings.

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.

Safari Web Inspector mostrando os arquivos legados sendo carregados.
Guia 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 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.

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

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.

App Fugu Greetings com uma caixa de diálogo de arquivo aberto.
A caixa de diálogo de abertura do arquivo.
O app Fugu Greetings agora com uma imagem importada.
A imagem importada.
App Fugu Greetings com a imagem modificada.
Salve 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 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.

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

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.

Planilha de compartilhamento no nível do SO mostrando vários apps para compartilhar a imagem.
Escolher um app para compartilhar o arquivo.
Widget &quot;Escrever e-mail&quot; do Gmail com a imagem anexada.
O arquivo é anexado a um novo e-mail no Editor do Gmail.

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.

Seletor de contatos mostrando os nomes de dois contatos no catálogo de endereços.
Seleção de dois nomes com o seletor de contatos no catálogo de endereços.
Os nomes dos dois contatos escolhidos anteriormente desenhados no cartão comemorativo.
Os dois nomes aparecem no 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.

App Fugu Greetings mostrando a solicitação de permissão da área de transferência.
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 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.

O app macOS Preview com uma imagem sem título, recém-colada.
Uma imagem colada no app macOS Preview.

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.

Números de um a sete desenhados no cartão comemorativo, cada um com apenas um traço de caneta.
Desenhar 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.
O contador de traços de caneta na forma do selo de ícone do app.

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.

App Fugu Greetings com uma nova imagem de cartão comemorativo do dia.
Ao pressionar o botão Plano de fundo, a imagem do dia aparece.

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.

O app Fugu Greetings com uma solicitação perguntando ao usuário quando ele quer ser lembrado de terminar o cartão comemorativo.
Programar uma notificação local para ser lembrado de terminar um 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.

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

A caixa de seleção de insônia, se marcada, mantém a tela ativada.
A caixa de seleção Insomnia mantém o app ativado.

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.

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

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.

Painel &quot;Network&quot; do Chrome DevTools mostrando somente solicitações de arquivos com código compatível com o navegador atual.
Guia "Rede" do Chrome DevTools mostrando somente 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 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.

Fugu Greetings em execução no Android Chrome, mostrando muitos recursos disponíveis.
Fugu Greetings em execução no Android Chrome.
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 Saudações em execução no Chrome para computador.

Se você tiver interesse pelo app Fugu Greetings, acesse e bifurque o app no GitHub (link em inglês).

Fugu Greetings no GitHub.
App Fugu Greetings no GitHub.

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.