API Fetch

Este codelab faz parte do curso de treinamento "Desenvolvimento de Apps Web Progressivos", desenvolvido pela equipe de treinamento do Google Developers. Você vai aproveitar mais este curso se fizer os codelabs em sequência.

Para detalhes completos sobre o curso, consulte a visão geral sobre o desenvolvimento de Progressive Web Apps.

Introdução

Neste laboratório, você vai aprender a usar a API Fetch, uma interface simples para buscar recursos e uma melhoria em relação à API XMLHttpRequest.

O que você vai aprender

  • Como usar a API Fetch para solicitar recursos
  • Como fazer solicitações GET, HEAD e POST com busca
  • Como ler e definir cabeçalhos personalizados
  • O uso e as limitações do CORS

O que você precisa saber

  • JavaScript e HTML básicos
  • Familiaridade com o conceito e a sintaxe básica de Promises do ES2015

O que é necessário

  • Computador com acesso ao terminal/shell
  • Conexão com a Internet
  • Um navegador compatível com Fetch
  • Um editor de texto
  • Node e npm

Observação:embora a API Fetch não seja compatível com todos os navegadores no momento, há um polyfill.

Faça o download ou clone o repositório pwa-training-labs do GitHub e instale a versão LTS do Node.js, se necessário.

Abra a linha de comando do computador. Navegue até o diretório fetch-api-lab/app/ e inicie um servidor de desenvolvimento local:

cd fetch-api-lab/app
npm install
node server.js

Você pode encerrar o servidor a qualquer momento com Ctrl-c.

Abra o navegador e acesse localhost:8081/. Uma página com botões para fazer solicitações vai aparecer, mas eles ainda não vão funcionar.

Observação:cancele o registro de todos os service workers e limpe todos os caches deles para localhost para que não interfiram no laboratório. No Chrome DevTools, clique em Limpar dados do site na seção Limpar armazenamento da guia Aplicativo.

Abra a pasta fetch-api-lab/app/ no editor de texto de sua preferência. A pasta app/ é onde você vai criar o laboratório.

Essa pasta contém:

  • echo-servers/ contém arquivos usados para executar servidores de teste.
  • examples/ contém recursos de amostra que usamos para testar a busca.
  • js/main.js é o JavaScript principal do app, e é onde você vai escrever todo o código.
  • index.html é a página HTML principal do nosso site/aplicativo de exemplo.
  • package-lock.json e package.json são arquivos de configuração para nosso servidor de desenvolvimento e dependências do servidor de eco.
  • server.js é um servidor de desenvolvimento de nós.

A API Fetch tem uma interface relativamente simples. Esta seção explica como escrever uma solicitação HTTP básica usando o fetch.

Buscar um arquivo JSON

Em js/main.js, o botão Buscar JSON do app está anexado à função fetchJSON.

Atualize a função fetchJSON para solicitar o arquivo examples/animals.json e registrar a resposta:

function fetchJSON() {
  fetch('examples/animals.json')
    .then(logResult)
    .catch(logError);
}

Salve o script e atualize a página. Clique em Buscar JSON. O console precisa registrar a resposta da busca.

Explicação

O método fetch aceita o caminho do recurso que queremos recuperar como um parâmetro, neste caso, examples/animals.json. fetch retorna uma promessa que é resolvida em um objeto de resposta. Se a promessa for resolvida, a resposta será transmitida para a função logResult. Se a promessa for rejeitada, o catch assumirá o controle e o erro será transmitido para a função logError.

Os objetos de resposta representam a resposta a uma solicitação. Eles contêm o corpo da resposta e também propriedades e métodos úteis.

Testar respostas inválidas

Examine a resposta registrada no console. Observe os valores das propriedades status, url e ok.

Substitua o recurso examples/animals.json em fetchJSON por examples/non-existent.json. A função fetchJSON atualizada vai ficar assim:

function fetchJSON() {
  fetch('examples/non-existent.json')
    .then(logResult)
    .catch(logError);
}

Salve o script e atualize a página. Clique em Buscar JSON novamente para tentar buscar esse recurso inexistente.

Observe que a busca foi concluída com êxito e não acionou o bloqueio catch. Agora encontre as propriedades status, URL e ok da nova resposta.

