Aprimorar progressivamente seu Progressive Web App

Desenvolvendo para navegadores modernos e melhorando progressivamente como em 2003

Em março de 2003, Nick Finck e Steve Champeon surpreendeu o mundo do web design com o conceito de aprimoramento progressivo, uma estratégia de web design que enfatize primeiro o carregamento do conteúdo principal da página da web, Isso adiciona progressivamente mais nuances e tecnicamente rigorosas de apresentação e recursos sobre o conteúdo. Enquanto em 2003, o aprimoramento progressivo consistia em usar, na época, o uso de recursos recursos CSS, JavaScript discreto e até mesmo gráficos vetoriais escaláveis. O aprimoramento progressivo de 2020 em diante envolve o uso de recursos modernos do navegador.

Web design 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, a situação de suporte do navegador para o JavaScript com núcleo mais recente do 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, repouso e propagação, Map/Set WeakMap/WeakSet e muitos outros. Todos são compatíveis.

A tabela de suporte CanIUse para recursos ES6 mostrando suporte para todos os principais navegadores.
A tabela de suporte para navegadores ECMAScript 2015 (ES6). (fonte).

Funções assíncronas, um recurso do ES 2017 e um dos meus favoritos, pode ser usado em todos os principais navegadores. As palavras-chave async e await permitem um comportamento assíncrono e baseado em promessas seja escrita em um estilo mais limpo, evitando a necessidade de configurar explicitamente as cadeias de promessas.

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

E até mesmo adições de idiomas recentes do ES 2020, como encadeamento opcional e uma fusão nula chegaram ao suporte rapidamente. Veja um exemplo de código abaixo. Quando se trata dos principais recursos do JavaScript, a grama não poderia ser muito mais ecológica 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 do Windows XP.
Os principais recursos do JavaScript são muito claros. (captura de tela do produto da Microsoft, usada com permissão.

O app de exemplo: Fugu Greetings

Para este artigo, trabalho com um PWA simples, chamado Saudações do fugu (GitHub). O nome do app é uma demonstração do Project Fugu 🐡, uma iniciativa para dar todo o trabalho na Web a capacidade dos aplicativos para Android/iOS/computador. Você pode ler mais sobre o projeto na página de destino.

O Fugu Greetings é um app de desenho que permite criar cartões comemorativos virtuais e enviar para seus entes queridos. Ele exemplifica Principais conceitos do PWA. Está confiável e totalmente off-line ativado, portanto, mesmo tiver uma rede, ainda poderá usá-la. Também é instalável. à tela inicial de um dispositivo e integra-se perfeitamente ao sistema operacional como um aplicativo independente.

PWA Fugu Greetings com um desenho parecido com o logotipo da comunidade PWA.
App de exemplo Fugu Greetings (links em inglês).

Aprimoramento progressivo

Com isso resolvido, é hora de 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 linha de base conteúdo e funcionalidade essenciais para o maior número possível de usuários, enquanto oferecendo a melhor experiência possível somente para os usuários dos dispositivos que podem executar todos os códigos necessários.

Detecção de recursos geralmente é usado para determinar se os navegadores aceitam funcionalidades mais modernas, e os polyfills são frequentemente usadas para adicionar recursos ausentes com JavaScript.

[…]

O aprimoramento progressivo é uma técnica útil que permite que os desenvolvedores Web se concentrem desenvolver os melhores sites possíveis e, ao mesmo tempo, fazer com que eles funcionem em vários user agents desconhecidos. Degradação suave está relacionada, mas não é a mesma coisa e costuma ser vista como indo na direção oposta ao aprimoramento progressivo. Na realidade, as duas abordagens são válidas e muitas vezes podem se complementar.

Colaboradores do MDN

Começar cada cartão comemorativo 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 daí? Com uma abordagem tradicional, você teria usado <input type=file> para que isso aconteça. Primeiro, você criaria o elemento, definiria a type dele como 'file' e adicionaria tipos MIME à propriedade accept. e depois "clicar" de forma programática e detectar alterações. 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 é necessário ter um recurso de exportação. para que os usuários possam salvar os cartões localmente. A maneira tradicional de salvar arquivos é criar um link âncora com um download e com um URL de blob como href. Você também clicaria programaticamente que acione o download e, 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 pouco. Mentalmente, você não fez o download um cartão comemorativo, você tem "salvo" reimplantá-lo. Em vez de exibir a opção "Salvar" que permite escolher onde colocar o arquivo, o navegador fez o download do cartão comemorativo diretamente, sem interação do usuário e o coloca diretamente na pasta "Downloads". Isso não é muito bom.

E se houvesse uma maneira melhor? E se você pudesse abrir um arquivo local, editá-lo e salvar as modificações, para um novo arquivo ou de volta para o arquivo original que você abriu inicialmente? Acontece que existe. A API File System Access permite abrir e criar arquivos e diretórios, bem como modificá-los e salvá-los .

Como posso detectar atributos em uma API? A API File System Access expõe um novo método window.chooseFileSystemEntries(). Consequentemente, preciso carregar condicionalmente módulos de importação e exportação diferentes, dependendo da disponibilidade desse método. 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 nos aprofundarmos nos detalhes da API File System Access, vou destacar rapidamente o padrão de aprimoramento progressivo. Em navegadores que atualmente não oferecem suporte à API File System Access, os scripts legados são carregados. Abaixo, é possível conferir 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 "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 de forma elegante graças à import() dinâmico, que todos os navegadores modernos do servidor. Como eu disse antes, a grama está bem 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 abordei isso, é hora de analisar a implementação real com base na API File System Access. Para importar uma imagem, chamo window.chooseFileSystemEntries() e transmitir uma propriedade accepts onde eu digo 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, do qual 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 o mesmo, mas desta vez Preciso transmitir um parâmetro de tipo de 'save-file' ao método chooseFileSystemEntries(). Depois, vejo uma caixa de diálogo para salvar arquivos. Com o arquivo aberto, isso não era necessário porque 'open-file' é o padrão. Configurei o parâmetro accepts de forma semelhante a antes, mas desta vez se limitei apenas a imagens PNG. Mais uma vez, obtenho um identificador de arquivo, mas, em vez de receber o arquivo, Desta vez, crie um stream gravável chamando createWritable(). Em seguida, crio o blob, que é a imagem do meu cartão comemorativo, 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 simplesmente o usuário cancele a caixa de diálogo do arquivo. É por isso que sempre unifico 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);
  }
};

