Back End: Utilizarea Gatsby.js și Node.js pentru actualizări statice ale site-urilor
Publicat: 2022-03-11În această serie de articole, vom dezvolta un prototip de site web cu conținut static. Va genera pagini HTML statice simple și actualizate zilnic pentru depozitele GitHub populare pentru a urmări ultimele lor versiuni. Cadrele statice de generare a paginilor web au caracteristici excelente pentru a realiza acest lucru – vom folosi Gatsby.js, unul dintre cele mai populare.
În Gatsby, există multe modalități de a colecta date pentru un front-end fără a avea un back-end (fără server), platforme CMS Headless și pluginuri sursă Gatsby printre ele. Dar vom implementa un back-end pentru a stoca informații de bază despre depozitele GitHub și cele mai recente versiuni ale acestora. Astfel, vom avea control deplin atât asupra back-end-ului, cât și asupra front-end-ului.
De asemenea, voi acoperi un set de instrumente pentru a declanșa o actualizare zilnică a aplicației dvs. De asemenea, îl puteți declanșa manual sau ori de câte ori are loc un anumit eveniment.
Aplicația noastră front-end va fi rulată pe Netlify, iar aplicația back-end va funcționa pe Heroku folosind un plan gratuit. Acesta va dormi periodic: „Când cineva accesează aplicația, managerul dyno va activa automat dyno-ul web pentru a rula tipul de proces web.” Deci, îl putem trezi prin AWS Lambda și AWS CloudWatch. În momentul în care scriu acest articol, acesta este cel mai rentabil mod de a avea un prototip online 24/7.
Exemplu de site-ul nostru static Node: la ce să vă așteptați
Pentru a menține aceste articole concentrate pe un singur subiect, nu voi acoperi autentificarea, validarea, scalabilitatea sau alte subiecte generale. Partea de codificare a acestui articol va fi cât se poate de simplă. Structura proiectului și utilizarea setului corect de instrumente sunt mai importante.
În această primă parte a seriei, vom dezvolta și implementa aplicația noastră back-end. În a doua parte, vom dezvolta și implementa aplicația noastră front-end și vom declanșa versiuni zilnice.
Back End-ul Node.js
Aplicația back-end va fi scrisă în Node.js (nu este obligatoriu, dar pentru simplitate) și toate comunicările vor fi prin API-uri REST. Nu vom colecta date de la front-end în acest proiect. (Dacă sunteți interesat să faceți asta, aruncați o privire la Gatsby Forms.)
În primul rând, vom începe prin a implementa un back-end simplu REST API care expune operațiunile CRUD ale colecției de depozit din MongoDB. Apoi vom programa un job cron care consumă GitHub API v4 (GraphQL) pentru a actualiza documentele din această colecție. Apoi vom implementa toate acestea în norul Heroku. În cele din urmă, vom declanșa o reconstrucție a front-end-ului la sfârșitul lucrării noastre cron.
Front End-ul Gatsby.js
În al doilea articol, ne vom concentra pe implementarea API-ului createPages
. Vom aduna toate depozitele din back-end și vom genera o singură pagină de pornire care conține o listă a tuturor depozitelor, plus o pagină pentru fiecare document de depozit returnat. Apoi ne vom implementa front-end-ul în Netlify.
De la AWS Lambda și AWS CloudWatch
Această parte nu este obligatorie dacă aplicația dvs. nu va inactiv. În caz contrar, trebuie să vă asigurați că back-end-ul dvs. este în funcțiune în momentul actualizării depozitelor. Ca soluție, puteți crea un program cron pe AWS CloudWatch cu 10 minute înainte de actualizarea zilnică și îl puteți lega ca declanșator la metoda GET
din AWS Lambda. Accesarea aplicației back-end va trezi instanța Heroku. Mai multe detalii vor fi la sfârșitul celui de-al doilea articol.
Iată arhitectura pe care o vom implementa:
Ipoteze
Presupun că cititorii acestui articol au cunoștințe în următoarele domenii:
- HTML
- CSS
- JavaScript
- API-urile REST
- MongoDB
- Git
- Node.js
De asemenea, este bine dacă știi:
- Express.js
- Mangustă
- GitHub API v4 (GraphQL)
- Heroku, AWS sau orice altă platformă cloud
- Reacţiona
Să ne aprofundăm în implementarea back-end-ului. Îl vom împărți în două sarcini. Primul este pregătirea punctelor finale API REST și legarea acestora la colecția noastră de depozite. Al doilea este implementarea unui job cron care consumă API-ul GitHub și actualizează colecția.
Dezvoltarea back-end-ului Node.js Static Site Generator, Pasul 1: O API REST simplă
Vom folosi Express pentru cadrul aplicației noastre web și Mongoose pentru conexiunea MongoDB. Dacă sunteți familiarizat cu Express și Mongoose, este posibil să puteți sări la Pasul 2.
(Pe de altă parte, dacă aveți nevoie de mai multă familiaritate cu Express, puteți consulta ghidul oficial de inițiere Express; dacă nu sunteți la curent cu Mongoose, ghidul oficial de inițiere pentru Mongoose ar trebui să vă fie de ajutor.)
Structura proiectului
Ierarhia fișierelor/dosarelor proiectului nostru va fi simplă:
In detaliu:
-
env.config.js
este fișierul de configurare a variabilelor de mediu -
routes.config.js
este pentru maparea punctelor finale de rest -
repository.controller.js
conține metode de lucru pe modelul nostru de depozit -
repository.model.js
conține schema MongoDB a depozitului și operațiunilor CRUD -
index.js
este o clasă de inițializare -
package.json
conține dependențe și proprietăți ale proiectului
Implementarea
Rulați npm install
(sau yarn
, dacă aveți Yarn instalat) după adăugarea acestor dependențe la 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" } // ... }
Fișierul nostru env.config.js
are doar proprietăți port
, environment
( dev
sau prod
) și mongoDbUri
pentru moment:
module.exports = { "port": process.env.PORT || 3000, "environment": "dev", "mongoDbUri": process.env.MONGODB_URI || "mongodb://localhost/github-consumer" };
routes.config.js
conține mapări de solicitări și va apela metoda corespunzătoare a controlerului nostru:
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 ]); };
Fișierul repository.controller.js
este nivelul nostru de servicii. Responsabilitatea sa este de a apela metoda corespunzătoare a modelului nostru de depozit:
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
se ocupă de conexiunea MongoDb și de operațiunile CRUD pentru modelul de depozit. Câmpurile modelului sunt:
-
owner
: proprietarul depozitului (companie sau utilizator) -
name
: numele depozitului -
createdAt
: data creării ultimei lansări -
resourcePath
: Ultima cale de lansare -
tagName
: ultima etichetă de lansare -
releaseDescription
: Note de lansare -
homepageUrl
: URL-ul principal al proiectului -
repositoryDescription
: Descrierea depozitului -
avatarUrl
: URL-ul avatarului proprietarului proiectului
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 }); };
Aceasta este ceea ce avem după primul nostru commit: o conexiune MongoDB și operațiunile noastre REST.

