Das Backend: Verwenden von Gatsby.js und Node.js für statische Site-Updates

Veröffentlicht: 2022-03-11

In dieser Artikelserie entwickeln wir den Prototyp einer Website mit statischem Inhalt. Es wird täglich aktualisierte, einfache statische HTML-Seiten für beliebte GitHub-Repositories generieren, um ihre neuesten Veröffentlichungen zu verfolgen. Frameworks zur Generierung statischer Webseiten haben großartige Funktionen, um dies zu erreichen – wir verwenden Gatsby.js, eines der beliebtesten.

In Gatsby gibt es viele Möglichkeiten, Daten für ein Frontend zu sammeln, ohne dass ein Backend (serverlos), Headless-CMS-Plattformen und Gatsby-Quell-Plugins darunter sind. Aber wir werden ein Backend implementieren, um grundlegende Informationen über GitHub-Repositories und ihre neuesten Versionen zu speichern. Somit haben wir die volle Kontrolle sowohl über unser Backend als auch über unser Frontend.

Außerdem werde ich eine Reihe von Tools behandeln, um ein tägliches Update Ihrer Anwendung auszulösen. Sie können es auch manuell auslösen oder wenn ein bestimmtes Ereignis eintritt.

Unsere Front-End-Anwendung wird auf Netlify ausgeführt, und die Back-End-Anwendung wird auf Heroku mit einem kostenlosen Plan arbeiten. Es schläft regelmäßig: „Wenn jemand auf die App zugreift, weckt der Dyno-Manager automatisch den Web-Dyno auf, um den Web-Prozesstyp auszuführen.“ Wir können es also über AWS Lambda und AWS CloudWatch aufwecken. Zum jetzigen Zeitpunkt ist dies die kostengünstigste Möglichkeit, einen Prototyp rund um die Uhr online zu haben.

Unser Beispiel für eine statische Node-Website: Was Sie erwartet

Damit sich diese Artikel auf ein Thema konzentrieren, werde ich Authentifizierung, Validierung, Skalierbarkeit oder andere allgemeine Themen nicht behandeln. Der Codierungsteil dieses Artikels wird so einfach wie möglich sein. Wichtiger sind die Struktur des Projekts und die Verwendung der richtigen Werkzeuge.

In diesem ersten Teil der Serie werden wir unsere Back-End-Anwendung entwickeln und bereitstellen. Im zweiten Teil werden wir unsere Front-End-Anwendung entwickeln und bereitstellen und tägliche Builds auslösen.

Das Node.js-Backend

Die Back-End-Anwendung wird in Node.js geschrieben (nicht obligatorisch, aber der Einfachheit halber) und die gesamte Kommunikation erfolgt über REST-APIs. Wir werden in diesem Projekt keine Daten vom Frontend sammeln. (Wenn Sie daran interessiert sind, werfen Sie einen Blick auf Gatsby Forms.)

Zuerst werden wir mit der Implementierung eines einfachen REST-API-Backends beginnen, das die CRUD-Operationen der Repository-Sammlung in unserer MongoDB verfügbar macht. Dann planen wir einen Cron-Job, der GitHub API v4 (GraphQL) nutzt, um Dokumente in dieser Sammlung zu aktualisieren. Dann werden wir all dies in der Heroku-Cloud bereitstellen. Schließlich lösen wir am Ende unseres Cron-Jobs einen Neuaufbau des Frontends aus.

Das Gatsby.js-Frontend

Im zweiten Artikel konzentrieren wir uns auf die Implementierung der createPages API. Wir sammeln alle Repositories vom Backend und generieren eine einzelne Homepage, die eine Liste aller Repositories sowie eine Seite für jedes zurückgegebene Repository-Dokument enthält. Dann werden wir unser Frontend für Netlify bereitstellen.

Von AWS Lambda und AWS CloudWatch

