El back-end: uso de Gatsby.js y Node.js para actualizaciones de sitios estáticos

Publicado: 2022-03-11

En esta serie de artículos, desarrollaremos un prototipo de sitio web de contenido estático. Generará páginas HTML estáticas simples y actualizadas diariamente para los repositorios populares de GitHub para rastrear sus últimos lanzamientos. Los marcos de generación de páginas web estáticas tienen excelentes características para lograrlo: usaremos Gatsby.js, uno de los más populares.

En Gatsby, hay muchas formas de recopilar datos para un front-end sin tener un back-end (sin servidor), plataformas Headless CMS y complementos de fuente de Gatsby entre ellos. Pero implementaremos un back-end para almacenar información básica sobre los repositorios de GitHub y sus últimas versiones. Por lo tanto, tendremos un control total sobre nuestro back-end y front-end.

Además, cubriré un conjunto de herramientas para activar una actualización diaria de su aplicación. También puede activarlo manualmente o cada vez que ocurra algún evento específico.

Nuestra aplicación de front-end se ejecutará en Netlify y la aplicación de back-end funcionará en Heroku con un plan gratuito. Dormirá periódicamente: "Cuando alguien acceda a la aplicación, el administrador de dinamómetro activará automáticamente el dinamómetro web para ejecutar el tipo de proceso web". Entonces, podemos activarlo a través de AWS Lambda y AWS CloudWatch. Al momento de escribir este artículo, esta es la forma más rentable de tener un prototipo en línea las 24 horas del día, los 7 días de la semana.

Nuestro ejemplo de sitio web estático de nodo: qué esperar

Para mantener estos artículos centrados en un tema, no cubriré la autenticación, la validación, la escalabilidad u otros temas generales. La parte de codificación de este artículo será lo más simple posible. La estructura del proyecto y el uso del conjunto correcto de herramientas son más importantes.

En esta primera parte de la serie, desarrollaremos e implementaremos nuestra aplicación de back-end. En la segunda parte, desarrollaremos e implementaremos nuestra aplicación front-end y activaremos compilaciones diarias.

El back-end de Node.js

La aplicación de back-end se escribirá en Node.js (no es obligatorio, pero por simplicidad) y todas las comunicaciones se realizarán a través de API REST. No recopilaremos datos desde el front-end en este proyecto. (Si está interesado en hacer eso, eche un vistazo a Gatsby Forms).

Primero, comenzaremos implementando un back-end API REST simple que expone las operaciones CRUD de la colección del repositorio en nuestro MongoDB. Luego programaremos un trabajo cron que consuma GitHub API v4 (GraphQL) para actualizar los documentos en esta colección. Luego desplegaremos todo esto en la nube de Heroku. Finalmente, activaremos una reconstrucción de la interfaz al final de nuestro trabajo cron.

La interfaz de usuario de Gatsby.js

En el segundo artículo, nos centraremos en la implementación de la API createPages . Reuniremos todos los repositorios desde el back-end y generaremos una sola página de inicio que contenga una lista de todos los repositorios, además de una página para cada documento de repositorio devuelto. Luego implementaremos nuestro front-end en Netlify.

De AWS Lambda y AWS CloudWatch

Esta parte no es obligatoria si su aplicación no duerme. De lo contrario, debe asegurarse de que su back-end esté funcionando en el momento de actualizar los repositorios. Como solución, puede crear una programación cron en AWS CloudWatch 10 minutos antes de su actualización diaria y vincularla como disparador a su método GET en AWS Lambda. Acceder a la aplicación de back-end activará la instancia de Heroku. Más detalles estarán al final del segundo artículo.

Esta es la arquitectura que implementaremos:

Diagrama de arquitectura que muestra AWS Lambda y CloudWatch haciendo ping al back-end de Node.js, que recibe actualizaciones diarias al consumir la API de GitHub y luego crea el front-end basado en Gatsby, que consume las API del back-end para actualizar sus páginas estáticas y se implementa en Netlify. El back-end también se implementa en Heroku con un plan gratuito.

suposiciones

Supongo que los lectores de este artículo tienen conocimientos en las siguientes áreas:

  • HTML
  • CSS
  • JavaScript
  • API REST
  • MongoDB
  • Git
  • Nodo.js

También es bueno si sabes:

  • Express.js
  • Mangosta
  • API de GitHub v4 (GraphQL)
  • Heroku, AWS o cualquier otra plataforma en la nube
  • Reaccionar

