Pular para o conteúdo
murad.guide

As Três Camadas

Capítulo 23

Aplicativo de imagens: local primeiro, rede depois

5 min de leitura 823 palavras 1 gravura

Você vai carregar uma lista, escolher um item e esperar uma imagem. O exemplo exemplos/23-app-fotos/ começa com uma ilustração local de um cão. Não é uma fotografia da API. Depois você pode escolher a fonte pública dog.ceo, se tiver internet.

Sirva curso somente em 127.0.0.1, como no capítulo 22. Abrir por file:// pode bloquear a leitura do JSON. A cópia inicio é independente e completa.

O que você vai aprender

Um resultado sem depender da internet

Abra a etapa e deixe Fonte em Demonstração local. O programa lê racas-demo.json. A chave message contém um objeto; Object.keys transforma seus nomes numa lista. A opção estudo usa imagem-demo.svg, uma ilustração entregue na pasta.

const racas = Object.keys(dados.message);
select.replaceChildren(...racas.map(raca => new Option(raca, raca)));

new Option cria uma opção. O texto não vira HTML. replaceChildren substitui as opções antigas. O arquivo app.js contém o programa completo; não misture pedaços de versões diferentes.

Mude uma coisa. Na cópia inicio, acrescente uma chave segunda ao objeto message do JSON. Preveja duas opções; a imagem continua a mesma porque é uma demonstração local. Salve, recarregue e confira.

Erro comum: esquecer uma vírgula ou colocar comentário no JSON. JSON é um formato de dados e não aceita comentários. Se a lista falhar, compare com o arquivo de referência.

Dois pedidos diferentes

Na demonstração local há um pedido ao servidor 127.0.0.1 para ler o JSON e outro para abrir a ilustração. Isso continua sendo rede local, não publicação pública.

A opção API pública faz pedidos a dog.ceo. A lista vem de /api/breeds/list/all; a foto escolhida vem de /api/breed/RAÇA/images/random. A resposta contém um endereço de imagem: ainda é preciso carregar esse arquivo.

async function obter(url) {
  const resposta = await fetch(url, {signal: AbortSignal.timeout(10000)});
  if (!resposta.ok) throw new Error("Pedido indisponível");
  return resposta.json();
}

await espera o resultado. resposta.ok confere o resultado HTTP. O limite de 10000 milissegundos evita esperar indefinidamente. throw interrompe o caminho normal e entrega a falha ao bloco catch.

A API é opcional. Não é um serviço criado pelo aluno e sua disponibilidade não é garantida. As fotografias são carregadas da fonte remota; não são redistribuídas neste pacote.

Mude uma coisa. Escolha API pública, observe a lista e depois volte à demonstração local. Preveja uma única opção estudo na volta. Confira mesmo se a rede externa estiver indisponível.

Esperar a imagem, não só o JSON

O aviso Carregando foto aparece antes de trocar a imagem. O programa remove a classe pronta da imagem antiga. Quando o arquivo termina, o evento load esconde o aviso e mostra a imagem nova. O evento error mostra uma falha legível.

foto.onload = () => {
  spinner.hidden = true;
  foto.classList.add("pronta");
};
foto.onerror = () => {
  retorno.textContent = "A imagem não carregou. Tente novamente.";
};

Esse trecho explica os eventos. A referência completa também confere se o pedido ainda é o atual: uma resposta lenta não deve substituir uma escolha mais recente.

Mude uma coisa. Na cópia, renomeie imagem-demo.svg sem atualizar o endereço no programa. Preveja uma mensagem de erro, sem aviso girando para sempre. Restaure o nome e clique Tentar novamente.

Diagrama do aviso de carregamento e da chegada de uma imagem

A figura é um diagrama do volume. A aparência real pode ser conferida nos arquivos executáveis.

Falhar de um jeito que permite continuar