Os valores precisam ser diferentes para os dois arquivos. Você entende por quê? Se você recebeu erros no console, os valores correspondem ao contexto do erro?

Explicação

Por que uma resposta com falha não ativou o bloco catch? Esta é uma observação importante para busca e promessas: respostas ruins (como 404s) ainda são resolvidas. Uma promessa de busca só é rejeitada se a solicitação não puder ser concluída. Portanto, sempre verifique a validade da resposta. Vamos validar as respostas na próxima seção.

Para saber mais

Verificar a validade da resposta

Precisamos atualizar nosso código para verificar a validade das respostas.

Em main.js, adicione uma função para validar respostas:

function validateResponse(response) {
  if (!response.ok) {
    throw Error(response.statusText);
  }
  return response;
}

Em seguida, substitua fetchJSON pelo seguinte código:

function fetchJSON() {
  fetch('examples/non-existent.json')
    .then(validateResponse)
    .then(logResult)
    .catch(logError);
}

Salve o script e atualize a página. Clique em Buscar JSON. Verifique o console. Agora, a resposta para examples/non-existent.json deve acionar o bloco catch.

Substitua examples/non-existent.json na função fetchJSON pelo examples/animals.json original. A função atualizada vai ficar assim:

function fetchJSON() {
  fetch('examples/animals.json')
    .then(validateResponse)
    .then(logResult)
    .catch(logError);
}

Salve o script e atualize a página. Clique em Buscar JSON. Você vai notar que a resposta está sendo registrada como antes.

Explicação

Agora que adicionamos a verificação validateResponse, respostas ruins (como 404s) geram um erro, e o catch assume o controle. Isso nos permite processar respostas com falha e impede que respostas inesperadas se propaguem pela cadeia de busca.

Leia a resposta

As respostas de busca são representadas como ReadableStreams (especificação de streams) e precisam ser lidas para acessar o corpo da resposta. Os objetos de resposta têm métodos para fazer isso.

Em main.js, adicione uma função readResponseAsJSON com o seguinte código:

function readResponseAsJSON(response) {
  return response.json();
}

Em seguida, substitua a função fetchJSON pelo seguinte código:

function fetchJSON() {
  fetch('examples/animals.json') // 1
  .then(validateResponse) // 2
  .then(readResponseAsJSON) // 3
  .then(logResult) // 4
  .catch(logError);
}

Salve o script e atualize a página. Clique em Buscar JSON. Verifique o console para conferir se o JSON de examples/animals.json está sendo registrado (em vez do objeto Response).

Explicação

Vamos analisar o que está acontecendo.

Etapa 1. A busca é chamada em um recurso, examples/animals.json. A busca retorna uma promessa que é resolvida em um objeto de resposta. Quando a promessa é resolvida, o objeto de resposta é transmitido para validateResponse.

Etapa 2. O validateResponse verifica se a resposta é válida (é um 200?). Caso contrário, um erro será gerado, ignorando o restante dos blocos then e acionando o bloco catch. Isso é particularmente importante. Sem essa verificação, respostas ruins são transmitidas pela cadeia e podem quebrar um código posterior que dependa de uma resposta válida. Se a resposta for válida, ela será transmitida para readResponseAsJSON.

Etapa 3. readResponseAsJSON lê o corpo da resposta usando o método Response.json(). Esse método retorna uma promessa que é resolvida como JSON. Quando essa promessa é resolvida, os dados JSON são transmitidos para logResult. Se a promessa de response.json() for rejeitada, o bloco catch será acionado.

Etapa 4. Por fim, os dados JSON da solicitação original para examples/animals.json são registrados por logResult.

Para saber mais

A busca não é limitada a JSON. Neste exemplo, vamos buscar uma imagem e anexá-la à página.

Em main.js, escreva uma função showImage com o seguinte código:

function showImage(responseAsBlob) {
  const container = document.getElementById('img-container');
  const imgElem = document.createElement('img');
  container.appendChild(imgElem);
  const imgUrl = URL.createObjectURL(responseAsBlob);
  imgElem.src = imgUrl;
}

Em seguida, adicione uma função readResponseAsBlob que lê respostas como um Blob:

function readResponseAsBlob(response) {
  return response.blob();
}

