Le back-end : utiliser Gatsby.js et Node.js pour les mises à jour statiques du site

Publié: 2022-03-11

Dans cette série d'articles, nous développerons un prototype de site Web à contenu statique. Il générera des pages HTML statiques simples et mises à jour quotidiennement pour les référentiels GitHub populaires afin de suivre leurs dernières versions. Les frameworks de génération de pages Web statiques ont d'excellentes fonctionnalités pour y parvenir - nous utiliserons Gatsby.js, l'un des plus populaires.

Dans Gatsby, il existe de nombreuses façons de collecter des données pour un front-end sans avoir de back-end (sans serveur), des plates-formes Headless CMS et des plugins source Gatsby parmi eux. Mais nous allons implémenter un back-end pour stocker des informations de base sur les référentiels GitHub et leurs dernières versions. Ainsi, nous aurons un contrôle total sur notre back-end et notre front-end.

De plus, je couvrirai un ensemble d'outils pour déclencher une mise à jour quotidienne de votre application. Vous pouvez également le déclencher manuellement ou chaque fois qu'un événement spécifique se produit.

Notre application frontale fonctionnera sur Netlify et l'application principale fonctionnera sur Heroku en utilisant un plan gratuit. Il dormira périodiquement : "Lorsque quelqu'un accède à l'application, le gestionnaire de dyno réveille automatiquement le dyno Web pour exécuter le type de processus Web." Ainsi, nous pouvons le réveiller via AWS Lambda et AWS CloudWatch. Au moment d'écrire ces lignes, c'est le moyen le plus rentable d'avoir un prototype en ligne 24h/24 et 7j/7.

Notre exemple de site Web statique Node : à quoi s'attendre

Pour garder ces articles concentrés sur un sujet, je ne couvrirai pas l'authentification, la validation, l'évolutivité ou d'autres sujets généraux. La partie codage de cet article sera aussi simple que possible. La structure du projet et l'utilisation du bon ensemble d'outils sont plus importantes.

Dans cette première partie de la série, nous allons développer et déployer notre application back-end. Dans la seconde partie, nous allons développer et déployer notre application front-end, et déclencher des builds quotidiens.

Le back-end Node.js

L'application back-end sera écrite en Node.js (pas obligatoire, mais pour plus de simplicité) et toutes les communications se feront via des API REST. Nous ne collecterons pas de données du front-end dans ce projet. (Si cela vous intéresse, jetez un œil à Gatsby Forms.)

Tout d'abord, nous commencerons par implémenter un simple back-end d'API REST qui expose les opérations CRUD de la collection de référentiels dans notre MongoDB. Ensuite, nous planifierons une tâche cron qui consomme l'API GitHub v4 (GraphQL) afin de mettre à jour les documents de cette collection. Ensuite, nous déploierons tout cela dans le cloud Heroku. Enfin, nous déclencherons une reconstruction du front-end à la fin de notre tâche cron.

Le frontal Gatsby.js

Dans le deuxième article, nous nous concentrerons sur l'implémentation de l'API createPages . Nous rassemblerons tous les référentiels du back-end et générerons une page d'accueil unique contenant une liste de tous les référentiels, ainsi qu'une page pour chaque document de référentiel renvoyé. Ensuite, nous déploierons notre frontal sur Netlify.

Depuis AWS Lambda et AWS CloudWatch

Cette partie n'est pas obligatoire si votre application ne dort pas. Sinon, vous devez vous assurer que votre serveur principal est opérationnel au moment de la mise à jour des référentiels. Comme solution, vous pouvez créer une planification cron sur AWS CloudWatch 10 minutes avant votre mise à jour quotidienne et la lier en tant que déclencheur à votre méthode GET dans AWS Lambda. L'accès à l'application principale réveillera l'instance Heroku. Plus de détails seront à la fin du deuxième article.

Voici l'architecture que nous allons implémenter :

Diagramme d'architecture montrant qu'AWS Lambda et CloudWatch envoient un ping au back-end Node.js, qui obtient des mises à jour quotidiennes en utilisant l'API GitHub, puis crée le frontal basé sur Gatsby, qui utilise des API back-end pour mettre à jour ses pages statiques et se déploie sur Netlify. Le back-end se déploie également sur Heroku avec un plan gratuit.

Hypothèses

Je suppose que les lecteurs de cet article ont des connaissances dans les domaines suivants :

  • HTML
  • CSS
  • Javascript
  • API REST
  • MongoDB
  • Gite
  • Node.js

C'est bien aussi si vous savez :

  • Express.js
  • Mangouste
  • API GitHub v4 (GraphQL)
  • Heroku, AWS ou toute autre plate-forme cloud
  • Réagir