O programa diferencia lista vazia, lista indisponível e erro da imagem. O select fica desativado enquanto não há uma escolha válida. Tentar novamente repete o percurso; escolher a demonstração local oferece um caminho independente da API.

A mensagem usa textContent: conteúdo recebido não é inserido como marcação. O aviso tem role="status". O campo tem label, a imagem tem alt atualizado e o foco permanece visível. Menos movimento desativa o giro decorativo.

Mude uma coisa. Na cópia, deixe o JSON como {"message":{}}. Preveja Lista vazia e nenhum pedido de foto. Recarregue, confira e restaure o JSON da referência.

Erro comum: imaginar que catch do JSON também detecta uma foto quebrada. São duas etapas. A imagem tem seu próprio evento error.

Atividades

  1. Por que abrir o HTML por file:// pode impedir a lista local?
  2. O que o evento load da imagem confirma que resposta.json não confirma?
  3. O que deve acontecer se message for um objeto vazio?
  4. Renomeie a imagem na cópia. Como recuperar o exercício sem alterar o programa?

Respostas comentadas

  1. O navegador restringe pedidos entre arquivos do disco. O servidor 127.0.0.1 oferece uma origem local para ler JSON; não publica o volume.
  2. Confirma que o arquivo da imagem carregou. Ler o JSON apenas entrega o endereço desse arquivo.
  3. A página mostra Lista vazia, mantém a escolha desativada e permite tentar novamente ou trocar a fonte.
  4. Restaure o nome imagem-demo.svg e use Tentar novamente. A referência ao lado mostra o nome correto.

Referências

O arquivo completo

Referência do exemplo 23-app-fotos, reproduzida aqui para que você possa conferir sem sair da página.

index.html

<!DOCTYPE html>

<html lang="pt-BR">
<head>
<meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1" name="viewport"/>
<title>Raças na Tela</title>
<link href="app.css" rel="stylesheet"/>
<style>body{overflow-wrap:anywhere}img,input,textarea,select,iframe{max-width:100%}img{height:auto}fieldset,.cartao{min-width:0}pre{max-width:100%;overflow:auto}:focus-visible{outline:3px solid #005fcc;outline-offset:3px}.atalho-conteudo{position:absolute;left:.5rem;top:-6rem;background:white;color:#172f50;padding:.5rem;z-index:999}.atalho-conteudo:focus{top:.5rem}@media(prefers-reduced-motion:reduce){*,*::before,*::after{animation:none!important;transition:none!important;scroll-behavior:auto!important}}</style></head>
<body><a class="atalho-conteudo" href="#conteudo">Pular para o conteúdo</a><main id="conteudo" tabindex="-1"><p><label for="fonte">Fonte</label><select id="fonte"><option value="local">Demonstração local</option><option value="rede">API pública: precisa de rede</option></select><button id="tentar" type="button">Tentar novamente</button><p id="retorno" role="status"></p></p>
<p>
<label for="raca">Raça</label>
<select id="raca">
<option value="">Carregando lista…</option>
</select>
</p>
<p aria-live="polite" class="spinner" hidden="" id="spinner">Carregando foto…</p>
<p><img alt="Ilustração local de um cão para estudo" height="360" id="foto" width="480"/></p>
</main><header>
<h1>Raças na Tela</h1>
<p>Comece na demonstração local. A API pública dog.ceo é opcional e precisa de rede.</p>
</header><script src="app.js"></script></body>
</html>

app.css

body { font-family: "Segoe UI", Tahoma, sans-serif; background: #fffaf3; margin: 0; padding: 1rem; }
label { font-weight: 700; margin-right: 0.5rem; }
select:focus { outline: 3px solid #c45c26; }
#foto { max-width: min(30rem, 100%); height: auto; display: none; }
#foto.pronta { display: inline-block; }
.spinner { font-weight: 700; }
.spinner.gira::after {
  content: " ◜";
  display: inline-block;
  animation: gira 1s linear infinite;
}
@keyframes gira { to { transform: rotate(360deg); } }