Putem rula aplicația noastră cu următoarea comandă:
node index.js
Testare
Pentru testare, trimiteți cereri către localhost:3000
(folosind de exemplu Postman sau cURL):
Inserați un depozit (doar câmpurile obligatorii)
Postare: http://localhost:3000/repositories
Corp:
{ "owner" : "facebook", "name" : "react" }
Obțineți depozite
Obțineți: http://localhost:3000/repositories
Obțineți prin ID
Obțineți: http://localhost:3000/repositories/:id
Corectează după ID
Patch: http://localhost:3000/repositories/:id
Corp:
{ "owner" : "facebook", "name" : "facebook-android-sdk" }
Odată ce funcționează, este timpul să automatizăm actualizările.
Dezvoltarea back-end-ului Node.js Static Site Generator, Pasul 2: O lucrare Cron pentru a actualiza versiunile de depozit
În această parte, vom configura un job cron simplu (care va începe la miezul nopții UTC) pentru a actualiza depozitele GitHub pe care le-am inserat în baza noastră de date. Am adăugat doar parametrii owner
și name
doar în exemplul nostru de mai sus, dar aceste două câmpuri sunt suficiente pentru ca noi să accesăm informații generale despre un anumit depozit.
Pentru a ne actualiza datele, trebuie să consumăm API-ul GitHub. Pentru această parte, cel mai bine este să fii familiarizat cu GraphQL și v4 a API-ului GitHub.
De asemenea, trebuie să creăm un token de acces GitHub. Domeniile minime necesare pentru aceasta sunt:
Acesta va genera un token și putem trimite cereri către GitHub cu acesta.
Acum să revenim la codul nostru.
Avem două noi dependențe în package.json
:
-
"axios": "^0.18.0"
este un client HTTP, așa că putem face solicitări către API-ul GitHub -
"cron": "^1.7.0"
este un programator de job cron
Ca de obicei, rulați npm install
sau yarn
după adăugarea dependențelor.
Vom avea nevoie și de două proprietăți noi în config.js
:
-
"githubEndpoint": "https://api.github.com/graphql"
-
"githubAccessToken": process.env.GITHUB_ACCESS_TOKEN
(va trebui să setați variabila de mediuGITHUB_ACCESS_TOKEN
cu propriul simbol de acces personal)
Creați un fișier nou în folderul controller
cu numele cron.controller.js
. Pur și simplu va apela metoda updateResositories
a repository.controller.js
la ore programate:
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'); };
Modificările finale pentru această parte vor fi în repository.controller.js
. Pentru concizie, îl vom proiecta pentru a actualiza toate depozitele simultan. Dar dacă aveți un număr mare de depozite, este posibil să depășiți limitările de resurse ale API-ului GitHub. Dacă acesta este cazul, va trebui să modificați acest lucru pentru a rula în loturi limitate, repartizate în timp.
Implementarea totală a funcționalității de actualizare va arăta astfel:
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'); }); };
În cele din urmă, vom apela punctul final și vom actualiza modelul de depozit.
Funcția getLatestRelease
va genera o interogare GraphQL și va apela API-ul GitHub. Răspunsul de la acea solicitare va fi apoi procesat în funcția 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); }); }
După al doilea comit, vom fi implementat un programator cron pentru a obține actualizări zilnice din depozitele noastre GitHub.
Aproape am terminat cu partea din spate. Dar ultimul pas ar trebui făcut după implementarea front-end-ului, așa că îl vom acoperi în următorul articol.
Implementarea nodului static Site Generator Back End la Heroku
În acest pas, vom implementa aplicația noastră în Heroku, așa că va trebui să configurați un cont cu ei dacă nu aveți deja unul. Dacă ne legăm contul Heroku la GitHub, ne va fi mult mai ușor să avem o implementare continuă. În acest scop, îmi găzduiesc proiectul pe GitHub.
După ce vă conectați la contul Heroku, adăugați o nouă aplicație din tabloul de bord:
Dați-i un nume unic:
Veți fi redirecționat către o secțiune de implementare. Selectați GitHub ca metodă de implementare, căutați depozitul dvs., apoi faceți clic pe butonul „Conectați”:
Pentru simplitate, puteți activa implementările automate. Se va implementa ori de câte ori împingeți un commit în depozitul dvs. GitHub:
Acum trebuie să adăugăm MongoDB ca resursă. Accesați fila Resurse și faceți clic pe „Găsiți mai multe suplimente”. (Eu personal folosesc mLab mongoDB.)
Instalați-l și introduceți numele aplicației dvs. în caseta de introducere „Aplicație pentru furnizarea către”:
În cele din urmă, trebuie să creăm un fișier numit Procfile
la nivelul rădăcină al proiectului nostru, care specifică comenzile care sunt executate de aplicație atunci când Heroku o pornește.
Procfile
nostru este la fel de simplu ca acesta:
web: node index.js
Creați fișierul și confirmați-l. Odată ce împingeți comanda, Heroku va implementa automat aplicația dvs., care va fi accesibilă ca https://[YOUR_UNIQUE_APP_NAME].herokuapp.com/
.
Pentru a verifica dacă funcționează, putem trimite aceleași solicitări pe care le-am trimis către localhost
.
Node.js, Express, MongoDB, Cron și Heroku: Suntem la jumătatea drumului!
După al treilea nostru comit, așa va arăta repo-ul nostru.
Până acum, am implementat API-ul REST bazat pe Node.js/Express în back-end-ul nostru, actualizatorul care consumă API-ul GitHub și un job cron pentru a-l activa. Apoi am implementat back-end-ul nostru, care ulterior va furniza date pentru generatorul nostru de conținut web static folosind Heroku cu un cârlig pentru integrare continuă. Acum sunteți gata pentru a doua parte, în care implementăm front-end-ul și completăm aplicația!