Back End: Używanie Gatsby.js i Node.js do statycznych aktualizacji witryn
Opublikowany: 2022-03-11W 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:
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:
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:
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:
Nadaj mu unikalną nazwę:
Zostaniesz przekierowany do sekcji wdrażania. Wybierz GitHub jako metodę wdrażania, wyszukaj swoje repozytorium, a następnie kliknij przycisk „Połącz”:
Dla uproszczenia możesz włączyć automatyczne wdrożenia. Zostanie wdrożony za każdym razem, gdy wypchniesz zatwierdzenie do swojego repozytorium GitHub:
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.)
Zainstaluj go i wprowadź nazwę swojej aplikacji w polu wprowadzania „Aplikacja do obsługi”:
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ę!