O domínio do app
Para mostrar a forma de programação mini app aplicada a um app da Web, eu precisava de uma ideia de app pequena, mas completa. O treinamento intervalado de alta intensidade (HIIT, na sigla em inglês) é uma estratégia de exercícios cardiovasculares que alternam conjuntos de períodos curtos de exercícios anaeróbicos intensos com períodos de recuperação menos intensos. Muitos treinos de HIIT usam timers de HIIT, por exemplo, esta sessão on-line de 30 minutos do canal do YouTube The Body Coach TV.
![Sessão on-line de treinamento de HIIT com cronômetro verde de alta intensidade.](https://web.developers.google.cn/static/articles/mini-apps/mini-app-example-project/image/hiit-training-online-sess-ed5b035be1093.png?authuser=6&hl=pt)
![Sessão on-line de treinamento de HIIT com cronômetro vermelho de baixa intensidade.](https://web.developers.google.cn/static/articles/mini-apps/mini-app-example-project/image/hiit-training-online-sess-a0dd5340756f1.png?authuser=6&hl=pt)
App de exemplo de HIIT Time
Neste capítulo, criei um exemplo básico de um aplicativo de timer de HIIT chamado "HIIT Time", que permite ao usuário definir e gerenciar vários timers, sempre com um intervalo de alta e baixa intensidade, e 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 anéis de progresso: o número de conjuntos, o período ativo e o período em repouso.
- Timers:gerencia timers já existentes e permite que o usuário crie novos.
- Preferências:permite alternar efeitos sonoros e saída de fala, além de selecionar idioma e tema.
As capturas de tela abaixo dão uma impressão do aplicativo.
Estrutura do app
Conforme 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 Navbar e a Tabbar são realizadas como iframes com um contêiner <div>
entre elas, com mais três iframes
para as páginas. Um deles fica sempre visível e depende da seleção ativa na barra de guias.
Um iframe final que aponta para about:blank
é veiculado para páginas no app criadas dinamicamente, que são necessárias para modificar os timers
existentes ou criar novos.
Eu chamo esse padrão de app de várias páginas de página única (MPSPA, na sigla em inglês).
![Visualização do Chrome DevTools da estrutura HTML do app mostrando que ela 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 final para páginas dinâmicas.](https://web.developers.google.cn/static/articles/mini-apps/mini-app-example-project/image/chrome-devtools-view-the-9da043317ba5b.png?authuser=6&hl=pt)
Marcação lit-html baseada em componentes
A estrutura de cada página é percebida como um scaffolding lit-html
que é avaliado dinamicamente no tempo de execução.
Para entender melhor o lit-html, é uma biblioteca de modelos HTML extensível, expressiva e eficiente para JavaScript.
Ao usá-lo diretamente nos arquivos HTML, o modelo de programação mental é diretamente orientado pelas saídas.
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 <sl-progress-ring>
do Shoelace 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 lit-html, como mostrado na lista 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.](https://web.developers.google.cn/static/articles/mini-apps/mini-app-example-project/image/three-buttons-a-progress-df2f6a20c093.png?authuser=6&hl=pt)
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 fornecendo os dados para cada página.
Essa classe também oferece suporte a 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 compartilhamento de estado por página e estado global opcionalmente persistidos.
Todas as strings são gerenciadas centralmente, portanto, a internacionalização é integrada.
O roteamento é gerenciado pelo navegador praticamente sem custos, já que o app só alterna a visibilidade do iframe e,
para páginas criadas dinamicamente, o atributo src
do iframe de marcador é modificado.
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 considerada um iframe.](https://web.developers.google.cn/static/articles/mini-apps/mini-app-example-project/image/in-app-page-realized-an-25ec73e15af7f.png?authuser=6&hl=pt)
Estilo
O estilo das páginas acontece por página no próprio arquivo CSS com escopo.
Isso significa que, em geral, os elementos podem ser abordados diretamente pelos nomes deles,
já que não pode ocorrer conflito com outras páginas.
Estilos globais são adicionados a cada página. Portanto, as configurações centrais, como font-family
ou box-sizing
,
não precisam ser declaradas repetidamente.
Também é aqui que definimos os temas e as opções do modo escuro.
A lista abaixo mostra as regras para a 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 no layout de grade.](https://web.developers.google.cn/static/articles/mini-apps/mini-app-example-project/image/hiit-app-preferences-pag-3d83359174159.png?authuser=6&hl=pt)
Wake lock de tela
Durante um treino, a tela não deve ser desligada. Em navegadores compatíveis, o HIIT Time percebe isso com um wake lock da tela. 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. Teste a demonstração em uma nova janela ou diretamente na incorporação do iframe abaixo, que simula um dispositivo móvel.
Agradecimentos
Este artigo foi revisado por Joe Medley, Kayce Basques, Milica Mihajlija, Alan Kent e Keith Gu.