Back End: Używanie Gatsby.js i Node.js do statycznych aktualizacji witryn

Opublikowany: 2022-03-11

W tej serii artykułów opracujemy prototyp strony zawierającej statyczne treści. Będzie generować codziennie aktualizowane, proste statyczne strony HTML dla popularnych repozytoriów GitHub, aby śledzić ich najnowsze wydania. Struktury generowania statycznych stron internetowych mają świetne funkcje, aby to osiągnąć — użyjemy Gatsby.js, jednego z najpopularniejszych.

W Gatsby istnieje wiele sposobów zbierania danych dla frontonu bez posiadania zaplecza (bez serwera), platform Headless CMS i wtyczek źródłowych Gatsby. Ale zaimplementujemy backend do przechowywania podstawowych informacji o repozytoriach GitHub i ich najnowszych wydaniach. W ten sposób będziemy mieć pełną kontrolę zarówno nad naszym backendem, jak i frontendem.

Omówię również zestaw narzędzi do uruchamiania codziennej aktualizacji Twojej aplikacji. Możesz także uruchomić go ręcznie lub za każdym razem, gdy wydarzy się jakieś określone zdarzenie.

Nasza aplikacja frontendowa będzie działać na Netlify, a aplikacja backendowa będzie działać na Heroku, korzystając z darmowego planu. Będzie okresowo spać: „Kiedy ktoś uzyska dostęp do aplikacji, menedżer hamowni automatycznie obudzi hamownię internetową, aby uruchomić typ procesu sieciowego”. Możemy więc go obudzić za pomocą AWS Lambda i AWS CloudWatch. W chwili pisania tego tekstu jest to najbardziej opłacalny sposób na posiadanie prototypu online 24/7.

Przykład naszej statycznej strony internetowej węzła: czego się spodziewać

Aby te artykuły koncentrowały się na jednym temacie, nie będę omawiać uwierzytelniania, walidacji, skalowalności ani innych ogólnych tematów. Kodowanie części tego artykułu będzie tak proste, jak to tylko możliwe. Ważniejsza jest struktura projektu i wykorzystanie odpowiedniego zestawu narzędzi.

W pierwszej części serii opracujemy i wdrożymy naszą aplikację back-end. W drugiej części opracujemy i wdrożymy naszą aplikację front-endową oraz uruchomimy codzienne kompilacje.

Back End Node.js

Aplikacja zaplecza zostanie napisana w Node.js (nie jest to obowiązkowe, ale dla uproszczenia), a cała komunikacja będzie odbywać się przez interfejsy API REST. W tym projekcie nie będziemy zbierać danych z frontendu. (Jeśli jesteś tym zainteresowany, spójrz na Gatsby Forms.)

Najpierw zaczniemy od zaimplementowania prostego zaplecza API REST, które udostępnia operacje CRUD kolekcji repozytorium w naszej MongoDB. Następnie zaplanujemy zadanie cron, które wykorzystuje GitHub API v4 (GraphQL) w celu aktualizacji dokumentów w tej kolekcji. Następnie wdrożymy to wszystko w chmurze Heroku. Na koniec uruchomimy przebudowę frontendu pod koniec naszej pracy z cronem.

Interfejs Gatsby.js

W drugim artykule skupimy się na implementacji API createPages . Zbierzemy wszystkie repozytoria z zaplecza i wygenerujemy jedną stronę główną zawierającą listę wszystkich repozytoriów oraz stronę dla każdego zwróconego dokumentu repozytorium. Następnie wdrożymy nasz frontend do Netlify.

Z AWS Lambda i AWS CloudWatch

Ta część nie jest obowiązkowa, jeśli aplikacja nie śpi. W przeciwnym razie musisz mieć pewność, że Twój backend jest gotowy do pracy w momencie aktualizacji repozytoriów. Jako rozwiązanie możesz utworzyć harmonogram crona w AWS CloudWatch 10 minut przed codzienną aktualizacją i powiązać go jako wyzwalacz z metodą GET w AWS Lambda. Uzyskanie dostępu do aplikacji zaplecza spowoduje wybudzenie instancji Heroku. Więcej szczegółów na końcu drugiego artykułu.

Oto architektura, którą wdrożymy:

Diagram architektury przedstawiający AWS Lambda i CloudWatch pingujący backend Node.js, który otrzymuje codzienne aktualizacje, korzystając z interfejsu API GitHub, a następnie buduje interfejs oparty na Gatsby, który wykorzystuje interfejsy API backendu do aktualizowania swoich stron statycznych i wdrażania w Netlify. Zaplecze jest również wdrażane w Heroku z bezpłatnym planem.

Założenia

Zakładam, że czytelnicy tego artykułu posiadają wiedzę w następujących obszarach:

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

Dobrze też, jeśli wiesz:

  • Express.js
  • Mangusta
  • GitHub API v4 (GraphQL)
  • Heroku, AWS lub dowolna inna platforma w chmurze
  • Reagować

