Objeto: criação de uma página que é construída com base em interação com com um sistema externo. A interação com o sistema externo é realizada no servidor, ou no "back-end", nome mais moderninho
Principais termos técnicos abordados: NextJS, fetch, async, await, getServerSideProps, JavaScript, HTML
Requisitos para as instruções funcionarem: ter um projeto NextJS aberto e configurado
Requisitos para compreensão das instruções: noções bem básicas de programação, HTML e NextJS
Como ler essa receita: instruções, dentro dos passos da receita e que requerem ação sua no computador, estarão escritas em cor azul. Comentários sobre os passos estarão em fonte normal, cor preta. Comandos, código-fonte, termos técnicos ou configuração explícita, estarão com fonte diferenciada.
Observação: eventuais instruções de terminal serão comandos Linux. Adapte caso esteja usando outro sistema operacional
Vamos fazer uma aplicação Web que vai interagir com um sistema externo. Nosso software vai comunicar-se com esse sistema, via http, fazendo-lhe alguma solicitação. O sistema vai responder com um texto JSON, um texto estruturado facilmente manipulável em JavaScript. Nosso aplicativo usará essa resposta para montar uma página HTML com os dados recebidos do sistema externo.
Esse sistema externo pode ser o de qualquer empresa, inclusive a NASA. Isso mesmo, a NASA e outros entes gigantescos disponibilizam a execução de funções EM SEUS SERVIDORES a partir de requisições http (ou https) e essas funções retornam, através do mesmo protocolo, um texto estruturado.
As respostas para as requisições, ou o "retorno das funções" executadas por esses sistemas externos, vêm no tal formato JSON e não em HTML como seria de se esperar. Os programas que recebem os dados no formato JSON, no entanto, podem processar esses dados para, entre outras coisas, construírem páginas HTML para serem vistas por viventes.
A gente pode entender esse cenário como sendo uma máquina (o nosso aplicativo Web) solicitando a execução de uma função numa outra máquina (o sistema externo) e processando o resultado da execução dessa função pelo sistema externo (um texto JSON) para construir algo intelegível para algum usuário (uma página HTML).
A gente chama esse conjunto de funções disponibilizadas para execução por esse sistema externo de API. Não esqueça esse nomezinho, ele é importante. No mundo Web, API é como um sistema para ser acessado por um software e não por uma pessoa. Você quer oferecer em seu aplicativo o login através do Google ou do Facebook, por exemplo? Como você não é nem o Google nem o Facebook, torça para que essas empresas disponibilizem em seus servidores uma API para que o seu aplicativo possa interagir com os sistemas de Google e Facebook e chamar alguma função de autenticação.
Essas API's normalmente não estão abertas para qualquer programa solicitar a execução de suas funções. As empresas que hospedam as API's comummente fazem algumas restrições para não acanalhar o tráfego em seus servidores.
Este sistema que acessaremos especificamente nesta receita exige que cada requisição que lhe é submetida contenha uma "API key", uma espécie de passe ou autorização para que a requisição seja efetuada. Assim, precisamos solicitar essa "API key" no site da empresa.
Abrir o site www.omdbapi.com e clicar em API Key.
Esse site permite a execução de funções de pesquisa sobre uma base de dados de filmes e retorna (em JSON) o resultado dessas pesquisas. Podemos pesquisar filmes que contenham alguma palavra no título, por exemplo, e que tenham sido produzidos num determinado ano.
Na página que foi aberta, preencher os seus dados e abrir seu email.
Neste email deve constar a API key, que nada mais é que uma sequência de caracteres alfanuméricos. Copie essa sequência, vamos precisar dela.
Vamos testar a API key para ver se com ela conseguimos interagir com o sistema externo.
Acesse pelo navegador o link: http://www.omdbapi.com/?apikey=ME_SUBSTITUA&s=bagdad trocando o ME_SUBSTITUA pela API key que você recebeu no seu email.
Estamos fazendo uma requisição para um aplicativo Web passando 2 parâmetros, um é o apikey, que o aplicativo deve verificar se é válido, e o outro parâmetro é s, parâmetro cujo valor fixamos em bagdad. Essencialmente, isso deve chamar uma função que vai pesquisar (numa base de dados) e retornar todos os filmes em cujo título conste a palavra bagdad (o "s" deve ser de "search")
Se tudo der certo, você deve receber uma resposta parecida com isso, provavelmente não tão endentado assim e com mais conteúdo (por simplicidade, cortei alguns resultados):
{
"Search": [
{
"Title": "Bagdad Café",
"Year": "1987",
"imdbID": "tt0095801",
"Type": "movie",
"Poster": "https://m.media-amazon.com/images/M/MV5BNmUwZGZhZTktNGNmOS00NDA0LWEzYWUtZjE2ODA5OTM2OTc2XkEyXkFqcGdeQXVyMTQ3Njg3MQ@@._V1_SX300.jpg"
},
{
"Title": "The Thief of Bagdad",
"Year": "1940",
"imdbID": "tt0033152",
"Type": "movie",
"Poster": "https://m.media-amazon.com/images/M/MV5BZWFhYjg4NTEtY2IzMS00YTc2LTg1NGUtMTEzNDBlZDIxZTk3XkEyXkFqcGdeQXVyMTMxMTY0OTQ@._V1_SX300.jpg"
},
{
"Title": "Bagdad",
"Year": "1949",
"imdbID": "tt0041149",
"Type": "movie",
"Poster": "https://m.media-amazon.com/images/M/MV5BZjAxYTJmMmItYzZhMS00OWMwLTkxMjEtZjVmNmM3ZGRlZTljXkEyXkFqcGdeQXVyNTk1MTk0MDI@._V1_SX300.jpg"
},
{
"Title": "Bowery to Bagdad",
"Year": "1955",
"imdbID": "tt0044388",
"Type": "movie",
"Poster": "https://m.media-amazon.com/images/M/MV5BZTMwMTUyZTktOTk4OC00ZGY2LWFmMzAtYjkxMWMxNjdmOTg4XkEyXkFqcGdeQXVyNDY3MzU2MDM@._V1_SX300.jpg"
},
{
"Title": "Babes in Bagdad",
"Year": "1952",
"imdbID": "tt0044389",
"Type": "movie",
"Poster": "https://m.media-amazon.com/images/M/MV5BYjBmNmRlNzAtZDNjYi00ZjQ0LTljYTktZGUxOGNhMjFhZWI1XkEyXkFqcGdeQXVyMTE2NzA0Ng@@._V1_SX300.jpg"
}
],
"totalResults": "59",
"Response": "True"
}
- esse é o tal JSON?
É.
- e o que djabo isso quer dizer?
Bem, as chaves mais externas significam que temos um objeto JSON como resposta. Esse objeto tem 3 atributos, um identificado por Search e outros por totalResults e Response.
O atributo Search é um array. Note que, após o "Search":, temos um abre colchete [ que é fechado ] apenas bem mais abaixo. Entre os colchetes estão os elementos que compõem o array identificado por Search, separados por vírgula.
O atributo totalResults é um string (está entre aspas) de valor 59.
O atributo Response é o string de valor "True".
Cada elemento do array Search é, em si, um objeto (começam e terminam com chaves). Cada um desses objetos que compões o array Search tem os atributos Title, Year, imdbID, Type e Poster, todos autoexplicativos e todos do tipo string.
Nós queremos que nosso aplicativo Web receba essa resposta e crie uma página HTML com título e ano de todos os objetos que compõem o array Search.
Abrir o projeto NextJS da receita anterior e crie uma nova rota app/movies/page.js. Edite o arquivo page.js e deixe-o com o seguinte conteúdo:
export default function Home(){
const data = {
"Search": [
{
"Title": "Bagdad Café",
"Year": "1987",
"imdbID": "tt0095801",
"Type": "movie",
"Poster": "https://m.media-amazon.com/images/M/MV5BNmUwZGZhZTktNGNmOS00NDA0LWEzYWUtZjE2ODA5OTM2OTc2XkEyXkFqcGdeQXVyMTQ3Njg3MQ@@._V1_SX300.jpg"
},
{
"Title": "The Thief of Bagdad",
"Year": "1940",
"imdbID": "tt0033152",
"Type": "movie",
"Poster": "https://m.media-amazon.com/images/M/MV5BZWFhYjg4NTEtY2IzMS00YTc2LTg1NGUtMTEzNDBlZDIxZTk3XkEyXkFqcGdeQXVyMTMxMTY0OTQ@._V1_SX300.jpg"
},
{
"Title": "Bagdad",
"Year": "1949",
"imdbID": "tt0041149",
"Type": "movie",
"Poster": "https://m.media-amazon.com/images/M/MV5BZjAxYTJmMmItYzZhMS00OWMwLTkxMjEtZjVmNmM3ZGRlZTljXkEyXkFqcGdeQXVyNTk1MTk0MDI@._V1_SX300.jpg"
},
{
"Title": "Bowery to Bagdad",
"Year": "1955",
"imdbID": "tt0044388",
"Type": "movie",
"Poster": "https://m.media-amazon.com/images/M/MV5BZTMwMTUyZTktOTk4OC00ZGY2LWFmMzAtYjkxMWMxNjdmOTg4XkEyXkFqcGdeQXVyNDY3MzU2MDM@._V1_SX300.jpg"
},
{
"Title": "Babes in Bagdad",
"Year": "1952",
"imdbID": "tt0044389",
"Type": "movie",
"Poster": "https://m.media-amazon.com/images/M/MV5BYjBmNmRlNzAtZDNjYi00ZjQ0LTljYTktZGUxOGNhMjFhZWI1XkEyXkFqcGdeQXVyMTE2NzA0Ng@@._V1_SX300.jpg"
}
],
"totalResults": "59",
"Response": "True"
}
return (
<div>
<div>
{data.Search[0].Title} --- {data.Search[0].Year}
</div>
</div>
)
}
Salve o arquivo e levante a aplicação com o comando de terminal npm run dev.
Acesse a página http://localhost:3000/movies
Você deve ver algo como a figura a seguir (e provavelmente mais algumas informações provenientes do layout da receita anterior, mas o que interessa é essa parte com nome do filme e título).
Mas isso parece trapaça.
Primeiro, não estamos chamando nenhuma função de sistema externo para obter os dados: os dados estão hardcoded, estão fixos e atribuídos diretamente ao objeto data.
Segundo, estamos exibindo apenas dados do primeiro elemento do vetor Search, que é atributo do objeto data.
De fato, eu copiei e colei parte do resultado de uma consulta direta pelo navegador. É uma solução provisória.
E, de fato, o código...
<div>
<div>
{data.Search[0].Title} --- {data.Search[0].Year}
</div>
</div>
...acessa apenas o primeiro elemento do array Search do objeto data. Essas chaves dentro do <div> são o que chamamos interpolação. Ali, naquele ponto do código, estamos escrevendo um código HTML mas queremos injetar valores com base nas variáveis (ou constantes, como é o caso do objeto data) que temos em mão. Daí abrimos chaves e entre elas escrevemos expressões, em JavaScript, que nos dêem os valores que devem ser injetados no código HTML que estamos construindo. Nesse caso, estamos injetando dentro do <div> os valores resultantes das expressões data.Search[0].Title (que resulta em Bagdad Café) e data.Search[0].Year (que resulta em 1987).
Assim, quando a função executa o código...
return (
<div>
<div>
{data.Search[0].Title} --- {data.Search[0].Year}
</div>
</div>
)
...o HTML retornado para que o NextJS monte a página é...
<div>
<div>
Bagdad Café --- 1987
</div>
</div>
Agora vamos resolver essa questão de apenas um elemento do array data.Search estar sendo exibido.
No arquivo /app/movies/page.js, substitua o return da função Home() para que assim fique:
return (
<div>
{ data.Search.map( (m) => <div>{m.Title} --- {m.Year}</div>) }
</div>
)
Salve o arquivo e dê uma olhada no navegador.
A página já deve estar diferente, mostrando dados de todos os elementos do array data.Search.
Mas vamos discutir o que danado é esse:
data.Search.map( (m) => <div>{m.Title} --- {m.Year}</div> )
Mais especificamente, vamos discutir o que danado é esse map.
E esse map é uma função, uma função que todo array em JavaScript tem.
- E o que essa função faz, professor?
Ela transforma um array em um outro array. Se o array tiver 100 elementos, o array resultante da função map terá também 100 elementos, mas essa função permite que a gente processe esses elementos, transforme-os, para que o array resultante não seja igual ao array original.
Um elemento do array data.Search é um objeto JSON...
{
"Title": "Bagdad Café",
"Year": "1987",
"imdbID": "tt0095801",
"Type": "movie",
"Poster": "https://m.media-amazon.com/images/M/MV5BNmUwZGZhZTktNGNmOS00NDA0LWEzYWUtZjE2ODA5OTM2OTc2XkEyXkFqcGdeQXVyMTQ3Njg3MQ@@._V1_SX300.jpg"
}
... e nós não queremos que os elementos do array resultante da função map sejam assim. Nós queremos que eles sejam algo como...
<div>
<div>Bagdad Café --- 1987</div>
</div>
Então aparentemente está tudo em casa, já que a função map, conforme já escrevi, permite que se processe cada elemento do array original e o transforme no que se desejar que conste no array resultante.
- E como fazer isso?
Através do parâmetro da função map.
- E que parâmetro é esse?
É uma outra função.
- Outra função?? Vige...
Isso, outra função.
Podemos passar funções como parâmetros para funções, ou compor funções, que nem na boa e velha matemática da qual vocês não lembram mais porra nenhuma. Toda função JS é um objeto e a gente pode passar objetos como parâmetro, a gente vive fazendo isso.
Essa função específica que passamos como parâmetro para a função map, ela recebe um elemento do array original e retorna o elemento que deve constar no array resultante.
Essa função é isso aqui:
(m) => <div>{m.Title} --- {m.Year}</div>
Em JavaScript, isso é uma função (sem nome), que recebe um parâmetro m e retorna <div>{m.Title} --- {m.Year}</div>.
A função map recebe esta função e a chama passando cada um dos elementos do array (lembre, chamamos a função map sobre um array, ela "enxerga" uma ruma de elementos) e, com os resultados, constrói e retorna um outro array.
A gente poderia escrever essa função em separado. Se você quiser, tente escrever essa função no arquivo page.js, fora do corpo da função Home()...
function transformar(elemento){
return (<div>{elemento.Title} --- {elemento.Year}</div>)
}
...e em seguida substituir o return da função Home() por...
return (
<div>
{ data.Search.map(transformar) }
</div>
)
Pode ser mais fácil de entender assim, mas sugiro que se acostume com a primeira abordagem.
É mais comum em exemplos e materiais didáticos e é coisa de quem está mais seguro com a arte da Programação Web.
- Professor, aqui tá dando um erro na execução do script, apesar de mostrar tudo direitinho na tela.
O erro deve ser coisa do tipo...
Each child in a list should have a unique "key" prop
Isso acontece porque o React (incorporado ao Next JS) requer que essas marcas renderizadas através processamento de listas (nesse caso, o <div> lá na função passada para o map) tenham uma propriedade key que seja única para cada marca. A explicação simples é que isso ajuda o React na questão do desempenho, ajudando-o a saber que porções da interface gráfica devem ser redesenhadas quando alguma informação for modificada.
Como aquele atributo imdbID na lista de objetos JSON é único para cada objeto, vamos resolver esses problema sem refletir muito e de forma bastante simples.
No arquivo /app/movies/page.js, substitua o return da função Home() para que assim fique:
return (
<div>
{ data.Search.map( (m) => <div key={m.imdbID}>{m.Title} --- {m.Year}</div>) }
</div>
)
Salve o arquivo e dê uma conferida no navegador e no console do Visual Code (ou da IDE que estiver utilizando). O erro deve ter sumido.
Agora, como diria o sábio, só falta o principal: carregar esse objeto data de um servidor externo, via chamada de API.
Precisamos escrever o código que vai fazer isso e retirar aquele código seboso e enorme de atribuição ao objeto data.
Nosso aplicativo Web pode rodar esse código que vai solicitar os dados a um sistema externo no "nosso" servidor e também no navegador do usuário. É possível acessar essa API de qualquer um desses lugares, há funções JavaScript para isso. Dependendo de onde o código esteja rodando, entretanto, escrevemos coisas diferentes, usamos bibliotecas diferentes, convenções diferentes. Nesta receita, vamos fazer com que o código que acessa o sistema externo rode no servidor que executa o nosso aplicativo Web. Esse é meio que um modelo clássico de sistema web. O navegador (cliente) solicita uma página, o servidor recebe a requisição, o servidor vai buscar dados em alguma fonte (API, base de dados), o servidor constrói uma página com os dados retornados e manda a página de volta para o cliente/navegador.
No Next JS, a partir da versão 13 (estamos na 15 enquanto reescrevo esse texto), se queremos que um código seja executado no servidor quando uma página é requisitada, precisamos apenas tornar a função que representa a rota (Home, no nosso caso) assíncrona e fazermos as requisições que bem entendermos no corpo de execução da função. A partir daquela versão, todos os componentes e páginas são "server components" por default então, caso quiséssemos uma execução do código no cliente/navegador teríamos, aí sim teríamos 1 real e 99 a mais de burocracia.
Por enquanto, é só ir buscar os dados numa API do mesmo jeito que a gente aprendeu quando estudava "JavaScript Papai e Mamãe".
Modifique sua função Home.
export default async function Home(){
const res = await fetch(`http://www.omdbapi.com/?apikey=ME_SUBSTITUA&s=bagdad`)
const data = await res.json()
return (
<div>
<div>
{data.Search.map( (m) => <div key={m.imdbID}>{m.Title} --- {m.Year}</div> )}
</div>
</div>
)
}
Vamos analisar a linha...
const res = await fetch(`http://www.omdbapi.com/?apikey=ME_SUBSTITUA&s=bagdad`)
A função fetch faz basicamente o que fizemos com o navegador: uma requisição, a uma máquina remota, de um recurso descrito numa URL, via http.
O await serve para que o programa aguarde o resultado da função para prosseguir. O fetch é função assíncrona, retorna imediatamente, sem, resultados (retorna uma "promessa", mas isso foge do assunto desta receita). O await permite que o código fique em estado de espera até que os resultados da função fetch efetivamente cheguem.
Já a linha...
const data = await res.json()
...simplesmente converte a resposta para JSON, o que não deve ser muito trabalhoso pois, como vimos, a resposta já é um objeto JSON, e armazena o resultado desta conversão no objeto data.
E é isso. O restante do código é praticamente a mesma coisa. Precisamos apenas tomar o cuidado de analisar a resposta que recebemos da chamada à API e perceber que a lista de objetos que queremos manipular está na propriedade Search da resposta, por isso escrevemos a linha
data.Search.map(...)
- Professor, mas não fica meio paia não o caba fazer sempre a mesma busca? Teria como o navegador mandar aquela chave de pesquisa pro nosso componente que roda no servidor não?
Tem sim!
Vamos lá.
A gente pode passar parâmetros para uma requisição através da URL, do mesmo jeito que fazemos com a chamada à API. Quando a gente escreve lá no fetch...
http://www.omdbapi.com/?apikey=ME_SUBSTITUA&s=bagdad
... estamos passando os parâmetros apikey e s com os valores depois do símbolo de igualdade para o servidor que roda na máquina www.omdbapi.com. Note que, para o servidor que roda em www.omdbapi.com, o nosso componente é o cliente, muito embora rode em outro servidor. Cliente, em rigor, é quem faz uma requisição.
Então já sabemos fazer com que o navegador envie parâmetros para nosso componente. Se escrevermos lá na barra do navegador algo como...
http://localhost:3000/movies?titleSearchKey=city
...estaremos passando o parâmetro de pesquisa identificado por titleSearchKey com valor city. O nome titleSearchKey é completamente arbitrário e da escolha do programador.
A questão agora é como acessar o valor desse parâmetro lá na nossa função Home. E, nesse ponto, até um programador reinventor de roda já deve ter percebido que o Next JS deve passar esse valor como parâmetro para a nossa função Home. Vamos ver como fica.
Reescreva a função Home.
export default async function Home({searchParams}){
const {titleSearchKey = 'bagdad'} = await searchParams
const res = await fetch(`http://www.omdbapi.com/?apikey=f1cbc41e&s=${titleSearchKey}`)
const data = await res.json()
return (
<div>
<div>
{data.Search.map( (m) => <div key={m.imdbID}>{m.Title} --- {m.Year}</div> )}
</div>
</div>
)
}
Vamos lá. Tem pouca linha de código mas muita emoção acontecendo.
Primeiro o Next JS passa outras coisas para a função, mas o que nos interessa, aqueles parâmetros da requisição, o Next JS identifica por searchParams, então a gente escreve...
{searchParams}
... entre chaves para extrair apenas a parte que nos interessa. Em outras funcionalidades, poderemos precisar de outros parâmetros, mas agora apenas precisamos deste.
Em seguida temos outra novidade...
const {titleSearchKey = 'bagdad'} = await searchParams
... e aqui é que é emocionante mesmo. O parâmetro searchParams é uma promessa que se resolve num objeto JSON, que "retorna" um objeto JSON. Não vejo muita necessidade disso ser uma promessa, mas o pessoal que implementou o framework é bem mais sabido que eu, deve haver uma boa razão.
Bem, se é uma promessa, a gente tem que dar esse await para acessar o JSON que a promessa nos dá. Então entenda isso: await searchParams nos entrega um objeto JSON - um objeto JSON onde vão estar todos os parâmetros da requisição.
- E a outra parte, professor??? Aquele const {titleSearchKey = 'bagdad'}
Isso é uma operação de desestruturação. Estamos arregaçando o objeto JSON e arrancando de lá o que nos interessa, que é o parâmetro titleSearchKey.
- Mas a gente não tá atribuindo o valor 'bagdad' para a variável titleSearchKey não?
Não, nobre conquistador de boneca inflável. A gente está atribuindo o valor 'bagdad' à variável titleSearchKey se a desestruturação falhar, ou seja, se não houver nenhuma propriedade titleSearchKey lá no searchParams. O 'bagdad' vai funcionar como um valor default. Assim, se a gente chamar, lá do navegador...
... o valor atribuído à variável titleSearchKey vai ser 'bagdad' porque a desestruturação vai retornar undefined (algo como nulo). Se a gente escrever um identificador de parâmetro diferente...
http://localhost:3000/movies?chaveDePesquisa=city
... vai acontecer a mesma coisa, o valor vai ser 'bagdad'.
- Aaaahhhhhh...
Eu falei que era emocionante.
Só se ligue. O identificador que for na requisição (antes do símbolo de igualdade) tem que bater com o identificador usado na operação de desestruturação.
É bem facin de dar merda, mas é assim que funciona.
Na página oficial da criança: https://nextjs.org/docs/app/api-reference/file-conventions/page
O jeito antigo de se fazer isso (antes da versão 13): https://www.youtube.com/watch?v=qUhYZi-KMPQ
JSON: https://www.youtube.com/watch?v=P81dE-tkaaA
+JSON: https://www.youtube.com/watch?v=3MREC-YEQG4
JavaScript map (vários): https://www.youtube.com/results?search_query=javascript+map
tentar refinar a pesquisa, acrescentando outros parâmetros. Veja a documentação da API para saber as possibilidades
exibir, além do título e ano de produção, uma imagem com o poster do filme