Usar 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, por fim, salvá-las em 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 a eternidade.

App Fugu Greetings com uma caixa de diálogo aberta em um arquivo.
Caixa de diálogo de abertura do arquivo.
.
App Fugu Greetings agora com uma imagem importada.
A imagem importada.
.
App Fugu Greetings com a imagem modificada.
Salvando a imagem modificada em um novo arquivo.

APIs Web Share e Web Share Target

Além de guardar para sempre, talvez eu queira compartilhar meu cartão comemorativo. Isso é algo que a API Web Share e com a API Web Share Target. Para dispositivos móveis e, mais recentemente, os sistemas operacionais de desktop ganharam compartilhamento integrado de segurança. Por exemplo, abaixo está a página de compartilhamento do Safari para computador no macOS acionada de um artigo sobre meu blog. Ao clicar no botão Compartilhar artigo, você pode compartilhar um link para o 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 Web Share no Safari do computador no macOS.

O código para que isso aconteça é bem simples. Eu chamo navigator.share() e transmita a ele title, text e url opcionais em um objeto. 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 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ões comemorativos do Fugu. Primeiro, preciso preparar um objeto data com uma matriz files composta por um blob e, em seguida, um title e um text. Em seguida, como prática recomendada, eu uso o novo método navigator.canShare(), que não o que o nome sugere: Ele me diz se o objeto data que estou tentando compartilhar pode tecnicamente ser compartilhado pelo navegador. Se o navigator.canShare() disser que os dados podem ser compartilhados, estou pronto para chame navigator.share() como antes. Como tudo pode falhar, estou novamente usando 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);
  }
};

Assim como antes, eu uso o aprimoramento progressivo. Se 'share' e 'canShare' existirem no objeto navigator, somente então carregar share.mjs usando import() dinâmico. Em navegadores como o Safari para dispositivos móveis, que só atendem a uma das duas condições, eu 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 planilha de compartilhamento integrada será aberta. Posso, por exemplo, escolher o Gmail, e o widget de composição de e-mails será exibido com o em anexo.

Página de compartilhamento do SO mostrando vários apps para compartilhar a imagem.
Escolher um app para compartilhar o arquivo.
.
Widget de escrever de e-mails do Gmail com a imagem anexada.
O arquivo é anexado a um novo e-mail no Editor do Gmail.

API Contact Picker