Atualize a função fetchImage com o seguinte código:

function fetchImage() {
  fetch('examples/fetching.jpg')
    .then(validateResponse)
    .then(readResponseAsBlob)
    .then(showImage)
    .catch(logError);
}

Salve o script e atualize a página. Clique em Buscar imagem. Você vai ver um cachorro adorável buscando um graveto na página (é uma piada de busca!).

Explicação

Neste exemplo, uma imagem está sendo buscada, examples/fetching.jpg. Assim como no exercício anterior, a resposta é validada com validateResponse. A resposta é lida como um Blob (em vez de JSON, como na seção anterior). Um elemento de imagem é criado e anexado à página, e o atributo src da imagem é definido como um URL de dados que representa o Blob.

Observação:o método createObjectURL() do objeto URL é usado para gerar um URL de dados que representa o Blob. Isso é importante. Não é possível definir a origem de uma imagem diretamente como um blob. O blob precisa ser convertido em um URL de dados.

Para saber mais

Esta seção é um desafio opcional.

Atualize a função fetchText para

  1. fetch /examples/words.txt
  2. validar a resposta com validateResponse
  3. ler a resposta como texto (dica: consulte Response.text())
  4. e mostrar o texto na página

Você pode usar esta função showText como auxiliar para mostrar o texto final:

function showText(responseAsText) {
  const message = document.getElementById('message');
  message.textContent = responseAsText;
}

Salve o script e atualize a página. Clique em Buscar texto. Se você implementou fetchText corretamente, um texto vai aparecer na página.

Observação : embora seja tentador buscar HTML e anexá-lo usando o atributo innerHTML, tenha cuidado. Isso pode expor seu site a ataques de scripting em vários locais.

Para saber mais

Por padrão, a busca usa o método GET, que recupera um recurso específico. Mas a busca também pode usar outros métodos HTTP.

Fazer uma solicitação HEAD

Substitua a função headRequest pelo seguinte código:

function headRequest() {
  fetch('examples/words.txt', {
    method: 'HEAD'
  })
  .then(validateResponse)
  .then(readResponseAsText)
  .then(logResult)
  .catch(logError);
}

Salve o script e atualize a página. Clique em Solicitação HEAD. Observe que o conteúdo de texto registrado está vazio.

Explicação

O método fetch pode receber um segundo parâmetro opcional, init. Esse parâmetro permite a configuração da solicitação de busca, como o método de solicitação, o modo de cache, as credenciais, entre outros.

Neste exemplo, definimos o método de solicitação de busca como HEAD usando o parâmetro init. As solicitações HEAD são iguais às GET, exceto que o corpo da resposta está vazio. Esse tipo de solicitação pode ser usado quando você só quer metadados sobre um arquivo, mas não precisa transportar todos os dados dele.

Opcional: encontrar o tamanho de um recurso

Vamos analisar os cabeçalhos da resposta de busca para examples/words.txt e determinar o tamanho do arquivo.

Atualize a função headRequest para registrar a propriedade content-length da resposta headers. Dica: consulte a documentação de cabeçalhos e o método get.

Depois de atualizar o código, salve o arquivo e atualize a página. Clique em Solicitação HEAD. O console precisa registrar o tamanho (em bytes) de examples/words.txt.

Explicação

Neste exemplo, o método HEAD é usado para solicitar o tamanho (em bytes) de um recurso (representado no cabeçalho content-length) sem carregar o recurso em si. Na prática, isso pode ser usado para determinar se o recurso completo deve ser solicitado (ou até mesmo como solicitá-lo).

Opcional: descubra o tamanho de examples/words.txt usando outro método e confirme se ele corresponde ao valor do cabeçalho de resposta. Para saber como fazer isso no seu sistema operacional específico, pesquise na Internet.

Para saber mais

O Fetch também pode enviar dados com solicitações POST.

Configurar um servidor de eco

Para este exemplo, você precisa executar um servidor de eco. No diretório fetch-api-lab/app/, execute o seguinte comando. Se a linha de comando estiver bloqueada pelo servidor localhost:8081, abra uma nova janela ou guia de linha de comando:

node echo-servers/cors-server.js