Plongeons-nous dans l'implémentation du back-end. Nous allons le diviser en deux tâches. Le premier consiste à préparer les points de terminaison de l'API REST et à les lier à notre collection de référentiels. La seconde consiste à implémenter une tâche cron qui utilise l'API GitHub et met à jour la collection.

Développement du back-end du générateur de site statique Node.js, étape 1 : une API REST simple

Nous utiliserons Express pour notre framework d'application Web et Mongoose pour notre connexion MongoDB. Si vous connaissez Express et Mongoose, vous pourrez peut-être passer à l'étape 2.

(D'un autre côté, si vous avez besoin de plus de familiarité avec Express, vous pouvez consulter le guide de démarrage officiel d'Express ; si vous n'êtes pas au courant de Mongoose, le guide de démarrage officiel de Mongoose devrait vous être utile.)

Structure du projet

La hiérarchie des fichiers/dossiers de notre projet sera simple :

Une liste de dossiers de la racine du projet, montrant les dossiers config, controller, model et node_modules, ainsi que quelques fichiers racine standard comme index.js et package.json. Les fichiers des trois premiers dossiers suivent la convention de dénomination consistant à répéter le nom du dossier dans chaque nom de fichier d'un dossier donné.

Plus en détail:

  • env.config.js est le fichier de configuration des variables d'environnement
  • routes.config.js sert à mapper les points de terminaison de repos
  • repository.controller.js contient des méthodes pour travailler sur notre modèle de référentiel
  • repository.model.js contient le schéma MongoDB du référentiel et des opérations CRUD
  • index.js est une classe d'initialisation
  • package.json contient les dépendances et les propriétés du projet

Mise en œuvre

Exécutez npm install (ou yarn , si vous avez installé Yarn) après avoir ajouté ces dépendances à 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" } // ... }

Notre fichier env.config.js n'a pour l'instant que les propriétés port , environment ( dev ou prod ) et mongoDbUri :

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

routes.config.js contient des mappages de requêtes et appellera la méthode correspondante de notre contrôleur :

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

Le fichier repository.controller.js est notre couche de service. Sa responsabilité est d'appeler la méthode correspondante de notre modèle de référentiel :

 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 gère la connexion MongoDb et les opérations CRUD pour le modèle de référentiel. Les champs du modèle sont :

  • owner : Le propriétaire du référentiel (société ou utilisateur)
  • name : Le nom du dépôt
  • createdAt : la date de création de la dernière version
  • resourcePath : le dernier chemin de version
  • tagName : la dernière balise de version
  • releaseDescription : Notes de version
  • homepageUrl : l'URL d'accueil du projet
  • repositoryDescription : la description du référentiel
  • avatarUrl : URL de l'avatar du propriétaire du projet
 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 }); };

Voici ce que nous avons après notre premier commit : une connexion MongoDB et nos opérations REST.

Nous pouvons exécuter notre application avec la commande suivante :

 node index.js

Essai

Pour les tests, envoyez des requêtes à localhost:3000 (en utilisant par exemple Postman ou cURL) :

Insérer un référentiel (uniquement les champs obligatoires)

Message : http://localhost:3000/repositories

Corps:

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

Obtenir des référentiels

Obtenez : http://localhost:3000/dépôts

Obtenir par ID

Obtenez : http://localhost:3000/repositories/:id

Correctif par ID

Correctif : http://localhost:3000/repositories/:id

Corps:

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

Avec cela, il est temps d'automatiser les mises à jour.

Développement du back-end du générateur de site statique Node.js, étape 2 : une tâche cron pour mettre à jour les versions du référentiel

Dans cette partie, nous allons configurer une simple tâche cron (qui démarrera à minuit UTC) pour mettre à jour les référentiels GitHub que nous avons insérés dans notre base de données. Nous n'avons ajouté que les paramètres de owner et de name uniquement dans notre exemple ci-dessus, mais ces deux champs nous suffisent pour accéder aux informations générales sur un référentiel donné.

Afin de mettre à jour nos données, nous devons consommer l'API GitHub. Pour cette partie, il est préférable de se familiariser avec GraphQL et la v4 de l'API GitHub.

Nous devons également créer un jeton d'accès GitHub. Les portées minimales requises pour cela sont :

Les étendues de jetons GitHub dont nous avons besoin sont repo:status, repo_deployment, public_repo, read:org et read:user.

Cela générera un jeton, et nous pourrons envoyer des requêtes à GitHub avec.

Revenons maintenant à notre code.

Nous avons deux nouvelles dépendances dans package.json :

  • "axios": "^0.18.0" est un client HTTP, nous pouvons donc faire des requêtes à l'API GitHub
  • "cron": "^1.7.0" est un planificateur de tâches cron