Profundicemos en la implementación del back-end. Lo dividiremos en dos tareas. El primero es preparar los puntos finales de la API REST y vincularlos a nuestra colección de repositorios. El segundo es implementar un trabajo cron que consume la API de GitHub y actualiza la colección.

Desarrollo del back-end del generador de sitios estáticos de Node.js, paso 1: una API REST simple

Usaremos Express para nuestro marco de aplicación web y Mongoose para nuestra conexión MongoDB. Si está familiarizado con Express y Mongoose, es posible que pueda saltar al Paso 2.

(Por otro lado, si necesita familiarizarse más con Express, puede consultar la guía de inicio oficial de Express; si no está al tanto de Mongoose, la guía de inicio oficial de Mongoose debería ser útil).

Estructura del proyecto

La jerarquía de archivos/carpetas de nuestro proyecto será simple:

Una lista de carpetas de la raíz del proyecto, que muestra las carpetas config, controller, model y node_modules, además de algunos archivos raíz estándar como index.js y package.json. Los archivos de las primeras tres carpetas siguen la convención de nomenclatura de repetir el nombre de la carpeta en cada nombre de archivo dentro de una carpeta dada.

Con más detalle:

  • env.config.js es el archivo de configuración de variables de entorno
  • routes.config.js es para mapear puntos finales de descanso
  • repository.controller.js contiene métodos para trabajar en nuestro modelo de repositorio
  • repository.model.js contiene el esquema MongoDB de repositorio y operaciones CRUD
  • index.js es una clase de inicializador
  • package.json contiene dependencias y propiedades del proyecto

Implementación

Ejecute npm install (o yarn , si tiene Yarn instalado) después de agregar estas dependencias a 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" } // ... }

Nuestro archivo env.config.js solo tiene propiedades de port , environment ( dev o prod ) y mongoDbUri por ahora:

 module.exports = { "port": process.env.PORT || 3000, "environment": "dev", "mongoDbUri": process.env.MONGODB_URI || "mongodb://localhost/github-consumer" };

routes.config.js contiene asignaciones de solicitudes y llamará al método correspondiente de nuestro controlador:

 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 ]); };

El archivo repository.controller.js es nuestra capa de servicio. Su responsabilidad es llamar al método correspondiente de nuestro modelo de repositorio:

 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 maneja la conexión MongoDb y las operaciones CRUD para el modelo de repositorio. Los campos del modelo son:

  • owner : El propietario del repositorio (empresa o usuario)
  • name : El nombre del repositorio
  • createdAt : La fecha de creación de la última versión
  • resourcePath : la ruta de la última versión
  • tagName : la última etiqueta de lanzamiento
  • releaseDescription : Notas de la versión
  • homepageUrl : URL de inicio del proyecto
  • repositoryDescription : La descripción del repositorio
  • avatarUrl : la URL del avatar del propietario del proyecto
 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 }); };

Esto es lo que tenemos después de nuestra primera confirmación: una conexión MongoDB y nuestras operaciones REST.

Podemos ejecutar nuestra aplicación con el siguiente comando:

 node index.js

Pruebas

Para realizar pruebas, envíe solicitudes a localhost:3000 (usando, por ejemplo, Postman o cURL):

Insertar un Repositorio (Solo Campos Requeridos)

Publicar: http://localhost:3000/repositorios

Cuerpo:

 { "owner" : "facebook", "name" : "react" }

Obtener repositorios

Obtener: http://localhost:3000/repositorios

Obtener por identificación

Obtenga: http://localhost:3000/repositories/:id

Parche por ID

Parche: http://localhost:3000/repositories/:id

Cuerpo:

 { "owner" : "facebook", "name" : "facebook-android-sdk" }

Con eso funcionando, es hora de automatizar las actualizaciones.

Desarrollo del back-end del generador de sitios estáticos de Node.js, paso 2: un trabajo cron para actualizar las versiones del repositorio

En esta parte, configuraremos un trabajo cron simple (que comenzará a la medianoche UTC) para actualizar los repositorios de GitHub que insertamos en nuestra base de datos. Agregamos solo los parámetros de owner y name solo en nuestro ejemplo anterior, pero estos dos campos son suficientes para que podamos acceder a información general sobre un repositorio determinado.

Para actualizar nuestros datos, tenemos que consumir la API de GitHub. Para esta parte, es mejor estar familiarizado con GraphQL y v4 de la API de GitHub.

También necesitamos crear un token de acceso de GitHub. Los alcances mínimos requeridos para eso son:

Los alcances del token de GitHub que necesitamos son repo:status, repo_deployment, public_repo, read:org y read:user.

Eso generará un token y podemos enviar solicitudes a GitHub con él.