Dieser Teil ist nicht obligatorisch, wenn Ihre Anwendung nicht schläft. Andernfalls müssen Sie sicherstellen, dass Ihr Backend zum Zeitpunkt der Aktualisierung der Repositories betriebsbereit ist. Als Lösung können Sie 10 Minuten vor Ihrem täglichen Update einen Cron-Zeitplan auf AWS CloudWatch erstellen und ihn als Trigger an Ihre GET Methode in AWS Lambda binden. Durch den Zugriff auf die Back-End-Anwendung wird die Heroku-Instanz aktiviert. Weitere Details finden Sie am Ende des zweiten Artikels.

Hier ist die Architektur, die wir implementieren werden:

Architekturdiagramm, das zeigt, wie AWS Lambda & CloudWatch das Node.js-Back-End pingen, das tägliche Updates erhält, indem es die GitHub-API verwendet, und dann das Gatsby-basierte Front-End erstellt, das Back-End-APIs verwendet, um seine statischen Seiten zu aktualisieren und für Netlify bereitzustellen. Das Back-End wird auch mit einem kostenlosen Plan für Heroku bereitgestellt.

Annahmen

Ich gehe davon aus, dass die Leser dieses Artikels über Kenntnisse in den folgenden Bereichen verfügen:

  • HTML
  • CSS
  • JavaScript
  • REST-APIs
  • MongoDB
  • Git
  • Node.js

Es ist auch gut, wenn Sie wissen:

  • Express.js
  • Mungo
  • GitHub-API v4 (GraphQL)
  • Heroku, AWS oder jede andere Cloud-Plattform
  • Reagieren

Lassen Sie uns in die Implementierung des Backends eintauchen. Wir werden es in zwei Aufgaben aufteilen. Der erste bereitet REST-API-Endpunkte vor und bindet sie an unsere Repository-Sammlung. Die zweite ist die Implementierung eines Cron-Jobs, der die GitHub-API nutzt und die Sammlung aktualisiert.

Entwicklung des Node.js Static Site Generator Backend, Schritt 1: Eine einfache REST-API

Wir werden Express für unser Webanwendungs-Framework und Mongoose für unsere MongoDB-Verbindung verwenden. Wenn Sie mit Express und Mongoose vertraut sind, können Sie möglicherweise mit Schritt 2 fortfahren.

(Wenn Sie andererseits mehr Vertrautheit mit Express benötigen, können Sie sich den offiziellen Express-Starterleitfaden ansehen; wenn Sie Mongoose nicht kennen, sollte der offizielle Mongoose-Starterleitfaden hilfreich sein.)

Projektstruktur

Die Datei-/Ordnerhierarchie unseres Projekts wird einfach sein:

Eine Ordnerliste des Projektstammverzeichnisses mit den Ordnern „config“, „controller“, „model“ und „node_modules“ sowie einigen standardmäßigen Stammdateien wie „index.js“ und „package.json“. Die Dateien der ersten drei Ordner folgen der Namenskonvention, den Ordnernamen in jedem Dateinamen innerhalb eines bestimmten Ordners zu wiederholen.

Ausführlicher:

  • env.config.js ist die Konfigurationsdatei für Umgebungsvariablen
  • routes.config.js dient zum Zuordnen von Rest-Endpunkten
  • repository.controller.js enthält Methoden, um an unserem Repository-Modell zu arbeiten
  • repository.model.js enthält das MongoDB-Schema von Repository- und CRUD-Vorgängen
  • index.js ist eine Initialisierungsklasse
  • package.json enthält Abhängigkeiten und Projekteigenschaften

Implementierung

Führen Sie npm install (oder yarn , wenn Sie Yarn installiert haben) aus, nachdem Sie diese Abhängigkeiten zu 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" } // ... }

Unsere Datei env.config.js hat vorerst nur port , environment ( dev oder prod ) und mongoDbUri Eigenschaften:

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

routes.config.js enthält Request-Mappings und ruft die entsprechende Methode unseres Controllers auf:

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

