Como aplicar os princípios de programação do miniapp a um projeto de exemplo

O domínio do app

Para mostrar a maneira de programação de mini apps aplicada a um web app, eu precisava de uma ideia de app pequena, mas completa o suficiente. O treino intervalado de alta intensidade (HIIT) é uma estratégia de exercícios cardiovasculares que alterna períodos curtos de exercícios anaeróbicos intensos com períodos de recuperação menos intensos. Muitos treinos HIIT usam cronômetros HIIT, por exemplo, esta sessão on-line de 30 minutos do canal do YouTube The Body Coach TV.

Sessão de treino HIIT on-line com cronômetro verde de alta intensidade.
Período ativo.
Sessão de treino HIIT on-line com cronômetro vermelho de baixa intensidade.
Período de descanso.

App de exemplo HIIT Time

Para este capítulo, criei um exemplo básico de um aplicativo de cronômetro HIIT chamado "HIIT Time", que permite ao usuário definir e gerenciar vários cronômetros, sempre consistindo de um intervalo de alta e baixa intensidade, e depois selecionar um deles para uma sessão de treinamento. É um app responsivo com uma barra de navegação, uma barra de guias e três páginas:

  • Treino:a página ativa durante um treino. Ele permite que o usuário selecione um dos timers e apresenta três círculos de progresso: o número de séries, o período ativo e o período de descanso.
  • Cronômetros:gerencia os cronômetros atuais e permite que o usuário crie novos.
  • Preferências:permite ativar/desativar efeitos sonoros e saída de voz, além de selecionar idioma e tema.

As capturas de tela a seguir dão uma ideia do aplicativo.

Exemplo do app HIIT Time no modo retrato.
Guia "Treino" do HIIT Time no modo retrato.
Exemplo do app HIIT Time no modo paisagem.
Guia "Treino" do HIIT Time no modo paisagem.
Exemplo de app HIIT Time mostrando o gerenciamento de um timer.
Gerenciamento do timer do HIIT Time.

Estrutura do app

Como descrito acima, o app consiste em uma barra de navegação, uma barra de guias e três páginas, organizadas em uma grade. A barra de navegação e a barra de guias são realizadas como iframes com um contêiner <div> entre elas e mais três iframes para as páginas. Um deles está sempre visível e depende da seleção ativa na barra de guias. Um iframe final que aponta para about:blank serve para páginas no app criadas dinamicamente, que são necessárias para modificar timers atuais ou criar novos. Chamo esse padrão de app de página única com várias páginas (MPSPA, na sigla em inglês).

Visualização do Chrome DevTools da estrutura HTML do app mostrando que ele consiste em seis iframes: um para a barra de navegação, um para a barra de guias e três agrupados para cada página do app, com um iframe de marcador de posição final para páginas dinâmicas.
O app consiste em seis iframes.

Marcação lit-html baseada em componentes

A estrutura de cada página é realizada como um scaffold lit-html que é avaliado dinamicamente no tempo de execução. O lit-html é uma biblioteca de modelos HTML eficiente, expressiva e extensível para JavaScript. Ao usá-lo diretamente nos arquivos HTML, o modelo de programação mental é orientado diretamente para a saída. Como programador, você escreve um modelo de como será a saída final, e o lit-html preenche as lacunas dinamicamente com base nos seus dados e conecta os listeners de eventos. O app usa elementos personalizados de terceiros, como o <sl-progress-ring> do Shoelace (em inglês) ou um elemento personalizado autoimplementado chamado <human-duration>. Como os elementos personalizados têm uma API declarativa (por exemplo, o atributo percentage do anel de progresso), eles funcionam bem com o lit-html, como você pode ver na listagem abaixo.

<div>
  <button class="start" @click="${eventHandlers.start}" type="button">
    ${strings.START}
  </button>
  <button class="pause" @click="${eventHandlers.pause}" type="button">
    ${strings.PAUSE}
  </button>
  <button class="reset" @click="${eventHandlers.reset}" type="button">
    ${strings.RESET}
  </button>
</div>

<div class="progress-rings">
  <sl-progress-ring
    class="sets"
    percentage="${Math.floor(data.sets/data.activeTimer.sets*100)}"
  >
    <div class="progress-ring-caption">
      <span>${strings.SETS}</span>
      <span>${data.sets}</span>
    </div>
  </sl-progress-ring>
</div>
Três botões e um anel de progresso.
Seção renderizada da página correspondente à marcação acima.

Modelo de programação

Cada página tem uma classe Page correspondente que preenche a marcação lit-html com vida, fornecendo implementações dos manipuladores de eventos e os dados de cada página. Essa classe também é compatível com métodos de ciclo de vida, como onShow(), onHide(), onLoad() e onUnload(). As páginas têm acesso a um repositório de dados que serve para compartilhar o estado global e o estado por página persistido opcionalmente. Todas as strings são gerenciadas de forma centralizada, então a internacionalização é integrada. O roteamento é processado pelo navegador praticamente sem custo financeiro, já que tudo o que o app faz é alternar a visibilidade do iframe e, para páginas criadas dinamicamente, mudar o atributo src do iframe marcador de posição. O exemplo abaixo mostra o código para fechar uma página criada dinamicamente.

import Page from '../page.js';

const page = new Page({
  eventHandlers: {
    back: (e) => {
      e.preventDefault();
      window.top.history.back();
    },
  },
});
Página no app implementada como um iframe.
A navegação acontece de iframe para iframe.

Estilo

O estilo das páginas acontece por página no próprio arquivo CSS com escopo. Isso significa que os elementos geralmente podem ser abordados diretamente pelos nomes deles, já que não podem ocorrer conflitos com outras páginas. Os estilos globais são adicionados a cada página. Assim, configurações centrais, como font-family ou box-sizing, não precisam ser declaradas repetidamente. É aqui também que os temas e as opções do modo escuro são definidos. A lista abaixo mostra as regras da página "Preferências", que apresenta os vários elementos de formulário em uma grade.

main {
  max-width: 600px;
}

form {
  display: grid;
  grid-template-columns: auto 1fr;
  grid-gap: 0.5rem;
  margin-block-end: 1rem;
}

label {
  text-align: end;
  grid-column: 1 / 2;
}

input,
select {
  grid-column: 2 / 3;
}
Página de preferências do app HIIT Time mostrando um formulário em layout de grade.
Cada página é um mundo à parte. A estilização acontece diretamente com os nomes dos elementos.

Wake lock de tela

Durante um treino, a tela não deve desligar. Em navegadores compatíveis, o HIIT Time faz isso usando um wake lock. O snippet abaixo mostra como isso é feito.

if ('wakeLock' in navigator) {
  const requestWakeLock = async () => {
    try {
      page.shared.wakeLock = await navigator.wakeLock.request('screen');
      page.shared.wakeLock.addEventListener('release', () => {
        // Nothing.
      });
    } catch (err) {
      console.error(`${err.name}, ${err.message}`);
    }
  };
  // Request a screen wake lock…
  await requestWakeLock();
  // …and re-request it when the page becomes visible.
  document.addEventListener('visibilitychange', async () => {
    if (
      page.shared.wakeLock !== null &&
      document.visibilityState === 'visible'
    ) {
      await requestWakeLock();
    }
  });
}

Teste o aplicativo

O aplicativo HIIT Time está disponível no GitHub. Você pode testar a demonstração em uma nova janela ou no iframe incorporado abaixo, que simula um dispositivo móvel.

Agradecimentos

Este artigo foi revisado por Joe Medley, Kayce Basques, Milica Mihajlija, Alan Kent e Keith Gu.