Esse comando inicia um servidor simples em localhost:5000/ que ecoa as solicitações enviadas a ele.

Você pode encerrar esse servidor a qualquer momento com ctrl+c.

Fazer uma solicitação POST

Substitua a função postRequest pelo seguinte código. Defina a função showText da seção 4 se você não tiver concluído essa seção:

function postRequest() {
  fetch('http://localhost:5000/', {
    method: 'POST',
    body: 'name=david&message=hello'
  })
    .then(validateResponse)
    .then(readResponseAsText)
    .then(showText)
    .catch(logError);
}

Salve o script e atualize a página. Clique em Solicitação POST. Observe o pedido enviado repetido na página. Ele precisa conter o nome e a mensagem (ainda não estamos recebendo dados do formulário).

Explicação

Para fazer uma solicitação POST com busca, usamos o parâmetro init para especificar o método, semelhante a como definimos o método HEAD na seção anterior. É aqui também que definimos o corpo da solicitação, neste caso, uma string simples. O corpo são os dados que queremos enviar.

Observação:em produção, sempre criptografe dados sensíveis do usuário.

Quando os dados são enviados como uma solicitação POST para localhost:5000/, a solicitação é repetida como resposta. A resposta é validada com validateResponse, lida como texto e exibida na página.

Na prática, esse servidor representaria uma API de terceiros.

Opcional: usar a interface FormData

É possível usar a interface FormData para extrair dados de formulários com facilidade.

Na função postRequest, crie uma instância de um novo objeto FormData do elemento de formulário msg-form:

const formData = new FormData(document.getElementById('msg-form'));

Em seguida, substitua o valor do parâmetro body pela variável formData.

Salve o script e atualize a página. Preencha o formulário (campos Nome e Mensagem) na página e clique em Solicitação POST. Observe o conteúdo do formulário exibido na página.

Explicação

O construtor FormData pode receber um form HTML e criar um objeto FormData. Esse objeto é preenchido com as chaves e os valores do formulário.

Para saber mais

Iniciar um servidor de eco sem CORS

Pare o servidor de eco anterior (pressionando ctrl+c na linha de comando) e inicie um novo servidor de eco no diretório fetch-lab-api/app/ executando o seguinte comando:

node echo-servers/no-cors-server.js

Esse comando configura outro servidor de eco simples, desta vez em localhost:5001/. No entanto, esse servidor não está configurado para aceitar solicitações de origem cruzada.

Buscar do novo servidor

Agora que o novo servidor está em execução em localhost:5001/, podemos enviar uma solicitação de busca para ele.

Atualize a função postRequest para buscar de localhost:5001/ em vez de localhost:5000/. Depois de atualizar o código, salve o arquivo, atualize a página e clique em Solicitação POST.

Você vai receber um erro no console indicando que a solicitação entre origens foi bloqueada porque o cabeçalho Access-Control-Allow-Origin do CORS está ausente.

Atualize o fetch na função postRequest com o código a seguir, que usa o modo no-cors (como sugere o registro de erros) e remove as chamadas para validateResponse e readResponseAsText (confira a explicação abaixo):

function postRequest() {
  const formData = new FormData(document.getElementById('msg-form'));
  fetch('http://localhost:5001/', {
    method: 'POST',
    body: formData,
    mode: 'no-cors'
  })
    .then(logResult)
    .catch(logError);
}

Salve o script e atualize a página. Em seguida, preencha o formulário de mensagem e clique em Solicitação POST.

Observe o objeto de resposta registrado no console.

Explicação

