Il back-end: utilizzo di Gatsby.js e Node.js per gli aggiornamenti statici del sito

Pubblicato: 2022-03-11

In questa serie di articoli, svilupperemo un prototipo di sito Web a contenuto statico. Genererà pagine HTML statiche semplici e aggiornate quotidianamente per i repository GitHub più diffusi per tenere traccia delle loro ultime versioni. I framework di generazione di pagine Web statiche hanno ottime funzionalità per raggiungere questo obiettivo: utilizzeremo Gatsby.js, uno dei più popolari.

In Gatsby, ci sono molti modi per raccogliere dati per un front-end senza avere un back-end (serverless), piattaforme CMS Headless e plug-in di origine Gatsby tra di loro. Ma implementeremo un back-end per archiviare le informazioni di base sui repository GitHub e le loro ultime versioni. Pertanto, avremo il pieno controllo sia del back-end che del front-end.

Inoltre, tratterò una serie di strumenti per attivare un aggiornamento quotidiano della tua applicazione. Puoi anche attivarlo manualmente o ogni volta che si verifica un evento specifico.

La nostra applicazione front-end verrà eseguita su Netlify e l'applicazione back-end funzionerà su Heroku utilizzando un piano gratuito. Andrà in pausa periodicamente: "Quando qualcuno accede all'app, il gestore del banco prova riattiva automaticamente il banco prova web per eseguire il tipo di processo web". Quindi, possiamo riattivarlo tramite AWS Lambda e AWS CloudWatch. Al momento della stesura di questo articolo, questo è il modo più conveniente per avere un prototipo online 24 ore su 24, 7 giorni su 7.

Esempio di sito Web statico del nostro nodo: cosa aspettarsi

Per mantenere questi articoli incentrati su un argomento, non tratterò l'autenticazione, la convalida, la scalabilità o altri argomenti generali. La parte di codifica di questo articolo sarà il più semplice possibile. La struttura del progetto e l'utilizzo del corretto set di strumenti sono più importanti.

In questa prima parte della serie, svilupperemo e implementeremo la nostra applicazione back-end. Nella seconda parte, svilupperemo e implementeremo la nostra applicazione front-end e attiveremo build giornaliere.

Il back-end di Node.js

L'applicazione back-end verrà scritta in Node.js (non obbligatorio, ma per semplicità) e tutte le comunicazioni avverranno tramite API REST. Non raccoglieremo dati dal front-end in questo progetto. (Se sei interessato a farlo, dai un'occhiata a Gatsby Forms.)

Innanzitutto, inizieremo implementando un semplice back-end API REST che espone le operazioni CRUD della raccolta di repository nel nostro MongoDB. Quindi pianificheremo un lavoro cron che utilizza GitHub API v4 (GraphQL) per aggiornare i documenti in questa raccolta. Quindi distribuiremo tutto questo sul cloud Heroku. Infine, attiveremo una ricostruzione del front-end alla fine del nostro cron job.

Il front-end di Gatsby.js

Nel secondo articolo, ci concentreremo sull'implementazione dell'API createPages . Raccoglieremo tutti i repository dal back-end e genereremo una singola home page che contiene un elenco di tutti i repository, più una pagina per ogni documento del repository restituito. Quindi implementeremo il nostro front-end su Netlify.

Da AWS Lambda e AWS CloudWatch

Questa parte non è obbligatoria se l'applicazione non va in sospensione. Altrimenti, devi essere sicuro che il tuo back-end sia attivo e funzionante al momento dell'aggiornamento dei repository. Come soluzione, puoi creare una pianificazione cron su AWS CloudWatch 10 minuti prima dell'aggiornamento giornaliero e associarla come trigger al metodo GET in AWS Lambda. L'accesso all'applicazione back-end riattiverà l'istanza di Heroku. Maggiori dettagli saranno alla fine del secondo articolo.

Ecco l'architettura che implementeremo:

Diagramma dell'architettura che mostra AWS Lambda e CloudWatch che eseguono il ping del back-end Node.js, che riceve aggiornamenti giornalieri utilizzando l'API GitHub e quindi crea il front-end basato su Gatsby, che utilizza le API back-end per aggiornare le sue pagine statiche e distribuisce su Netlify. Il back-end viene distribuito anche su Heroku con un piano gratuito.

Presupposti

Presumo che i lettori di questo articolo abbiano conoscenze nelle seguenti aree:

  • HTML
  • CSS
  • JavaScript
  • API REST
  • MongoDB
  • Idiota
  • Node.js

Va bene anche se sai:

  • Express.js
  • Mangusta
  • API GitHub v4 (GraphQL)
  • Heroku, AWS o qualsiasi altra piattaforma cloud
  • Reagire

