Pular para o conteúdo
murad.guide

Introdução Completa à Web

Capítulo 21

JSON, fetch e async

5 min de leitura 909 palavras 2 gravuras

Às vezes o dado não está no HTML. Mora em outro arquivo. Depois que a página abriu, o JavaScript pede esse arquivo. O nome antigo disso é AJAX. O verbo moderno no navegador é fetch. Esta é o capítulo 21 de 23.

Nesta etapa o pedido é local: cardapio.json na mesma pasta do exemplo. Não chame API pública para cumprir o exercício. Rede fora da máquina é nota opcional no fim.

O exemplo está em exemplos/21-json-fetch/. Não abra pelo disco (file://). Sirva em 127.0.0.1.

Trabalhe na cópia inicio da etapa. Leia e digite o código; mude uma coisa. Antes de recarregar, anote sua previsão. Depois confira com a referência completa. Se nada mudar, confira o arquivo salvo e o caminho aberto. Blocos adicionais do capítulo podem ser digitados no console; o exemplo completo mostra a aplicação daquela etapa.

O que você vai aprender

JSON é texto combinado

JSON (JavaScript Object Notation) é um formato de mensagem. Frontend e arquivo (ou servidor) combinam o mesmo desenho: chaves entre aspas duplas, sem funções, sem comentários.

{
  "nome": "Café coado",
  "preco": 8,
  "origem": {
    "cidade": "Natal",
    "uf": "RN"
  }
}

Parece objeto JavaScript porque quase sempre é um objeto válido. A resposta da rede, porém, chega como string. Você converte.

Pare e experimente. Abra exemplos/21-json-fetch/cardapio.json. Preveja aspas nas chaves. Recarregue o arquivo no editor. Confira que não há function.

Erro comum: pôr aspas simples nas chaves do JSON. O combinado pede aspas duplas.

JSON na pasta; fetch pede o arquivo; async espera

parse e stringify

JSON.parse transforma texto em objeto. JSON.stringify faz o caminho inverso.

const texto = '{"nome":"Café coado","preco":8}';
console.log(texto);
const item = JSON.parse(texto);
console.log(item.nome);
console.log(item.preco);

Dentro de aspas de string, um " interno precisa de escape: \". Uma barra vira \\. Ou use crase `…` no JS para montar o texto de teste.

const objeto = { nome: "Tapioca", preco: 12 };
const deNovo = JSON.stringify(objeto);
console.log(deNovo);
console.log(JSON.stringify(objeto, null, 2));

O 2 no terceiro argumento é o recuo. null no segundo você ignora por agora.

Pare e experimente. No console do exemplo, rode JSON.stringify({ ok: true }). Preveja {"ok":true}. Confira.

Erro comum: chamar JSON.parse num objeto que já é objeto. Parse espera string.

fetch no arquivo da pasta

O cardápio do exemplo é uma lista. O script pede o nome relativo à página.

const resposta = await fetch("cardapio.json");
const itens = await resposta.json();

fetch devolve uma promessa: um valor que ainda vai chegar. await só funciona dentro de função async. resposta.json() lê o corpo e já monta o objeto ou o array.

Na pasta curso deste material:

python -m http.server 8771 --bind 127.0.0.1 --directory curso

Visite http://127.0.0.1:8771/exemplos/21-json-fetch/. Encerre com Ctrl+C. 127.0.0.1 é esta máquina. Não use 0.0.0.0. Não publique o JSON.

Pare e experimente. Abra a mesma pasta pelo Explorador (file://) e recarregue. Preveja falha. Volte ao 127.0.0.1. Confira a lista.

Erro comum: duplo clique em index.html e achar que o JSON “não existe”. Ele está no disco. Falta HTTP local.

Promessa com then

Pedido demora. Pode ser rápido ou não. A promessa representa o futuro. .then entrega uma função para quando a resposta chegar.

fetch("cardapio.json")
  .then(function (resposta) {
    if (!resposta.ok) {
      throw new Error("não achei o JSON");
    }
    return resposta.json();
  })
  .then(function (itens) {
    const lista = document.getElementById("lista");
    for (let i = 0; i < itens.length; i += 1) {
      const li = document.createElement("li");
      li.textContent = itens[i].nome + " — R$ " + itens[i].preco;
      lista.append(li);
    }
  })
  .catch(function (falha) {
    document.getElementById("lista").textContent = "Abra em 127.0.0.1.";
    console.error(falha);
  });

Você pode encadear .then. O return da primeira vira o argumento da segunda. .catch pega a falha.

O HTML do exemplo:

<!DOCTYPE html>
<html lang="pt-BR">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Cardápio — Casa da Restinga</title>
  <link rel="stylesheet" href="styles.css">
</head>
<body>
  <main>
    <h1>Cardápio da Restinga</h1>
    <ul id="lista"></ul>
  </main>
  <script src="script.js"></script>
</body>
</html>

Pare e experimente. Renomeie o JSON no disco e recarregue no servidor. Preveja a mensagem de falha. Restaure o nome. Recarregue. Confira os itens.

Erro comum: usar os itens antes do then. O array ainda não chegou.

async e await

A mesma espera, leitura em fila. await pausa aquela função até a promessa resolver.

async function carregar() {
  try {
    const resposta = await fetch("cardapio.json");
    if (!resposta.ok) {
      throw new Error("não achei o JSON");
    }
    const itens = await resposta.json();
    const lista = document.getElementById("lista");
    itens.forEach(function (item) {
      const li = document.createElement("li");
      li.textContent = item.nome + " — R$ " + item.preco;
      lista.append(li);
    });
  } catch (falha) {
    document.getElementById("lista").textContent = "Abra em 127.0.0.1.";
    console.error(falha);
  }
}
carregar();

await só dentro de async. Função async sempre devolve uma promessa, mesmo quando o return parece um valor comum.

Pare e experimente. Troque o nome do arquivo no fetch para "nao.json". Preveja o catch. Restaure "cardapio.json". Recarregue. Confira.

Erro comum: await fetch(...) no topo do arquivo, fora de async. O navegador recusa.

Início da referência desta etapa no navegador local

O que este volume não exige

O exercício fecha com o JSON da pasta. Não precisa de internet além do servidor local.

Algumas pessoas, depois, brincam com APIs públicas. Um exemplo conhecido é dog.ceo, que devolve foto de cão. Isso é extra opcional, fora da nota deste capítulo. A Casa da Restinga não depende disso. Não abra conta paga. Não publique chave. Não use 0.0.0.0.

Se um dia for além: fetch recebe uma URL, a resposta costuma ser JSON, resposta.json() lê. A lógica é a mesma do cardapio.json.

Pare e experimente. No exemplo, mude o texto de um item no JSON. Preveja o novo nome na lista. Recarregue em 127.0.0.1. Confira. O HTML não mudou: mudou o arquivo de dados.

Erro comum: achar que este capítulo “só funciona com API na nuvem”. O primeiro fetch do volume é local.

Atividades

  1. Qual a diferença visível entre o texto JSON e o objeto depois de parse?
  2. Por que file:// costuma quebrar o fetch deste exemplo?
  3. Onde await pode aparecer?
  4. Este capítulo exige a API dog.ceo?

Respostas comentadas

  1. JSON é string (ou arquivo de texto). Depois do parse você lê item.nome como chave.
  2. O navegador trata o disco como origem distinta e bloqueia o pedido ao JSON. HTTP em 127.0.0.1 é o teste combinado.
  3. Só dentro de função marcada async.
  4. Não. É nota opcional. O exercício pede cardapio.json na pasta.

MDN

O arquivo completo

Referência do exemplo 21-json-fetch, reproduzida aqui para que você possa conferir sem sair da página.

fetch.js

const lista = document.getElementById("lista");
async function carregar() {
  try {
    const resposta = await fetch("cardapio.json");
    if (!resposta.ok) {
      throw new Error("HTTP");
    }
    const itens = await resposta.json();
    lista.innerHTML = "";
    for (const item of itens) {
      const li = document.createElement("li");
      li.textContent = item.nome + " — " + item.preco;
      lista.append(li);
    }
  } catch (erro) {
    lista.innerHTML = "";
    const li = document.createElement("li");
    li.textContent = "Não deu para ler o JSON. Sirva em 127.0.0.1.";
    lista.append(li);
  }
}
document.getElementById("carregar").addEventListener("click", carregar);

assets/stylesheets/main.css

*,
*::before,
*::after { box-sizing: border-box; }
html, body, h1, h2, h3, p, ul { margin: 0; padding: 0; }
img { max-width: 100%; height: auto; }
body {
  font-family: "Segoe UI", Tahoma, sans-serif;
  line-height: 1.5;
  color: #1a2a2c;
  background: #f7f3ea;
}
a { color: #0d5c63; }
a:focus-visible, button:focus-visible, input:focus-visible {
  outline: 3px solid #c45c26;
  outline-offset: 3px;
}
.skip { position: absolute; left: -999px; }
.skip:focus { left: 0.5rem; top: 0.5rem; background: #fff; padding: 0.4rem 0.7rem; z-index: 2; }
.wrap { width: min(44rem, calc(100% - 2rem)); margin-inline: auto; }
header { background: #0d5c63; color: #fff; padding: 1rem 0; }
header a { color: #fff; }
.brand { font-weight: 700; text-decoration: none; }
nav ul { list-style: none; display: flex; gap: 1rem; margin-top: 0.5rem; }
nav a[aria-current="page"] { text-decoration: underline; text-underline-offset: 0.2em; }
main { padding: 1.5rem 0 3rem; }
footer { border-top: 1px solid #ddd4c4; padding: 1rem 0; font-size: 0.95rem; }
article { margin: 1.25rem 0; padding-bottom: 1rem; border-bottom: 1px solid #ddd4c4; }