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
epackage.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
- fetch
/examples/words.txt
- validar a resposta com
validateResponse
- ler a resposta como texto (dica: consulte Response.text())
- 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.