Immergiamoci nell'implementazione del back-end. Lo divideremo in due compiti. Il primo sta preparando gli endpoint API REST e li associa alla nostra raccolta di repository. Il secondo sta implementando un lavoro cron che utilizza l'API GitHub e aggiorna la raccolta.

Sviluppo del back-end del generatore di siti statici Node.js, passaggio 1: una semplice API REST

Useremo Express per il nostro framework di applicazioni web e Mongoose per la nostra connessione MongoDB. Se hai familiarità con Express e Mongoose, potresti essere in grado di saltare al passaggio 2.

(D'altra parte, se hai bisogno di maggiore familiarità con Express puoi consultare la guida introduttiva ufficiale di Express; se non sei su Mongoose, la guida introduttiva ufficiale di Mongoose dovrebbe essere utile.)

Struttura del progetto

La gerarchia di file/cartelle del nostro progetto sarà semplice:

Un elenco di cartelle della radice del progetto, che mostra le cartelle config, controller, model e node_modules, oltre ad alcuni file radice standard come index.js e package.json. I file delle prime tre cartelle seguono la convenzione di denominazione di ripetere il nome della cartella in ogni nome di file all'interno di una determinata cartella.

Più in dettaglio:

  • env.config.js è il file di configurazione delle variabili di ambiente
  • routes.config.js serve per mappare gli endpoint di riposo
  • repository.controller.js contiene metodi per lavorare sul nostro modello di repository
  • repository.model.js contiene lo schema MongoDB delle operazioni di repository e CRUD
  • index.js è una classe di inizializzazione
  • package.json contiene le dipendenze e le proprietà del progetto

Implementazione

Esegui npm install (o yarn , se hai installato Yarn) dopo aver aggiunto queste dipendenze 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" } // ... }

Il nostro file env.config.js ha solo le proprietà port , environment ( dev o prod ) e mongoDbUri per ora:

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

routes.config.js contiene le mappature delle richieste e chiamerà il metodo corrispondente del nostro 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 ]); };

Il file repository.controller.js è il nostro livello di servizio. La sua responsabilità è chiamare il metodo corrispondente del nostro modello di repository:

 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 gestisce la connessione MongoDb e le operazioni CRUD per il modello di repository. I campi del modello sono:

  • owner : il proprietario del repository (azienda o utente)
  • name : il nome del repository
  • createdAt : la data di creazione dell'ultima versione
  • resourcePath : l'ultimo percorso di rilascio
  • tagName : l'ultimo tag di rilascio
  • releaseDescription : Note sulla versione
  • homepageUrl : URL home del progetto
  • repositoryDescription : la descrizione del repository
  • avatarUrl : l'URL dell'avatar del proprietario del progetto
 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 }); };

Questo è ciò che abbiamo dopo il nostro primo commit: una connessione MongoDB e le nostre operazioni REST.

Possiamo eseguire la nostra applicazione con il seguente comando:

 node index.js

Test

Per il test, invia le richieste a localhost:3000 (usando ad esempio Postman o cURL):

Inserisci un repository (solo campi obbligatori)

Posta: http://localhost:3000/repositories

Corpo:

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

Ottieni repository

Ottieni: http://localhost:3000/repositories

Ottieni per ID

Ottieni: http://localhost:3000/repositories/:id

Patch per ID

Patch: http://localhost:3000/repositories/:id

Corpo:

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

Con questo lavoro, è tempo di automatizzare gli aggiornamenti.

Sviluppo del back-end del generatore di siti statici Node.js, passaggio 2: un lavoro Cron per aggiornare le versioni del repository

In questa parte configureremo un semplice cron job (che partirà a mezzanotte UTC) per aggiornare i repository GitHub che abbiamo inserito nel nostro database. Abbiamo aggiunto solo i parametri del owner e del name solo nel nostro esempio sopra, ma questi due campi sono sufficienti per accedere alle informazioni generali su un determinato repository.

Per aggiornare i nostri dati, dobbiamo utilizzare l'API GitHub. Per questa parte, è meglio avere familiarità con GraphQL e v4 dell'API GitHub.

Abbiamo anche bisogno di creare un token di accesso GitHub. Gli ambiti minimi richiesti per questo sono:

Gli ambiti dei token GitHub di cui abbiamo bisogno sono repo:status, repo_deployment, public_repo, read:org e read:user.

Ciò genererà un token e possiamo inviare richieste a GitHub con esso.

Ora torniamo al nostro codice.

