O back-end: usando Gatsby.js e Node.js para atualizações de sites estáticos

Publicados: 2022-03-11

Nesta 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:

Diagrama de arquitetura mostrando o AWS Lambda e o CloudWatch fazendo ping no back-end do Node.js, que obtém atualizações diárias consumindo a API do GitHub e, em seguida, cria o front-end baseado em Gatsby, que consome APIs de back-end para atualizar suas páginas estáticas e implanta no Netlify. O back-end também é implantado no Heroku com um plano gratuito.

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:

Uma listagem de pastas da raiz do projeto, mostrando as pastas config, controller, model e node_modules, além de alguns arquivos raiz padrão, como index.js e package.json. Os arquivos das três primeiras pastas seguem a convenção de nomenclatura de repetir o nome da pasta em cada nome de arquivo dentro de uma determinada pasta.

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:

Os escopos de token do GitHub que precisamos são repo:status, repo_deployment, public_repo, read:org e read:user.

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 ambiente GITHUB_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:

Escolhendo "Criar novo aplicativo" no menu Novo no painel do Heroku.

Dê um nome exclusivo:

Nomeando seu aplicativo no Heroku.

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”:

Vinculando seu novo repositório GitHub ao seu aplicativo Heroku.

Para simplificar, você pode habilitar implantações automáticas. Ele será implantado sempre que você enviar um commit para o repositório do GitHub:

Ativando implantações automáticas no Heroku.

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.)

Adicionando um recurso do MongoDB ao seu aplicativo Heroku.

Instale-o e digite o nome do seu aplicativo na caixa de entrada “Aplicativo para provisionar”:

A página de provisão do complemento mLab MongoDB no Heroku.

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!

Relacionado: Os 10 principais erros mais comuns que os desenvolvedores do Node.js cometem