Ahora volvamos a nuestro código.

Tenemos dos nuevas dependencias en package.json :

  • "axios": "^0.18.0" es un cliente HTTP, por lo que podemos realizar solicitudes a la API de GitHub
  • "cron": "^1.7.0" es un programador de trabajos cron

Como de costumbre, ejecute npm install o yarn después de agregar las dependencias.

También necesitaremos dos propiedades nuevas en config.js :

  • "githubEndpoint": "https://api.github.com/graphql"
  • "githubAccessToken": process.env.GITHUB_ACCESS_TOKEN (deberá configurar la variable de entorno GITHUB_ACCESS_TOKEN con su propio token de acceso personal)

Cree un nuevo archivo en la carpeta del controller con el nombre cron.controller.js . Simplemente llamará al método updateResositories de repository.controller.js en horarios 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'); };

Los cambios finales para esta parte estarán en repository.controller.js . Para abreviar, lo diseñaremos para actualizar todos los repositorios a la vez. Pero si tiene una gran cantidad de repositorios, puede exceder las limitaciones de recursos de la API de GitHub. Si ese es el caso, deberá modificar esto para que se ejecute en lotes limitados, repartidos en el tiempo.

La implementación integral de la funcionalidad de actualización se verá así:

 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'); }); };

Finalmente, llamaremos al punto final y actualizaremos el modelo de repositorio.

La función getLatestRelease generará una consulta GraphQL y llamará a la API de GitHub. La respuesta de esa solicitud se procesará en la función 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); }); }

Después de nuestro segundo compromiso, habremos implementado un programador cron para obtener actualizaciones diarias de nuestros repositorios de GitHub.

Casi hemos terminado con la parte trasera. Pero el último paso debe realizarse después de implementar el front-end, por lo que lo cubriremos en el próximo artículo.

Implementación del back-end del generador de sitios estáticos de nodos en Heroku

En este paso, implementaremos nuestra aplicación en Heroku, por lo que deberá configurar una cuenta con ellos si aún no tiene una. Si vinculamos nuestra cuenta de Heroku a GitHub, será mucho más fácil para nosotros tener una implementación continua. Con ese fin, estoy alojando mi proyecto en GitHub.

Después de iniciar sesión en su cuenta de Heroku, agregue una nueva aplicación desde el tablero:

Elegir "Crear nueva aplicación" en el menú Nuevo en el panel de control de Heroku.

Dale un nombre único:

Poner nombre a tu aplicación en Heroku.

Se le redirigirá a una sección de implementación. Seleccione GitHub como método de implementación, busque su repositorio y luego haga clic en el botón "Conectar":

Vincular su nuevo repositorio de GitHub a su aplicación Heroku.

Para simplificar, puede habilitar implementaciones automáticas. Se implementará cada vez que envíe una confirmación a su repositorio de GitHub:

Habilitación de despliegues automáticos en Heroku.

Ahora tenemos que agregar MongoDB como recurso. Vaya a la pestaña Recursos y haga clic en "Buscar más complementos". (Yo personalmente uso mLab mongoDB).

Agregar un recurso MongoDB a su aplicación Heroku.

Instálelo e ingrese el nombre de su aplicación en el cuadro de entrada "Aplicación para aprovisionar a":

La página de provisión del complemento mLab MongoDB en Heroku.

Finalmente, tenemos que crear un archivo llamado Procfile en el nivel raíz de nuestro proyecto, que especifica los comandos que ejecuta la aplicación cuando Heroku la inicia.

Nuestro Procfile es tan simple como esto:

 web: node index.js

Cree el archivo y confírmelo. Una vez que presione la confirmación, Heroku implementará automáticamente su aplicación, a la que se podrá acceder como https://[YOUR_UNIQUE_APP_NAME].herokuapp.com/ .

Para verificar si funciona, podemos enviar las mismas solicitudes que enviamos a localhost .

Node.js, Express, MongoDB, Cron y Heroku: ¡Estamos a mitad de camino!

Después de nuestro tercer compromiso, así es como se verá nuestro repositorio.

Hasta ahora, implementamos la API REST basada en Node.js/Express en nuestro back-end, el actualizador que consume la API de GitHub y un trabajo cron para activarlo. Luego, implementamos nuestro back-end que luego proporcionará datos para nuestro generador de contenido web estático utilizando Heroku con un gancho para la integración continua. ¡Ahora está listo para la segunda parte, donde implementamos el front-end y completamos la aplicación!

Relacionado: Los 10 errores más comunes que cometen los desarrolladores de Node.js