Zanurzmy się w implementację backendu. Podzielimy to na dwa zadania. Pierwszym z nich jest przygotowanie punktów końcowych REST API i powiązanie ich z naszą kolekcją repozytorium. Drugi to implementacja zadania cron, które wykorzystuje interfejs API GitHub i aktualizuje kolekcję.

Tworzenie zaplecza generatora stron statycznych Node.js, krok 1: prosty interfejs API REST

Użyjemy Express dla naszego frameworka aplikacji internetowych i Mongoose dla naszego połączenia MongoDB. Jeśli znasz Express i Mongoose, możesz przejść do kroku 2.

(Z drugiej strony, jeśli potrzebujesz więcej znajomości Express, możesz zapoznać się z oficjalnym przewodnikiem dla starterów Express; jeśli nie znasz Mongoose, oficjalny przewodnik dla starterów Mongoose powinien być pomocny.)

Struktura projektu

Hierarchia plików/folderów naszego projektu będzie prosta:

Lista folderów katalogu głównego projektu, pokazująca foldery config, controller, model i node_modules, a także kilka standardowych plików głównych, takich jak index.js i package.json. Pliki pierwszych trzech folderów są zgodne z konwencją nazewnictwa polegającą na powtarzaniu nazwy folderu w każdej nazwie pliku w danym folderze.

Bardziej szczegółowo:

  • env.config.js to plik konfiguracyjny zmiennych środowiskowych
  • routes.config.js służy do mapowania punktów końcowych spoczynku
  • repository.controller.js zawiera metody do pracy na naszym modelu repozytorium
  • repository.model.js zawiera schemat MongoDB repozytorium i operacji CRUD
  • index.js to klasa inicjująca
  • package.json zawiera zależności i właściwości projektu

Realizacja

Uruchom npm install (lub yarn , jeśli masz zainstalowany Yarn) po dodaniu tych zależności do 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" } // ... }

Nasz plik env.config.js ma na razie tylko właściwości port , environment ( dev lub prod ) i mongoDbUri :

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

routes.config.js zawiera mapowania żądań i wywoła odpowiednią metodę naszego kontrolera:

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

Plik repository.controller.js to nasza warstwa usługowa. Jego zadaniem jest wywołanie odpowiedniej metody naszego modelu repozytorium:

 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 obsługuje połączenie MongoDb i operacje CRUD dla modelu repozytorium. Pola modelu to:

  • owner : właściciel repozytorium (firma lub użytkownik)
  • name : nazwa repozytorium
  • createdAt : data utworzenia ostatniego wydania
  • resourcePath : ostatnia ścieżka wydania
  • tagName : tag ostatniego wydania
  • releaseDescription : informacje o wydaniu
  • homepageUrl : Domowy adres URL projektu
  • repositoryDescription : opis repozytorium
  • avatarUrl : URL awatara właściciela projektu
 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 }); };

Oto, co mamy po naszym pierwszym zatwierdzeniu: połączenie MongoDB i nasze operacje REST.

Naszą aplikację możemy uruchomić poleceniem:

 node index.js

Testowanie

W celu przetestowania wyślij żądania do localhost:3000 (używając np. Postman lub cURL):

Wstaw repozytorium (tylko pola wymagane)

Post: http://localhost:3000/repositories

Ciało:

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

Pobierz repozytoria

Pobierz: http://localhost:3000/repositories

Pobierz przez ID

Pobierz: http://localhost:3000/repositories/:id

Łata według ID

Poprawka: http://localhost:3000/repositories/:id

Ciało:

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

Gdy to działa, nadszedł czas na automatyzację aktualizacji.

Tworzenie zaplecza generatora stron statycznych Node.js, krok 2: Zadanie Cron do aktualizacji wersji repozytorium

W tej części skonfigurujemy proste zadanie cron (które rozpocznie się o północy UTC), aby zaktualizować repozytoria GitHub, które wstawiliśmy do naszej bazy danych. Dodaliśmy tylko parametry owner i name tylko w naszym przykładzie powyżej, ale te dwa pola wystarczą nam, aby uzyskać dostęp do ogólnych informacji o danym repozytorium.

Aby zaktualizować nasze dane, musimy korzystać z API GitHub. W tej części najlepiej jest zapoznać się z GraphQL i v4 interfejsu API GitHub.

Musimy również utworzyć token dostępu GitHub. Minimalne wymagane zakresy to:

Potrzebne zakresy tokenów GitHub to repo:status, repo_deployment, public_repo, read:org i read:user.

To wygeneruje token, za pomocą którego będziemy mogli wysyłać żądania do GitHub.

Wróćmy teraz do naszego kodu.

Mamy dwie nowe zależności w package.json :

  • "axios": "^0.18.0" to klient HTTP, więc możemy wysyłać żądania do API GitHub
  • "cron": "^1.7.0" to program do planowania zadań crona