As APIs Fetch e XMLHttpRequest seguem a política de mesma origem. Isso significa que os navegadores restringem solicitações HTTP entre origens de dentro dos scripts. Uma solicitação entre origens ocorre quando um domínio (por exemplo, http://foo.com/) pede um recurso de outro domínio (por exemplo, http://bar.com/).

Observação:as restrições de solicitações de origem cruzada costumam gerar confusão. Muitos recursos, como imagens, folhas de estilo e scripts, são buscados em vários domínios (ou seja, de origem cruzada). No entanto, essas são exceções à política da mesma origem. As solicitações entre origens ainda são restritas em scripts.

Como o servidor do nosso app tem um número de porta diferente dos dois servidores de eco, as solicitações para qualquer um deles são consideradas de origem cruzada. No entanto, o primeiro servidor de eco, executado em localhost:5000/, é configurado para oferecer suporte ao CORS. Abra echo-servers/cors-server.js e examine a configuração. O novo servidor de eco, executado em localhost:5001/, não está (por isso recebemos um erro).

Usar mode: no-cors permite buscar uma resposta opaca. Isso permite que usemos uma resposta, mas impede o acesso a ela com JavaScript. Por isso, não podemos usar validateResponse, readResponseAsText ou showResponse. A resposta ainda pode ser consumida por outras APIs ou armazenada em cache por um service worker.

Modificar cabeçalhos de solicitação

O Fetch também permite modificar cabeçalhos de solicitação. Pare o servidor de eco localhost:5001 (sem CORS) e reinicie o servidor de eco localhost:5000 (CORS) da seção 6:

node echo-servers/cors-server.js

Restaure a versão anterior da função postRequest que busca dados de localhost:5000/:

function postRequest() {
  const formData = new FormData(document.getElementById('msg-form'));
  fetch('http://localhost:5000/', {
    method: 'POST',
    body: formData
  })
    .then(validateResponse)
    .then(readResponseAsText)
    .then(showText)
    .catch(logError);
}

Agora use a interface Header para criar um objeto Headers dentro da função postRequest chamada messageHeaders com o cabeçalho Content-Type igual a application/json.

Em seguida, defina a propriedade headers do objeto init como a variável messageHeaders.

Atualize a propriedade body para ser um objeto JSON stringificado, como:

JSON.stringify({ lab: 'fetch', status: 'fun' })

Depois de atualizar o código, salve o arquivo e atualize a página. Em seguida, clique em Solicitação POST.

Observe que a solicitação repetida agora tem um Content-Type de application/json (em vez de multipart/form-data, como antes).

Agora adicione um cabeçalho Content-Length personalizado ao objeto messageHeaders e atribua um tamanho arbitrário à solicitação.

Depois de atualizar o código, salve o arquivo, atualize a página e clique em Solicitação POST. Observe que esse cabeçalho não é modificado na solicitação repetida.

Explicação

A interface de cabeçalho permite a criação e modificação de objetos Headers. Alguns cabeçalhos, como Content-Type, podem ser modificados pela busca. Outros, como Content-Length, são protegidos e não podem ser modificados por motivos de segurança.

Definir cabeçalhos de solicitação personalizados

A busca é compatível com a definição de cabeçalhos personalizados.

Remova o cabeçalho Content-Length do objeto messageHeaders na função postRequest. Adicione o cabeçalho personalizado X-Custom com um valor arbitrário (por exemplo, X-CUSTOM': 'hello world').

Salve o script, atualize a página e clique em Solicitação POST.

Você vai notar que a solicitação repetida tem a propriedade X-Custom que você adicionou.

Agora adicione um cabeçalho Y-Custom ao objeto "Headers". Salve o script, atualize a página e clique em Solicitação POST.

Você vai receber um erro semelhante a este no console:

Fetch API cannot load http://localhost:5000/. Request header field y-custom is not allowed by Access-Control-Allow-Headers in preflight response.

Explicação

Assim como as solicitações de origem cruzada, os cabeçalhos personalizados precisam ser compatíveis com o servidor de onde o recurso é solicitado. Neste exemplo, nosso servidor de eco está configurado para aceitar o cabeçalho X-Custom, mas não o cabeçalho Y-Custom. Abra echo-servers/cors-server.js e procure Access-Control-Allow-Headers para conferir. Sempre que um cabeçalho personalizado é definido, o navegador realiza uma verificação de simulação. Isso significa que o navegador primeiro envia uma solicitação OPTIONS ao servidor para determinar quais métodos e cabeçalhos HTTP são permitidos. Se o servidor estiver configurado para aceitar o método e os cabeçalhos da solicitação original, ela será enviada. Caso contrário, um erro será gerado.

Para saber mais

Código da solução

Para acessar uma cópia do código em funcionamento, navegue até a pasta solution.

Agora você sabe como usar a API Fetch.

Recursos

Para conferir todos os codelabs do curso de treinamento de PWA, consulte o codelab de boas-vindas do curso.