Comme d'habitude, exécutez npm install ou yarn après avoir ajouté des dépendances.

Nous aurons également besoin de deux nouvelles propriétés dans config.js :

  • "githubEndpoint": "https://api.github.com/graphql"
  • "githubAccessToken": process.env.GITHUB_ACCESS_TOKEN (vous devrez définir la variable d'environnement GITHUB_ACCESS_TOKEN avec votre propre jeton d'accès personnel)

Créez un nouveau fichier sous le dossier du controller avec le nom cron.controller.js . Il appellera simplement la méthode updateResositories de repository.controller.js à des heures planifiées :

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

Les modifications finales pour cette partie seront dans repository.controller.js . Par souci de concision, nous allons le concevoir pour mettre à jour tous les référentiels à la fois. Mais si vous avez un grand nombre de référentiels, vous pouvez dépasser les limitations de ressources de l'API de GitHub. Si tel est le cas, vous devrez le modifier pour qu'il s'exécute par lots limités, étalés dans le temps.

L'implémentation complète de la fonctionnalité de mise à jour ressemblera à ceci :

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

Enfin, nous appellerons le point de terminaison et mettrons à jour le modèle de référentiel.

La fonction getLatestRelease générera une requête GraphQL et appellera l'API GitHub. La réponse de cette requête sera ensuite traitée dans la fonction 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); }); }

Après notre deuxième commit, nous aurons implémenté un planificateur cron pour obtenir des mises à jour quotidiennes de nos référentiels GitHub.

Nous avons presque terminé avec le back-end. Mais la dernière étape doit être effectuée après la mise en œuvre du front-end, nous en parlerons donc dans le prochain article.

Déploiement du back-end du générateur de site statique de nœud sur Heroku

Dans cette étape, nous allons déployer notre application sur Heroku, vous devrez donc créer un compte avec eux si vous n'en avez pas déjà un. Si nous lions notre compte Heroku à GitHub, il nous sera beaucoup plus facile d'avoir un déploiement continu. À cette fin, j'héberge mon projet sur GitHub.

Après vous être connecté à votre compte Heroku, ajoutez une nouvelle application depuis le tableau de bord :

Choisissez "Créer une nouvelle application" dans le menu Nouveau du tableau de bord Heroku.

Donnez-lui un nom unique :

Nommez votre application dans Heroku.

Vous serez redirigé vers une section de déploiement. Sélectionnez GitHub comme méthode de déploiement, recherchez votre référentiel, puis cliquez sur le bouton "Se connecter" :

Associez votre nouveau référentiel GitHub à votre application Heroku.

Pour plus de simplicité, vous pouvez activer les déploiements automatiques. Il se déploiera chaque fois que vous pousserez un commit vers votre référentiel GitHub :

Activation des déploiements automatiques dans Heroku.

Nous devons maintenant ajouter MongoDB en tant que ressource. Allez dans l'onglet Ressources et cliquez sur "Trouver plus de modules complémentaires". (J'utilise personnellement mLab mongoDB.)

Ajout d'une ressource MongoDB à votre application Heroku.

Installez-le et entrez le nom de votre application dans la zone de saisie « App to provision to » :

La page de mise à disposition du module complémentaire mLab MongoDB dans Heroku.

Enfin, nous devons créer un fichier nommé Procfile au niveau racine de notre projet, qui spécifie les commandes exécutées par l'application lorsque Heroku la démarre.

Notre Procfile est aussi simple que cela :

 web: node index.js

Créez le fichier et validez-le. Une fois que vous avez poussé le commit, Heroku déploiera automatiquement votre application, qui sera accessible sous https://[YOUR_UNIQUE_APP_NAME].herokuapp.com/ .

Pour vérifier si cela fonctionne, nous pouvons envoyer les mêmes requêtes que nous avons envoyées à localhost .

Node.js, Express, MongoDB, Cron et Heroku : nous sommes à mi-chemin !

Après notre troisième commit, voici à quoi ressemblera notre dépôt.

Jusqu'à présent, nous avons implémenté l'API REST basée sur Node.js/Express sur notre back-end, le programme de mise à jour qui utilise l'API de GitHub et une tâche cron pour l'activer. Ensuite, nous avons déployé notre back-end qui fournira plus tard des données pour notre générateur de contenu Web statique utilisant Heroku avec un crochet pour l'intégration continue. Vous êtes maintenant prêt pour la deuxième partie, où nous implémentons le front-end et complétons l'application !

En relation: Les 10 erreurs les plus courantes commises par les développeurs Node.js