b
Implantação da aplicação na internet
Em seguida, vamos conectar o front-end que fizemos na Parte 2 ao nosso próprio back-end.
Na parte anterior, o front-end permitia requisições da lista de notas do json-server que tínhamos como back-end, a partir do endereço http://localhost:3001/notes. A estrutura de URL do nosso back-end agora é um pouco diferente, pois as notas podem ser encontradas em http://localhost:3001/api/notes. Vamos alterar o atributo baseUrl no src/services/notes.js assim:
import axios from 'axios'
const baseUrl = 'http://localhost:3001/api/notes'
const getAll = () => {
const request = axios.get(baseUrl)
return request.then(response => response.data)
}
// ...
export default { getAll, create, update }
Porém, agora a requisição GET do front-end para http://localhost:3001/api/notes não funciona por algum motivo:
O que está acontecendo aqui? Podemos acessar o back-end através do navegador e do postman sem problemas.
Política de Mesma Origem e CORS
O problema é uma coisa chamada Política de Mesma Origem
. A origem de uma URL é definida pela combinação do protocolo (protocol, também conhecido como esquema (scheme)), do nome do host e da porta (port).
http://example.com:80/index.html
protocol: http
host: example.com
port: 80
Quando você visita um site (ou seja, http://example.com), o navegador emite uma requisição para o servidor em que o site (example.com) está hospedado. A resposta enviada pelo servidor é um arquivo HTML que pode conter uma ou mais referências a recursos/ativos externos hospedados no mesmo servidor que example.com está hospedado ou em um site diferente. Quando o navegador vê referência(s) a uma URL no HTML de origem, ele emite uma requisição. Se a requisição for feita usando a URL na qual o HTML de origem foi obtido, o navegador processa a resposta sem problemas. No entanto, se o recurso for obtido usando uma URL que não compartilha a mesma origem (esquema, host, porta) que o HTML de origem, o navegador deverá verificar o cabeçalho de resposta Access-Control-Allow-origin (CORS). Se ele contiver * ou a URL do HTML de origem, o navegador processará a resposta, caso contrário, o navegador se recusará a processá-la e lançará um erro.
A Política de Mesma Origem é um mecanismo de segurança implementado pelos navegadores para impedir sequestro de sessão (session hijacking), entre outras vulnerabilidades de segurança.
Para permitir requisições legítimas de várias origens (requisições a URLs que não compartilham a mesma origem), a W3C criou um mecanismo chamado CORS (Cross-Origin Resource Sharing [Compartilhamento de Recursos de Origem Cruzada]). De acordo com a Wikipedia:
Cross-Origin Resource Sharing ou CORS é um mecanismo que permite que recursos restritos em uma página web sejam recuperados por outro domínio fora do domínio ao qual pertence o recurso que será recuperado. Uma página web pode integrar livremente recursos de diferentes origens, como imagens, folhas de estilo, scripts, iframes e vídeos. Certas "requisições de domínio cruzado", em particular as requisições Ajax, são proibidas por padrão pela política de segurança de mesma origem.
O problema é que, por padrão, o código JavaScript de uma aplicação que é executada em um navegador só pode se comunicar com um servidor na mesma origem (origin). Como nosso servidor está em localhost, porta 3001, enquanto nosso front-end está em localhost, porta 3000, eles não possuem a mesma origem.
Lembre-se de que a Política de Mesma Origem (same-origin policy) e CORS não são específicos de React ou Node. São princípios universais referentes à operação segura de aplicações web.
Podemos permitir requisições de outras origens usando o middleware cors do Node.
No repositório do seu back-end, instale o cors com o comando...
npm install cors
... use o middleware e permita requisições de todas as origens:
const cors = require('cors')
app.use(cors())
E o front-end funciona! No entanto, a funcionalidade para alternar a importância das notas ainda não foi implementada no back-end.
Você pode ler mais sobre o CORS na página da Mozilla.
A configuração de nosso aplicação agora é a seguinte:
A aplicação React sendo executada no navegador agora obtém os dados do servidor node/express que é executado em localhost:3001.
A Aplicação na Internet
Agora que toda stack está pronta, vamos mover nossa aplicação para a internet.
Há um número cada vez maior de serviços que podem ser usados para hospedar uma aplicação na internet. Serviços voltados a desenvolvedores (developer-friendly services), como o PaaS (Platform as a Service [Plataforma como Serviço]), cuidam da instalação do ambiente de execução (Node.js, por exemplo) e também podem fornecer vários serviços, como bancos de dados.
Durante uma década, Heroku dominou a cena PaaS. Infelizmente, o plano gratuito do Heroku acabou em 27 de novembro de 2022. Muitos desenvolvedores ficaram tristes com isso, especialmente estudantes. O Heroku ainda é uma opção viável se você estiver disposto a gastar algum dinheiro. Eles também têm um programa para estudantes que fornece alguns créditos gratuitos.
Agora estamos apresentando dois serviços: Fly.io e Render, ambos possuem um plano gratuito (limitado). O Fly.io é nosso serviço de hospedagem "oficial", pois pode ser usado com certeza também nas Partes 11 e 13 do curso. O Render será bom pelo menos para as outras partes deste curso.
Observe que, apesar de usar apenas o plano gratuito, o Fly.io pode exigir que você insira suas informações de cartão de crédito. No momento, o Render pode ser usado sem um cartão de crédito.
O Render pode ser um pouco mais fácil de usar, pois não requer a instalação de nenhum software em sua máquina.
Também existem outras opções gratuitas de hospedagem que funcionam bem para este curso, para todas as partes exceto a Parte 11 (CI/CD), que tem um exercício complicado de se fazer em outras plataformas.
Alguns participantes do curso também usaram estes serviços:
Se você conhece outros serviços bons e fáceis de usar para hospedar NodeJS, por favor, nos avise!
Tanto para o Fly.io quanto para o Render, precisamos mudar, no final do arquivo index.js, a definição da porta que nossa aplicação usa:
const PORT = process.env.PORT || 3001app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`)
})
Agora estamos usando a porta definida na variável de ambiente PORT ou a porta 3001 se a variável de ambiente PORT estiver indefinida. O Fly.io e o Render configuram a porta da aplicação com base nessa variável de ambiente.
Fly.io
Note que pode ser preciso fornecer seu número de cartão de crédito para o Fly.io, mesmo se estiver usando apenas o plano gratuito! Na verdade, houve relatos conflitantes sobre isso. Fato é que alguns alunos deste curso estão usando o Fly.io sem informar as informações do cartão de crédito. No momento, Render pode ser usado sem um cartão de crédito.
Por padrão, todos recebem duas máquinas virtuais gratuitas que podem ser usadas para executar duas aplicações ao mesmo tempo.
Se você decidir usar o Fly.io, comece instalando seu executável flyctl seguindo este guia. Após isso, você deve criar uma conta Fly.io.
Comece por autenticar-se via linha de comando com o comando:
fly auth login
Observação: se o comando fly não funcionar em sua máquina, você pode tentar a versão mais longa flyctl. Por exemplo, ambas as formas do comando funcionam no MacOS.
Se você não conseguir fazer o flyctl funcionar em sua máquina, é possível experimentar o Render (veja a próxima seção), que não requer nada a ser instalado em sua máquina.
Inicializa-se uma aplicação executando o seguinte comando no diretório raiz do aplicação:
fly launch
Dê um nome à aplicação ou deixe que o Fly.io gere um automaticamente. Escolha uma região onde a aplicação será executada. Não crie um banco de dados Postgres e não crie um banco de dados Upstash Redis, pois eles não são necessários.
A última pergunta é "Você gostaria de implantar agora? (Would you like to deploy now?)". Devemos responder "não" porque ainda não estamos prontos.
Fly.io cria um arquivo fly.toml na raiz da sua aplicação onde a mesma é configurada. Para colocar a aplicação em funcionamento, talvez precisemos fazer uma pequena adição na parte [env] da configuração:
[env]
PORT = "8080" # adicione isto
[experimental]
auto_rollback = true
[[services]]
http_checks = []
internal_port = 8080
processes = ["app"]
Agora definimos na parte [env] que a variável de ambiente PORT obterá a porta correta (definida na parte [services]) onde a aplicação deve criar o servidor. Observe que a definição pode já estar lá, mas às vezes ela falta.
Agora estamos prontos para implantar (deploy) a aplicação nos servidores Fly.io. Isso é feito com o seguinte comando:
fly deploy
Se tudo correr bem, a aplicação deverá estar em funcionamento. Você pode abri-la no navegador com o comando
fly apps open
Depois da configuração inicial, quando o código da aplicação for atualizado, poderá ser implantada na produção com o comando:
fly deploy
Um comando particularmente importante é fly logs. Este comando pode ser usado para visualizar os logs do servidor. É melhor manter os logs sempre visíveis!
Atenção: Em alguns casos (a causa é até agora desconhecida) executar comandos Fly.io, especialmente no Windows WSL, causou problemas. Se o seguinte comando simplesmente travar:
flyctl ping -o personal
seu computador não consegue, por algum motivo, se conectar ao Fly.io. Se isso acontecer com você, aqui encontra-se uma possível maneira de resolver o problema.
Se a saída do comando abaixo se parecer com isto:
$ flyctl ping -o personal
35 bytes from fdaa:0:8a3d::3 (gateway), seq=0 time=65.1ms
35 bytes from fdaa:0:8a3d::3 (gateway), seq=1 time=28.5ms
35 bytes from fdaa:0:8a3d::3 (gateway), seq=2 time=29.3ms
...
então não há problemas de conexão!
Render
Este serviço pressupõe que o login tenha sido feito com uma conta do GitHub.
Depois de fazer login, vamos criar um novo "Web Service":
O repositório da aplicação é então conectado ao Render:
A conexão parece exigir que o repositório da aplicação seja público.
A seguir, definiremos as configurações básicas. Se a aplicação não estiver na raiz do repositório, o diretório raiz precisa receber um valor apropriado:
Depois disso, a aplicação é iniciada no Render. O painel informa o estado da aplicação e a URL onde ela está sendo executada:
De acordo com a documentação, cada confirmação no GitHub deve fazer o redeploy (re-implantar) a aplicação. Por alguma razão, isso nem sempre funciona.
Felizmente, também é possível fazer o redeploy da aplicação manualmente:
Também é possível ver os logs da aplicação no painel:
Observamos nos logs que a aplicação foi iniciada na porta 10000. O código da aplicação obtém a porta correta por meio da variável de ambiente PORT, portanto, é essencial que o arquivo index.js tenha sido atualizado da seguinte maneira:
const PORT = process.env.PORT || 3001app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`)
})
Build de produção do front-end
Até agora, rodamos o código do React em modo de desenvolvimento. No modo de desenvolvimento, a aplicação é configurada para exibir mensagens de erro claras, renderizar imediatamente as mudanças de código para o navegador, e assim por diante.
Quando a aplicação é implantada (deployed), é necessário criar um build de produção, ou seja, uma versão da aplicação otimizada para produção.
Um build de produção de aplicações gerado com create-react-app pode ser criado com o comando npm run build.
Vamos executar esse comando a partir do diretório raiz do projeto front-end de notas que desenvolvemos na Parte 2.
Isso cria um diretório chamado build (que contém o único arquivo HTML da nossa aplicação, index.html) que contém o diretório static. Uma versão minificada do código JavaScript da nossa aplicação será gerada no diretório static. Embora o código da aplicação esteja em vários arquivos, todo o JavaScript será minificado em um arquivo. Todo o código de todas as dependências da aplicação também será minificado neste único arquivo.
O código minificado não é muito legível. O início do código se parece com isso:
!function(e){function r(r){for(var n,f,i=r[0],l=r[1],a=r[2],c=0,s=[];c<i.length;c++)f=i[c],o[f]&&s.push(o[f][0]),o[f]=0;for(n in l)Object.prototype.hasOwnProperty.call(l,n)&&(e[n]=l[n]);for(p&&p(r);s.length;)s.shift()();return u.push.apply(u,a||[]),t()}function t(){for(var e,r=0;r<u.length;r++){for(var t=u[r],n=!0,i=1;i<t.length;i++){var l=t[i];0!==o[l]&&(n=!1)}n&&(u.splice(r--,1),e=f(f.s=t[0]))}return e}var n={},o={2:0},u=[];function f(r){if(n[r])return n[r].exports;var t=n[r]={i:r,l:!1,exports:{}};return e[r].call(t.exports,t,t.exports,f),t.l=!0,t.exports}f.m=e,f.c=n,f.d=function(e,r,t){f.o(e,r)||Object.defineProperty(e,r,{enumerable:!0,get:t})},f.r=function(e){"undefined"!==typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"})
Servindo arquivos estáticos a partir do back-end
Uma opção para implantar o front-end é copiar a versão de produção (o diretório build) para a raiz do repositório back-end e configurar o back-end para mostrar a página principal do front-end (o arquivo build/index.html) como sua página principal.
Começamos copiando o build de produção do front-end para a raiz do back-end. Com um computador Mac ou Linux, a cópia pode ser feita a partir do diretório do front-end com o comando:
cp -r build ../backend
Se estiver usando um computador Windows, é possível usar o comando copy ou xcopy. Caso contrário, basta copiar e colar.
O diretório do back-end deve ficar assim agora:
Para fazer o Express exibir conteúdo estático — a página index.html e o JavaScript, etc. — que ele busca, precisamos de um middleware embutido do Express chamado static.
Quando adicionamos o seguinte código em meio às declarações dos middlewares...
app.use(express.static('build'))
... sempre que o Express recebe uma requisição HTTP GET, ele primeiro verifica se o diretório build contém um arquivo correspondente ao endereço da requisição. Se um arquivo correto for encontrado, o Express o retornará.
Agora, as requisições HTTP GET para o endereço www.serversaddress.com/index.html ou www.serversaddress.com mostrarão o front-end do React. As requisições GET para o endereço www.serversaddress.com/api/notes serão gerenciadas pelo código do back-end.
Dada nossa situação atual, pelo fato de tanto o front-end quanto o back-end estarem no mesmo endereço, podemos declarar o baseUrl como uma URL relativa (relative URL). Isso significa que podemos deixar de fora a parte que declara o servidor.
import axios from 'axios'
const baseUrl = '/api/notes'
const getAll = () => {
const request = axios.get(baseUrl)
return request.then(response => response.data)
}
// ...
Depois da alteração, temos que criar uma nova versão de produção e copiá-la para a raiz do repositório back-end.
A aplicação agora pode ser usada no endereço http://localhost:3001 do back-end:
Nossa aplicação agora funciona exatamente como o exemplo de Aplicação de Página Única que estudamos na Parte 0.
Quando usamos um navegador para acessar o endereço http://localhost:3001, o servidor retorna o arquivo index.html do repositório build. O conteúdo resumido do arquivo é o seguinte:
<head>
<meta charset="utf-8"/>
<title>React App</title>
<link href="/static/css/main.f9a47af2.chunk.css" rel="stylesheet">
</head>
<body>
<div id="root"></div>
<script src="/static/js/1.578f4ea1.chunk.js"></script>
<script src="/static/js/main.104ca08d.chunk.js"></script>
</body>
</html>
O arquivo contém instruções para buscar uma folha de estilo CSS que define os estilos da aplicação, e duas tags script que instruem o navegador a buscar o código JavaScript da aplicação — a aplicação React real.
O código React busca notas do endereço do servidor http://localhost:3001/api/notes e as renderiza na tela. As comunicações entre o servidor e o navegador podem ser vistas na guia Rede das Ferramentas do Desenvolvedor:
A configuração que está pronta para implantação de produção é a seguinte:
Ao contrário do que acontece quando a aplicação é executada em um ambiente de desenvolvimento, tudo está agora no mesmo back-end Node/Express que é executado em localhost:3001. Quando o navegador vai até a página, o arquivo index.html é renderizado. Isso faz com que o navegador busque o build de produção da aplicação React. Assim que começa a ser executada, ela busca os dados json do endereço localhost:3001/api/notes.
A aplicação toda na internet
Após garantir que a versão de produção da aplicação funciona localmente, confirme a versão de produção do front-end no repositório do back-end e envie o código para o GitHub novamente.
Se você estiver usando o Render, um "push" para o GitHub pode ser suficiente. Se a implantação automática não funcionar, selecione "manual deploy" (implantação manual) no painel do Render.
No caso do Fly.io, a nova implantação é feita com o comando:
fly deploy
A aplicação funciona perfeitamente, com exceção de que ainda não adicionamos a funcionalidade de alternar a importância de uma nota no back-end.
Nossa aplicação salva as notas em uma variável. Se a aplicação travar ou for reiniciada, todos os dados desaparecerão.
A aplicação precisa de um banco de dados. Antes de introduzirmos um, vamos passar por alguns pontos.
A configuração agora parece assim:
O back-end Node/Express agora reside no servidor Fly.io/Render. Quando o endereço raiz é acessado, o navegador carrega e executa a aplicação React que busca os dados json do servidor Fly.io/Render.
Otimização da implantação do front-end
Para criar uma nova versão de produção do front-end sem trabalho manual adicional, vamos adicionar alguns scripts npm ao package.json do repositório do back-end.
Fly.io
O script fica assim:
{
"scripts": {
// ...
"build:ui": "rm -rf build && cd ../part2-notes/ && npm run build && cp -r build ../notes-backend",
"deploy": "fly deploy",
"deploy:full": "npm run build:ui && npm run deploy",
"logs:prod": "fly logs"
}
}
O script npm run build:ui constrói o front-end e copia a versão de produção no repositório do back-end. npm run deploy libera a versão atual do back-end para o Fly.io.
npm run deploy:full combina esses dois scripts.
Existe também um script npm run logs:prod para mostrar os logs do Fly.io.
Observe que os caminhos de diretório no script build:ui dependem da localização dos repositórios no sistema de arquivos.
Render
No caso do Render, os scripts ficam assim:
{
"scripts": {
//...
"build:ui": "rm -rf build && cd ../frontend && npm run build && cp -r build ../backend",
"deploy:full": "npm run build:ui && git add . && git commit -m uibuild && git push"
}
}
O script npm run build:ui constrói o front-end e copia a versão de produção no repositório do back-end. npm run deploy:full contém também os comandos necessários git para atualizar o repositório do back-end.
Observe que os caminhos de diretório no script build:ui dependem da localização dos repositórios no sistema de arquivos.
Obs.: No Windows, scripts npm são executados em cmd.exe como o shell padrão que não oferece suporte a comandos bash. Para que os comandos bash acima funcionem, é possível alterar o shell padrão para Bash (na instalação padrão do Git para Windows) da seguinte forma:
npm config set script-shell "C:\\Program Files\\git\\bin\\bash.exe"
Outra opção é usar o shx.
Proxy
As alterações no front-end fizeram com que ele não funcionasse mais no modo de desenvolvimento (quando iniciado com o comando npm start), pois a conexão com o back-end não funciona.
Isso se deve à alteração do endereço do back-end para um URL relativo:
const baseUrl = '/api/notes'
Como no modo de desenvolvimento o front-end está no endereço localhost:3000, as requisições ao back-end vão para o endereço errado localhost:3000/api/notes. O back-end está em localhost:3001.
Esse problema é fácil de resolver se o projeto foi criado com "create-react-app". Basta adicionar a seguinte declaração ao arquivo package.json do repositório do front-end.
{
"dependencies": {
// ...
},
"scripts": {
// ...
},
"proxy": "http://localhost:3001"}
Após a reinicialização, o ambiente de desenvolvimento React funcionará como um proxy. Se o código React fizer uma requisição HTTP para um endereço de servidor em http://localhost:3000 não gerenciado pela aplicação React em si (ou seja, quando as requisições não se tratam de buscar o CSS ou JavaScript da aplicação), a requisição será redirecionada para o servidor em http://localhost:3001.
Agora o front-end já funciona bem: conecta-se ao servidor tanto no modo de desenvolvimento quanto no de produção.
Um aspecto negativo da nossa abordagem é o quão complicado é implantar o front-end. Implantar uma nova versão requer a geração de um novo build de produção do front-end e a cópia dele para o repositório do back-end. Isso torna a criação de um pipeline de implantação automatizado mais difícil. Pipeline de implantação refere-se a uma maneira automatizada e controlada de mover o código do computador do desenvolvedor por meio de diferentes testes e verificações de qualidade até o ambiente de produção. A criação de um pipeline de implantação é o tema da Parte 11 deste curso.
Existem várias maneiras de conseguir fazer isso (por exemplo, colocando o código do back-end e do front-end no mesmo repositório), mas não entraremos nesses detalhes agora.
Em algumas situações, é sensato implantar o código do front-end como sua própria aplicação. Fazer isso é simples com aplicações criadas com "create-react-app".
O código atual do back-end pode ser encontrado no GitHub, na branch part3-3. As alterações no código do frontend estão na branch part3-1 do repositório do front-end.