Agora, eu quero falar sobre os contatos, ou seja, o catálogo de endereços de um dispositivo ou gerenciador de contatos. Nem sempre é fácil escrever corretamente um cartão comemorativo o nome de alguém. Por exemplo, tenho um amigo Sergey que prefere que seu nome seja escrito em letras cirílicas. estou usando um teclado QWERTZ alemão e não sabe como digitar seu nome. Esse problema pode ser resolvido pela API Contact Picker. Como meu amigo está armazenado no app Contatos do smartphone, por meio da API de seletor de contatos, posso acessar meus contatos na web.

Primeiro, preciso especificar a lista de propriedades que quero acessar. Neste caso, só quero os nomes, Mas para outros casos de uso, posso ter interesse em números de telefone, e-mails, avatar ícones ou endereços físicos. Em seguida, vou configurar um objeto options e definir multiple como true, para poder selecionar mais 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);
  }
};

Agora você provavelmente já aprendeu o padrão: Eu só carrego o arquivo quando há suporte para a API.

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

No Fugu Greeting, quando toco no botão Contacts e seleciono meus dois melhores amigos, Бергей гитайлови comunicações Брин e 劳伦斯·爱德华·"拉里"·佩奇, (em inglês) é possível conferir como o seletor de contatos exibe apenas os nomes, mas não o endereço de e-mail ou outras informações, como números de telefone. Os nomes deles são atraídos no meu cartão comemorativo.

Seletor de contatos mostrando os nomes de dois contatos no catálogo de endereços.
Selecionar dois nomes com o seletor de contatos do catálogo de endereços.
.
Os nomes dos dois contatos escolhidos anteriormente desenhados no cartão comemorativo.
Os dois nomes são desenhados no cartão comemorativo.

A API Async Clipboard

O próximo passo é 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. vou colar uma imagem em um cartão comemorativo em que estou trabalhando, ou copiar meu cartão comemorativo para que eu possa continuar editando-o em outro lugar. A API Async Clipboard, oferece suporte a texto e imagens. Vamos conferir como adicionei o suporte ao recurso copiar e colar no Fugu Cumprimento.

Para copiar algo para a área de transferência do sistema, preciso gravar. O método navigator.clipboard.write() usa uma matriz de itens da área de transferência como . 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 fazer um loop aos itens da área de transferência que obtive chamando navigator.clipboard.read(): A razão para isso é que vários itens da área de transferência podem estar na área de transferência em representações de vetor diferentes. Cada item da área de transferência tem um campo types que informa os tipos MIME dos do Google Cloud. Eu chamo o método getType() do item da área de transferência, passando o Tipo MIME que eu 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 isso é quase desnecessário neste momento. 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 macOS Preview e copiar para a área de transferência. Quando clico em Colar, o app Fugu Greetings me pergunta se eu quero permitir que o aplicativo acesse textos 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 eu abro a visualização e clico em File e em New from Clipboard, o cartão comemorativo é colado em uma nova imagem sem título.

App macOS Preview com uma imagem sem título colada.
Uma imagem colada no app macOS Preview.

API Badging

Outra API útil é a API de selos. Como um PWA instalável, o Fugu Greetings, é claro, tem um ícone do app. que os usuários podem colocar no dock de apps ou na tela inicial. Uma maneira fácil e divertida 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 da caneta sempre que ocorre o evento pointerdown. e define o selo de ícone atualizado. Sempre que a tela é apagada, 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. Portanto, a lógica de carregamento está normalmente.

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 do ícone agora está em sete.

Os números de um a sete desenhados no cartão, cada um com apenas um traço da caneta.
desenhando os números de 1 a 7 usando sete traços de caneta;
.
Ícone de crachá no app Fugu Greetings mostrando o número 7.
O contador de traços de caneta na forma de um selo de ícone do app.

API Periodic Background Sync

Quer começar o dia com algo novo? Um recurso legal do app Fugu Greetings é que ele pode inspirar você a cada manhã com uma nova imagem de plano de fundo para iniciar o cartão comemorativo. O app usa a API Periodic Background Sync para isso.

A primeira etapa é registrar um evento de sincronização periódico 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 é recuperada pela função getImageOfTheDay(), e o resultado foi propagado para todos os clientes, para que pudessem atualizar as telas e armazenamento em cache.

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, este é realmente um aprimoramento progressivo, portanto, o código só é carregado quando o O navegador oferece suporte à API. Isso se aplica tanto ao código do cliente quanto ao código do service worker. Em navegadores não compatíveis, nenhum deles é carregado. Observe como no service worker, em vez de um import() dinâmico (incompatível com um contexto de service worker ainda), Eu uso a versão clássica importScripts()