Jak zwykle, po dodaniu zależności uruchom npm install lub yarn .

Potrzebujemy również dwóch nowych właściwości w config.js :

  • "githubEndpoint": "https://api.github.com/graphql"
  • "githubAccessToken": process.env.GITHUB_ACCESS_TOKEN (musisz ustawić zmienną środowiskową GITHUB_ACCESS_TOKEN z własnym osobistym tokenem dostępu)

Utwórz nowy plik w folderze controller o nazwie cron.controller.js . Po prostu wywoła metodę updateResositories z repository.controller.js w zaplanowanych godzinach:

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

Ostateczne zmiany dla tej części będą w repository.controller.js . Dla zwięzłości zaprojektujemy go tak, aby aktualizował wszystkie repozytoria jednocześnie. Ale jeśli masz dużą liczbę repozytoriów, możesz przekroczyć ograniczenia zasobów API GitHub. Jeśli tak jest, musisz to zmodyfikować, aby działały w ograniczonych partiach, rozłożonych w czasie.

Całościowa implementacja funkcji aktualizacji będzie wyglądać tak:

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

Na koniec wywołamy punkt końcowy i zaktualizujemy model repozytorium.

Funkcja getLatestRelease wygeneruje zapytanie GraphQL i wywoła API GitHub. Odpowiedź z tego żądania zostanie następnie przetworzona w funkcji 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); }); }

Po naszym drugim zatwierdzeniu wdrożymy harmonogram cron, aby otrzymywać codzienne aktualizacje z naszych repozytoriów GitHub.

Zaplecze prawie skończyliśmy. Ale ostatni krok należy wykonać po wdrożeniu frontendu, więc omówimy go w następnym artykule.

Wdrażanie węzła statycznego generatora stron zaplecza w Heroku

W tym kroku wdrożymy naszą aplikację w Heroku, więc będziesz musiał założyć u nich konto, jeśli jeszcze go nie masz. Jeśli połączymy nasze konto Heroku z GitHub, znacznie łatwiej będzie nam mieć ciągłe wdrażanie. W tym celu hostuję swój projekt na GitHub.

Po zalogowaniu się na swoje konto Heroku dodaj nową aplikację z pulpitu nawigacyjnego:

Wybierając „Utwórz nową aplikację” z menu Nowa na pulpicie Heroku.

Nadaj mu unikalną nazwę:

Nazywanie Twojej aplikacji w Heroku.

Zostaniesz przekierowany do sekcji wdrażania. Wybierz GitHub jako metodę wdrażania, wyszukaj swoje repozytorium, a następnie kliknij przycisk „Połącz”:

Łączenie nowego repozytorium GitHub z aplikacją Heroku.

Dla uproszczenia możesz włączyć automatyczne wdrożenia. Zostanie wdrożony za każdym razem, gdy wypchniesz zatwierdzenie do swojego repozytorium GitHub:

Włączenie automatycznych wdrożeń w Heroku.

Teraz musimy dodać MongoDB jako zasób. Przejdź do zakładki Zasoby i kliknij „Znajdź więcej dodatków”. (Ja osobiście używam mLab mongoDB.)

Dodawanie zasobu MongoDB do aplikacji Heroku.

Zainstaluj go i wprowadź nazwę swojej aplikacji w polu wprowadzania „Aplikacja do obsługi”:

Strona udostępniania dodatku mLab MongoDB w Heroku.

Na koniec musimy utworzyć plik o nazwie Procfile na poziomie głównym naszego projektu, który określa polecenia, które są wykonywane przez aplikację podczas jej uruchamiania.

Nasz Procfile jest tak prosty, jak ten:

 web: node index.js

Utwórz plik i zatwierdź go. Gdy wypchniesz zatwierdzenie, Heroku automatycznie wdroży Twoją aplikację, która będzie dostępna pod https://[YOUR_UNIQUE_APP_NAME].herokuapp.com/ .

Aby sprawdzić, czy działa, możemy wysłać te same żądania, które wysłaliśmy do localhost .

Node.js, Express, MongoDB, Cron i Heroku: jesteśmy w połowie drogi!

Po naszym trzecim commicie tak będzie wyglądało nasze repozytorium.

Do tej pory wdrożyliśmy na naszym zapleczu interfejs API REST oparty na Node.js/Express, aktualizator, który wykorzystuje interfejs API GitHub, oraz zadanie cron do jego aktywacji. Następnie wdrożyliśmy nasz back-end, który później będzie dostarczał dane dla naszego statycznego generatora treści internetowych za pomocą Heroku z hakiem do ciągłej integracji. Teraz jesteś gotowy na drugą część, w której wdrażamy frontend i uzupełniamy aplikację!

Powiązane: 10 najczęstszych błędów popełnianych przez programistów Node.js