O back-end: usando Gatsby.js e Node.js para atualizações de sites estáticos
Publicados: 2022-03-11Nesta série de artigos, desenvolveremos um protótipo de site de conteúdo estático. Ele gerará páginas HTML estáticas simples e atualizadas diariamente para repositórios populares do GitHub para rastrear seus lançamentos mais recentes. As estruturas de geração de páginas da Web estáticas têm ótimos recursos para conseguir isso - usaremos o Gatsby.js, um dos mais populares.
No Gatsby, existem muitas maneiras de coletar dados para um front-end sem ter um back-end (sem servidor), plataformas Headless CMS e plug-ins de origem Gatsby entre eles. Mas implementaremos um back-end para armazenar informações básicas sobre os repositórios do GitHub e seus últimos lançamentos. Assim, teremos controle total sobre nosso back-end e front-end.
Além disso, abordarei um conjunto de ferramentas para acionar uma atualização diária do seu aplicativo. Você também pode acioná-lo manualmente ou sempre que algum evento específico acontecer.
Nosso aplicativo front-end será executado no Netlify, e o aplicativo back-end funcionará no Heroku usando um plano gratuito. Ele dormirá periodicamente: “Quando alguém acessar o aplicativo, o gerenciador do dinamômetro ativará automaticamente o dinamômetro da web para executar o tipo de processo da web.” Assim, podemos ativá-lo via AWS Lambda e AWS CloudWatch. Até o momento, esta é a maneira mais econômica de ter um protótipo online 24 horas por dia, 7 dias por semana.
Nosso exemplo de site estático de nós: o que esperar
Para manter esses artigos focados em um tópico, não abordarei autenticação, validação, escalabilidade ou outros tópicos gerais. A parte de codificação deste artigo será o mais simples possível. A estrutura do projeto e o uso do conjunto correto de ferramentas são mais importantes.
Nesta primeira parte da série, desenvolveremos e implantaremos nosso aplicativo de back-end. Na segunda parte, desenvolveremos e implantaremos nosso aplicativo front-end e acionaremos compilações diárias.
O back-end do Node.js
O aplicativo de back-end será escrito em Node.js (não obrigatório, mas para simplificar) e todas as comunicações serão feitas por meio de APIs REST. Não coletaremos dados do front-end neste projeto. (Se você estiver interessado em fazer isso, dê uma olhada no Gatsby Forms.)
Primeiro, começaremos implementando um back-end de API REST simples que expõe as operações CRUD da coleção de repositórios em nosso MongoDB. Em seguida, agendaremos um cron job que consome GitHub API v4 (GraphQL) para atualizar documentos nesta coleção. Em seguida, implantaremos tudo isso na nuvem Heroku. Por fim, acionaremos uma reconstrução do front-end no final do nosso cron job.
O front-end do Gatsby.js
No segundo artigo, focaremos na implementação da API createPages
. Reuniremos todos os repositórios do back-end e geraremos uma única página inicial que contém uma lista de todos os repositórios, além de uma página para cada documento de repositório retornado. Em seguida, implantaremos nosso front-end no Netlify.
Do AWS Lambda e AWS CloudWatch
Esta parte não é obrigatória se seu aplicativo não dormir. Caso contrário, você precisa ter certeza de que seu back-end está funcionando no momento da atualização dos repositórios. Como solução, você pode criar uma programação cron no AWS CloudWatch 10 minutos antes de sua atualização diária e vinculá-la como um gatilho ao seu método GET
no AWS Lambda. Acessar o aplicativo de back-end ativará a instância do Heroku. Mais detalhes estarão no final do segundo artigo.
Aqui está a arquitetura que vamos implementar:
Suposições
Presumo que os leitores deste artigo tenham conhecimento nas seguintes áreas:
- HTML
- CSS
- JavaScript
- APIs REST
- MongoDB
- Git
- Node.js
Também é bom se você souber:
- Express.js
- Mangusto
- API do GitHub v4 (GraphQL)
- Heroku, AWS ou qualquer outra plataforma de nuvem
- Reagir
Vamos mergulhar na implementação do back-end. Vamos dividi-lo em duas tarefas. A primeira é preparar os endpoints da API REST e vinculá-los à nossa coleção de repositórios. A segunda é implementar um cron job que consome a API do GitHub e atualiza a coleção.
Desenvolvendo o back-end do Node.js Static Site Generator, Etapa 1: uma API REST simples
Usaremos Express para nossa estrutura de aplicativo da Web e Mongoose para nossa conexão MongoDB. Se você estiver familiarizado com o Express e o Mongoose, poderá pular para a Etapa 2.
(Por outro lado, se você precisar de mais familiaridade com o Express, consulte o guia inicial oficial do Express; se você não estiver no Mongoose, o guia inicial oficial do Mongoose deve ser útil.)
Estrutura do projeto
A hierarquia de arquivos/pastas do nosso projeto será simples:
Em mais detalhes:
-
env.config.js
é o arquivo de configuração das variáveis de ambiente -
routes.config.js
é para mapear endpoints de rest -
repository.controller.js
contém métodos para trabalhar em nosso modelo de repositório -
repository.model.js
contém o esquema MongoDB de repositório e operações CRUD -
index.js
é uma classe inicializadora -
package.json
contém dependências e propriedades do projeto
Implementação
Execute npm install
(ou yarn
, se você tiver o Yarn instalado) depois de adicionar essas dependências ao package.json
:
{ // ... "dependencies": { "body-parser": "1.7.0", "express": "^4.8.7", "moment": "^2.17.1", "moment-timezone": "^0.5.13", "mongoose": "^5.1.1", "node-uuid": "^1.4.8", "sync-request": "^4.0.2" } // ... }
Nosso arquivo env.config.js
tem apenas as propriedades port
, environment
( dev
ou prod
) e mongoDbUri
por enquanto:
module.exports = { "port": process.env.PORT || 3000, "environment": "dev", "mongoDbUri": process.env.MONGODB_URI || "mongodb://localhost/github-consumer" };
routes.config.js
contém mapeamentos de requisições e chamará o método correspondente do nosso controller:
const RepositoryController = require('../controller/repository.controller'); exports.routesConfig = function(app) { app.post('/repositories', [ RepositoryController.insert ]); app.get('/repositories', [ RepositoryController.list ]); app.get('/repositories/:id', [ RepositoryController.findById ]); app.patch('/repositories/:id', [ RepositoryController.patchById ]); app.delete('/repositories/:id', [ RepositoryController.deleteById ]); };
O arquivo repository.controller.js
é nossa camada de serviço. Sua responsabilidade é chamar o método correspondente do nosso modelo de repositório:
const RepositoryModel = require('../model/repository.model'); exports.insert = (req, res) => { RepositoryModel.create(req.body) .then((result) => { res.status(201).send({ id: result._id }); }); }; exports.findById = (req, res) => { RepositoryModel.findById(req.params.id) .then((result) => { res.status(200).send(result); }); }; exports.list = (req, res) => { RepositoryModel.list() .then((result) => { res.status(200).send(result); }) }; exports.patchById = (req, res) => { RepositoryModel.patchById(req.params.id, req.body) .then(() => { res.status(204).send({}); }); }; exports.deleteById = (req, res) => { RepositoryModel.deleteById(req.params.id, req.body) .then(() => { res.status(204).send({}); }); };
repository.model.js
lida com a conexão MongoDb e as operações CRUD para o modelo de repositório. Os campos do modelo são:
-
owner
: O proprietário do repositório (empresa ou usuário) -
name
: o nome do repositório -
createdAt
: A última data de criação da versão -
resourcePath
: O caminho da última versão -
tagName
: a última tag de lançamento -
releaseDescription
: Notas de lançamento -
homepageUrl
: URL inicial do projeto -
repositoryDescription
: A descrição do repositório -
avatarUrl
: URL do avatar do proprietário do projeto
const Mongoose = require('mongoose'); const Config = require('../config/env.config'); const MONGODB_URI = Config.mongoDbUri; Mongoose.connect(MONGODB_URI, { useNewUrlParser: true }); const Schema = Mongoose.Schema; const repositorySchema = new Schema({ owner: String, name: String, createdAt: String, resourcePath: String, tagName: String, releaseDescription: String, homepageUrl: String, repositoryDescription: String, avatarUrl: String }); repositorySchema.virtual('id').get(function() { return this._id.toHexString(); }); // Ensure virtual fields are serialised. repositorySchema.set('toJSON', { virtuals: true }); repositorySchema.findById = function(cb) { return this.model('Repository').find({ id: this.id }, cb); }; const Repository = Mongoose.model('repository', repositorySchema); exports.findById = (id) => { return Repository.findById(id) .then((result) => { if (result) { result = result.toJSON(); delete result._id; delete result.__v; return result; } }); }; exports.create = (repositoryData) => { const repository = new Repository(repositoryData); return repository.save(); }; exports.list = () => { return new Promise((resolve, reject) => { Repository.find() .exec(function(err, users) { if (err) { reject(err); } else { resolve(users); } }) }); }; exports.patchById = (id, repositoryData) => { return new Promise((resolve, reject) => { Repository.findById(id, function(err, repository) { if (err) reject(err); for (let i in repositoryData) { repository[i] = repositoryData[i]; } repository.save(function(err, updatedRepository) { if (err) return reject(err); resolve(updatedRepository); }); }); }) }; exports.deleteById = (id) => { return new Promise((resolve, reject) => { Repository.deleteOne({ _id: id }, (err) => { if (err) { reject(err); } else { resolve(err); } }); }); }; exports.findByOwnerAndName = (owner, name) => { return Repository.find({ owner: owner, name: name }); };
Isto é o que temos após nosso primeiro commit: Uma conexão MongoDB e nossas operações REST.

Podemos executar nossa aplicação com o seguinte comando:
node index.js
Teste
Para testar, envie solicitações para localhost:3000
(usando, por exemplo, Postman ou cURL):
Inserir um repositório (somente campos obrigatórios)
Postagem: http://localhost:3000/repositories
Corpo:
{ "owner" : "facebook", "name" : "react" }
Obter repositórios
Obtenha: http://localhost:3000/repositories
Obter por ID
Obtenha: http://localhost:3000/repositories/:id
Patch por ID
Patch: http://localhost:3000/repositories/:id
Corpo:
{ "owner" : "facebook", "name" : "facebook-android-sdk" }
Com isso funcionando, é hora de automatizar as atualizações.
Desenvolvendo o back-end do Node.js Static Site Generator, Etapa 2: um Cron Job para atualizar as versões do repositório
Nesta parte, vamos configurar um cron job simples (que começará à meia-noite UTC) para atualizar os repositórios do GitHub que inserimos em nosso banco de dados. Adicionamos apenas os parâmetros owner
e name
em nosso exemplo acima, mas esses dois campos são suficientes para acessarmos informações gerais sobre um determinado repositório.
Para atualizar nossos dados, temos que consumir a API do GitHub. Para esta parte, é melhor estar familiarizado com o GraphQL e a v4 da API do GitHub.
Também precisamos criar um token de acesso do GitHub. Os escopos mínimos necessários para isso são:
Isso gerará um token e podemos enviar solicitações para o GitHub com ele.
Agora vamos voltar ao nosso código.
Temos duas novas dependências em package.json
:
-
"axios": "^0.18.0"
é um cliente HTTP, então podemos fazer solicitações para a API do GitHub -
"cron": "^1.7.0"
é um agendador de tarefas cron
Como de costume, execute npm install
ou yarn
após adicionar dependências.
Também precisaremos de duas novas propriedades em config.js
:
-
"githubEndpoint": "https://api.github.com/graphql"
-
"githubAccessToken": process.env.GITHUB_ACCESS_TOKEN
(você precisará definir a variável de ambienteGITHUB_ACCESS_TOKEN
com seu próprio token de acesso pessoal)
Crie um novo arquivo na pasta do controller
com o nome cron.controller.js
. Ele simplesmente chamará o método updateResositories
de repository.controller.js
em horários programados:
const RepositoryController = require('../controller/repository.controller'); const CronJob = require('cron').CronJob; function updateDaily() { RepositoryController.updateRepositories(); } exports.startCronJobs = function () { new CronJob('0 0 * * *', function () {updateDaily()}, null, true, 'UTC'); };
As alterações finais para esta parte estarão em repository.controller.js
. Por brevidade, vamos projetá-lo para atualizar todos os repositórios de uma só vez. Mas se você tiver um grande número de repositórios, poderá exceder as limitações de recursos da API do GitHub. Se for esse o caso, você precisará modificá-lo para executar em lotes limitados, distribuídos ao longo do tempo.
A implementação de uma só vez da funcionalidade de atualização ficará assim:
async function asyncUpdate() { await RepositoryModel.list().then((array) => { const promises = array.map(getLatestRelease); return Promise.all(promises); }); } exports.updateRepositories = async function update() { console.log('GitHub Repositories Update Started'); await asyncUpdate().then(() => { console.log('GitHub Repositories Update Finished'); }); };
Por fim, chamaremos o endpoint e atualizaremos o modelo de repositório.
A função getLatestRelease
gerará uma consulta GraphQL e chamará a API do GitHub. A resposta dessa solicitação será processada na função updateDatabase
.
async function updateDatabase(responseData, owner, name) { let createdAt = ''; let resourcePath = ''; let tagName = ''; let releaseDescription = ''; let homepageUrl = ''; let repositoryDescription = ''; let avatarUrl = ''; if (responseData.repository.releases) { createdAt = responseData.repository.releases.nodes[0].createdAt; resourcePath = responseData.repository.releases.nodes[0].resourcePath; tagName = responseData.repository.releases.nodes[0].tagName; releaseDescription = responseData.repository.releases.nodes[0].description; homepageUrl = responseData.repository.homepageUrl; repositoryDescription = responseData.repository.description; if (responseData.organization && responseData.organization.avatarUrl) { avatarUrl = responseData.organization.avatarUrl; } else if (responseData.user && responseData.user.avatarUrl) { avatarUrl = responseData.user.avatarUrl; } const repositoryData = { owner: owner, name: name, createdAt: createdAt, resourcePath: resourcePath, tagName: tagName, releaseDescription: releaseDescription, homepageUrl: homepageUrl, repositoryDescription: repositoryDescription, avatarUrl: avatarUrl }; await RepositoryModel.findByOwnerAndName(owner, name) .then((oldGitHubRelease) => { if (!oldGitHubRelease[0]) { RepositoryModel.create(repositoryData); } else { RepositoryModel.patchById(oldGitHubRelease[0].id, repositoryData); } console.log(`Updated latest release: http://github.com${repositoryData.resourcePath}`); }); } } async function getLatestRelease(repository) { const owner = repository.owner; const name = repository.name; console.log(`Getting latest release for: http://github.com/${owner}/${name}`); const query = ` query { organization(login: "${owner}") { avatarUrl } user(login: "${owner}") { avatarUrl } repository(owner: "${owner}", name: "${name}") { homepageUrl description releases(first: 1, orderBy: {field: CREATED_AT, direction: DESC}) { nodes { createdAt resourcePath tagName description } } } }`; const jsonQuery = JSON.stringify({ query }); const headers = { 'User-Agent': 'Release Tracker', 'Authorization': `Bearer ${GITHUB_ACCESS_TOKEN}` }; await Axios.post(GITHUB_API_URL, jsonQuery, { headers: headers }).then((response) => { return updateDatabase(response.data.data, owner, name); }); }
Após nosso segundo commit, teremos implementado um agendador cron para obter atualizações diárias de nossos repositórios do GitHub.
Estamos quase terminando o back-end. Mas o último passo deve ser feito após a implementação do front-end, então vamos cobri-lo no próximo artigo.
Implantando o back-end do Node Static Site Generator no Heroku
Nesta etapa, implantaremos nosso aplicativo no Heroku, portanto, você precisará configurar uma conta com eles, caso ainda não tenha uma. Se vincularmos nossa conta Heroku ao GitHub, será muito mais fácil ter uma implantação contínua. Para isso, estou hospedando meu projeto no GitHub.
Depois de fazer login na sua conta Heroku, adicione um novo aplicativo no painel:
Dê um nome exclusivo:
Você será redirecionado para uma seção de implantação. Selecione GitHub como método de implantação, procure seu repositório e clique no botão “Conectar”:
Para simplificar, você pode habilitar implantações automáticas. Ele será implantado sempre que você enviar um commit para o repositório do GitHub:
Agora temos que adicionar o MongoDB como um recurso. Vá para a guia Recursos e clique em "Encontrar mais complementos". (Eu pessoalmente uso mLab mongoDB.)
Instale-o e digite o nome do seu aplicativo na caixa de entrada “Aplicativo para provisionar”:
Finalmente, temos que criar um arquivo chamado Procfile
no nível raiz do nosso projeto, que especifica os comandos que são executados pelo aplicativo quando o Heroku o inicia.
Nosso Procfile
é tão simples quanto isto:
web: node index.js
Crie o arquivo e confirme-o. Depois de enviar o commit, o Heroku implantará automaticamente seu aplicativo, que estará acessível como https://[YOUR_UNIQUE_APP_NAME].herokuapp.com/
.
Para verificar se está funcionando, podemos enviar as mesmas solicitações que enviamos para localhost
.
Node.js, Express, MongoDB, Cron e Heroku: estamos no meio do caminho!
Após nosso terceiro commit, é assim que nosso repositório ficará.
Até agora, implementamos a API REST baseada em Node.js/Express em nosso back-end, o atualizador que consome a API do GitHub e um cron job para ativá-la. Em seguida, implantamos nosso back-end, que posteriormente fornecerá dados para nosso gerador de conteúdo da Web estático usando o Heroku com um gancho para integração contínua. Agora você está pronto para a segunda parte, onde implementamos o front-end e completamos o aplicativo!