El back-end: uso de Gatsby.js y Node.js para actualizaciones de sitios estáticos
Publicado: 2022-03-11En 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:
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:
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:
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 entornoGITHUB_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:
Dale un nombre único:
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":
Para simplificar, puede habilitar implementaciones automáticas. Se implementará cada vez que envíe una confirmación a su repositorio de GitHub:
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).
Instálelo e ingrese el nombre de su aplicación en el cuadro de entrada "Aplicación para aprovisionar a":
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!