// In the client:
const registration = await navigator.serviceWorker.ready;
if (registration && 'periodicSync' in registration) {
  import('./periodic_background_sync.mjs');
}
// In the service worker:
if ('periodicSync' in self.registration) {
  importScripts('./image_of_the_day.mjs');
}

No Fugu Greetings, pressionar o botão Plano de fundo 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.
Pressionar o botão Plano de fundo mostra a imagem do dia.

API Notification Triggers

Às vezes, mesmo com muita inspiração, você precisa de um empurrãozinho para terminar uma saudação inicial carda. Esse é um recurso ativado pela API Notification Triggers. Como usuário, posso inserir um horário em que quero receber um lembrete para finalizar meu cartão comemorativo. Quando esse horário chegar, receberei uma notificação informando que meu cartão comemorativo está esperando.

Depois de solicitar o horário desejado, o app programa a notificação com um showTrigger. Pode ser um TimestampTrigger com a data do destino selecionada anteriormente. A notificação de lembrete será acionada localmente. Não é necessário 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 já mostrei até agora, este é 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 "Saudações do Fugu", aparece uma solicitação quando eu quiser ser lembrado de terminar meu cartão comemorativo.

App Fugu Greetings com uma solicitação perguntando ao usuário quando ele quer ser lembrado de terminar o cartão comemorativo.
Agendar uma notificação local para ser lembrada de terminar um cartão comemorativo.

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

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

API Wake Lock

Também quero incluir a API Wake Lock. Às vezes, você só precisa olhar para a tela por tempo suficiente até obter inspiração beijos você. O pior que pode acontecer é a tela desligar. A API Wake Lock pode impedir que isso aconteça.

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

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, este é um aprimoramento progressivo, então só preciso carregá-lo quando o navegador oferece suporte à API.

if ('wakeLock' in navigator && 'request' in navigator.wakeLock) {
  import('./wake_lock.mjs');
}

Em "Saudações do Fugu", há uma caixa de seleção Insônia que, quando marcada, mantém a tela ativada.

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

API Idle Detection

Às vezes, mesmo se você olhar para a tela por horas, Isso é inútil, e você não consegue ter a menor ideia do que fazer com um 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 limpa a tela. No momento, 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 somente a um dispositivo que o usuário esteja usando ativamente no momento.

Depois de ter certeza de que a permissão de notificações foi concedida, instalo a detector inativo. Eu registro um listener de eventos que detecta alterações ociosas, que incluem 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 inativo 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,
});

E, como sempre, eu só carrego esse código quando o navegador é compatível.

if ('IdleDetector' in window) {
  import('./idle_detection.mjs');
}

No app Fugu Greetings, a tela é apagada quando a caixa de seleção Ephemeral é está 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 estiver marcada e o usuário estiver inativo por muito tempo, a tela será desmarcada.

Encerramento

Uau, que viagem. Muitas APIs em apenas um app de exemplo. E, lembre-se, eu nunca obrigamos o usuário a pagar o custo do download para um recurso incompatível com o navegador. Ao usar o aprimoramento progressivo, posso garantir 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 aplicativos, embora seja melhor considerar um bundler para apps muito grandes.

O painel Network do Chrome DevTools mostra apenas solicitações de arquivos com código compatível com o navegador atual.
A guia "Network" do Chrome DevTools mostra apenas solicitações de arquivos com código compatível com o navegador atual.

O aplicativo pode parecer um pouco diferente em cada navegador, já que nem todas as plataformas são compatíveis com todos os recursos, mas as funcionalidades principais estão sempre presentes, aprimoradas 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.
Saudações do Fuug em execução no Chrome do 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 greet em execução no Chrome para computador mostrando muitos recursos disponíveis.
Fugu Saudações em execução no Chrome para computador.

Se você estiver interessado pelo app Fugu Greetings, encontre e bifurque no GitHub.

no GitHub.
App Fugu Greetings no GitHub.

A equipe do Chromium está trabalhando duro para tornar a grama mais verde quando se trata de APIs avançadas do Fugu. Ao aplicar o aprimoramento progressivo no desenvolvimento do meu app, Eu garanto que todos tenham uma experiência de referência boa e sólida, mas que as pessoas que usam navegadores com suporte a mais APIs de plataforma da Web terão uma experiência ainda melhor. Mal posso esperar para ver 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 artigo foi revisado por Joe Medley e basco kayce. Jake Archibald me ajudou a descobrir a situação com import() dinâmico em um contexto de service worker.