Die Datei repository.controller.js ist unsere Serviceschicht. Seine Aufgabe ist es, die entsprechende Methode unseres Repository-Modells aufzurufen:

 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 verarbeitet die MongoDb-Verbindung und die CRUD-Operationen für das Repository-Modell. Die Felder des Modells sind:

  • owner : Der Eigentümer des Repositorys (Firma oder Benutzer)
  • name : Der Repository-Name
  • createdAt : Das Erstellungsdatum des letzten Releases
  • resourcePath : Der letzte Freigabepfad
  • tagName : Das letzte Release-Tag
  • releaseDescription : Versionshinweise
  • homepageUrl : Die Home-URL des Projekts
  • repositoryDescription : Die Repository-Beschreibung
  • avatarUrl : Die Avatar-URL des Projektinhabers
 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 }); };

Das haben wir nach unserem ersten Commit: Eine MongoDB-Verbindung und unsere REST-Operationen.

Wir können unsere Anwendung mit dem folgenden Befehl ausführen:

 node index.js

Testen

Senden Sie zum Testen Anfragen an localhost:3000 (z. B. mit Postman oder cURL):

Repository einfügen (nur Pflichtfelder)

Beitrag: http://localhost:3000/repositories

Körper:

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

Holen Sie sich Repositories

Holen Sie sich: http://localhost:3000/repositories

Per ID abrufen

Holen Sie sich: http://localhost:3000/repositories/:id

Patch nach ID

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

Körper:

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

Wenn das funktioniert, ist es an der Zeit, Updates zu automatisieren.

Entwickeln des Node.js Static Site Generator Backend, Schritt 2: Ein Cron-Job zum Aktualisieren von Repository-Versionen

In diesem Teil konfigurieren wir einen einfachen Cron-Job (der um Mitternacht UTC startet), um die GitHub-Repositories zu aktualisieren, die wir in unsere Datenbank eingefügt haben. Wir haben in unserem obigen Beispiel nur die Parameter owner und name hinzugefügt, aber diese beiden Felder reichen aus, um auf allgemeine Informationen zu einem bestimmten Repository zuzugreifen.

Um unsere Daten zu aktualisieren, müssen wir die GitHub-API nutzen. Für diesen Teil ist es am besten, mit GraphQL und v4 der GitHub-API vertraut zu sein.

Wir müssen auch ein GitHub-Zugriffstoken erstellen. Die dafür erforderlichen Mindestumfänge sind:

Die GitHub-Tokenbereiche, die wir benötigen, sind repo:status, repo_deployment, public_repo, read:org und read:user.

Dadurch wird ein Token generiert, mit dem wir Anfragen an GitHub senden können.

Kommen wir nun zurück zu unserem Code.

Wir haben zwei neue Abhängigkeiten in package.json :

  • "axios": "^0.18.0" ist ein HTTP-Client, sodass wir Anfragen an die GitHub-API stellen können
  • "cron": "^1.7.0" ist ein Cron-Job-Scheduler

Führen Sie nach dem Hinzufügen von Abhängigkeiten wie gewohnt npm install oder „ yarn “ aus.

Wir brauchen auch zwei neue Eigenschaften in config.js :

  • "githubEndpoint": "https://api.github.com/graphql"
  • "githubAccessToken": process.env.GITHUB_ACCESS_TOKEN (Sie müssen die Umgebungsvariable GITHUB_ACCESS_TOKEN mit Ihrem eigenen persönlichen Zugriffstoken festlegen)

Erstellen Sie im controller -Ordner eine neue Datei mit dem Namen cron.controller.js . Es ruft einfach zu festgelegten Zeiten die updateResositories Methode von repository.controller.js auf:

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

Die letzten Änderungen für diesen Teil befinden sich in repository.controller.js . Der Kürze halber werden wir es so entwerfen, dass alle Repositories auf einmal aktualisiert werden. Wenn Sie jedoch über eine große Anzahl von Repositories verfügen, überschreiten Sie möglicherweise die Ressourcenbeschränkungen der API von GitHub. Wenn dies der Fall ist, müssen Sie dies so ändern, dass es in begrenzten Stapeln ausgeführt wird, die über die Zeit verteilt sind.