Abbiamo due nuove dipendenze in package.json :

  • "axios": "^0.18.0" è un client HTTP, quindi possiamo effettuare richieste all'API GitHub
  • "cron": "^1.7.0" è un programmatore di lavoro cron

Come al solito, esegui npm install o yarn dopo aver aggiunto le dipendenze.

Avremo bisogno anche di due nuove proprietà in config.js :

  • "githubEndpoint": "https://api.github.com/graphql"
  • "githubAccessToken": process.env.GITHUB_ACCESS_TOKEN (dovrai impostare la variabile d'ambiente GITHUB_ACCESS_TOKEN con il tuo token di accesso personale)

Crea un nuovo file nella cartella del controller con il nome cron.controller.js . Chiamerà semplicemente il metodo updateResositories di repository.controller.js a orari programmati:

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

Le modifiche finali per questa parte saranno in repository.controller.js . Per brevità, lo progetteremo per aggiornare tutti i repository contemporaneamente. Ma se disponi di un numero elevato di repository, potresti superare i limiti delle risorse dell'API di GitHub. In tal caso, dovrai modificarlo per l'esecuzione in batch limitati, distribuiti nel tempo.

L'implementazione completa della funzionalità di aggiornamento sarà simile a questa:

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

Infine, chiameremo l'endpoint e aggiorneremo il modello di repository.

La funzione getLatestRelease genererà una query GraphQL e chiamerà l'API GitHub. La risposta da tale richiesta verrà quindi elaborata nella funzione 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); }); }

Dopo il nostro secondo commit, avremo implementato uno scheduler cron per ottenere aggiornamenti giornalieri dai nostri repository GitHub.

Abbiamo quasi finito con il back-end. Ma l'ultimo passaggio dovrebbe essere fatto dopo aver implementato il front-end, quindi lo tratteremo nel prossimo articolo.

Distribuzione del back-end del generatore di siti statici del nodo su Heroku

In questo passaggio, implementeremo la nostra applicazione su Heroku, quindi dovrai creare un account con loro se non ne hai già uno. Se colleghiamo il nostro account Heroku a GitHub, sarà molto più facile per noi avere un'implementazione continua. A tal fine, sto ospitando il mio progetto su GitHub.

Dopo aver effettuato l'accesso al tuo account Heroku, aggiungi una nuova app dalla dashboard:

Scegli "Crea nuova app" dal menu Nuovo nella dashboard di Heroku.

Dagli un nome univoco:

Assegna un nome alla tua app in Heroku.

Verrai reindirizzato a una sezione di distribuzione. Seleziona GitHub come metodo di distribuzione, cerca il tuo repository, quindi fai clic sul pulsante "Connetti":

Collegamento del tuo nuovo repository GitHub alla tua app Heroku.

Per semplicità, puoi abilitare le distribuzioni automatiche. Verrà distribuito ogni volta che esegui il push di un commit nel repository GitHub:

Abilitazione delle distribuzioni automatiche in Heroku.

Ora dobbiamo aggiungere MongoDB come risorsa. Vai alla scheda Risorse e fai clic su "Trova altri componenti aggiuntivi". (Io personalmente uso mLab mongoDB.)

Aggiunta di una risorsa MongoDB alla tua app Heroku.

Installalo e inserisci il nome della tua app nella casella di input "App a cui eseguire il provisioning":

La pagina di fornitura del componente aggiuntivo mLab MongoDB in Heroku.

Infine, dobbiamo creare un file chiamato Procfile a livello di root del nostro progetto, che specifica i comandi che vengono eseguiti dall'app all'avvio di Heroku.

Il nostro Procfile è così semplice:

 web: node index.js

Crea il file e confermalo. Una volta eseguito il push del commit, Heroku distribuirà automaticamente la tua applicazione, che sarà accessibile come https://[YOUR_UNIQUE_APP_NAME].herokuapp.com/ .

Per verificare se funziona, possiamo inviare le stesse richieste che abbiamo inviato a localhost .

Node.js, Express, MongoDB, Cron e Heroku: siamo a metà strada!

Dopo il nostro terzo commit, ecco come apparirà il nostro repository.

Finora, abbiamo implementato l'API REST basata su Node.js/Express sul nostro back-end, il programma di aggiornamento che utilizza l'API di GitHub e un lavoro cron per attivarlo. Quindi abbiamo implementato il nostro back-end che in seguito fornirà i dati per il nostro generatore di contenuti web statici utilizzando Heroku con un hook per l'integrazione continua. Ora sei pronto per la seconda parte, dove implementiamo il front end e completiamo l'app!

Correlati: I 10 errori più comuni commessi dagli sviluppatori di Node.js