Die All-at-once-Implementierung der Update-Funktionalität sieht folgendermaßen aus:

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

Schließlich rufen wir den Endpunkt auf und aktualisieren das Repository-Modell.

Die getLatestRelease Funktion generiert eine GraphQL-Abfrage und ruft die GitHub-API auf. Die Antwort auf diese Anfrage wird dann in der updateDatabase Funktion verarbeitet.

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

Nach unserem zweiten Commit haben wir einen Cron-Scheduler implementiert, um tägliche Updates aus unseren GitHub-Repositories zu erhalten.

Wir sind fast fertig mit dem Backend. Der letzte Schritt dorthin sollte jedoch nach der Implementierung des Frontends erfolgen, daher behandeln wir ihn im nächsten Artikel.

Bereitstellen des Node Static Site Generator-Backends für Heroku

In diesem Schritt stellen wir unsere Anwendung für Heroku bereit, daher müssen Sie ein Konto bei Heroku einrichten, falls Sie noch keines haben. Wenn wir unser Heroku-Konto an GitHub binden, wird es für uns viel einfacher, eine kontinuierliche Bereitstellung zu haben. Zu diesem Zweck hoste ich mein Projekt auf GitHub.

Nachdem Sie sich bei Ihrem Heroku-Konto angemeldet haben, fügen Sie eine neue App über das Dashboard hinzu:

Wählen Sie „Neue App erstellen“ aus dem Menü „Neu“ im Heroku-Dashboard.

Geben Sie ihm einen eindeutigen Namen:

Benennen Sie Ihre App in Heroku.

Sie werden zu einem Bereitstellungsbereich weitergeleitet. Wählen Sie GitHub als Bereitstellungsmethode, suchen Sie nach Ihrem Repository und klicken Sie dann auf die Schaltfläche „Verbinden“:

Verknüpfen Ihres neuen GitHub-Repos mit Ihrer Heroku-App.

Der Einfachheit halber können Sie automatische Bereitstellungen aktivieren. Es wird immer dann bereitgestellt, wenn Sie ein Commit an Ihr GitHub-Repository senden:

Automatische Bereitstellungen in Heroku aktivieren.

Jetzt müssen wir MongoDB als Ressource hinzufügen. Gehen Sie zur Registerkarte Ressourcen und klicken Sie auf „Weitere Add-Ons finden“. (Ich persönlich verwende mLab mongoDB.)

Hinzufügen einer MongoDB-Ressource zu Ihrer Heroku-App.

Installieren Sie es und geben Sie den Namen Ihrer App in das Eingabefeld „App to provision to“ ein:

Die Bereitstellungsseite für das mLab MongoDB-Add-on in Heroku.

Schließlich müssen wir eine Datei namens Procfile auf der Stammebene unseres Projekts erstellen, die die Befehle angibt, die von der App ausgeführt werden, wenn Heroku sie startet.

Unser Procfile ist so einfach:

 web: node index.js

Erstellen Sie die Datei und übertragen Sie sie. Sobald Sie das Commit übertragen haben, stellt Heroku Ihre Anwendung automatisch bereit, auf die unter https://[YOUR_UNIQUE_APP_NAME].herokuapp.com/ .

Um zu überprüfen, ob es funktioniert, können wir dieselben Anfragen senden, die wir an localhost gesendet haben.

Node.js, Express, MongoDB, Cron und Heroku: Wir sind auf halbem Weg!

Nach unserem dritten Commit wird unser Repo so aussehen.

Bisher haben wir die auf Node.js/Express basierende REST-API in unserem Backend implementiert, den Updater, der die API von GitHub nutzt, und einen Cron-Job, um ihn zu aktivieren. Dann haben wir unser Backend bereitgestellt, das später Daten für unseren statischen Webinhaltsgenerator mit Heroku mit einem Hook für die kontinuierliche Integration bereitstellen wird. Jetzt sind Sie bereit für den zweiten Teil, in dem wir das Frontend implementieren und die App fertig stellen!

Siehe auch: Die 10 häufigsten Fehler, die Node.js-Entwickler machen