Серверная часть: использование Gatsby.js и Node.js для статических обновлений сайта

Опубликовано: 2022-03-11

В этой серии статей мы разработаем прототип веб-сайта со статическим содержимым. Он будет генерировать ежедневно обновляемые простые статические HTML-страницы для популярных репозиториев GitHub, чтобы отслеживать их последние выпуски. Фреймворки создания статических веб-страниц имеют отличные возможности для достижения этой цели — мы будем использовать Gatsby.js, один из самых популярных.

В Gatsby существует множество способов сбора данных для внешнего интерфейса без использования внутреннего (бессерверного), платформ Headless CMS и исходных плагинов Gatsby. Но мы реализуем серверную часть для хранения основной информации о репозиториях GitHub и их последних выпусках. Таким образом, мы будем иметь полный контроль как над нашим бэкендом, так и над интерфейсом.

Кроме того, я расскажу о наборе инструментов для запуска ежедневного обновления вашего приложения. Вы также можете запускать его вручную или всякий раз, когда происходит какое-то конкретное событие.

Наше внешнее приложение будет работать на Netlify, а внутреннее приложение будет работать на Heroku с использованием бесплатного плана. Периодически он будет засыпать: «Когда кто-то обращается к приложению, менеджер динамометра автоматически пробуждает веб-дино для запуска типа веб-процесса». Итак, мы можем разбудить его через AWS Lambda и AWS CloudWatch. На момент написания этой статьи это самый экономичный способ иметь прототип онлайн 24/7.

Пример статического веб-сайта Node: чего ожидать

Чтобы эти статьи были сосредоточены на одной теме, я не буду касаться аутентификации, проверки, масштабируемости или других общих тем. Часть этой статьи, посвященная кодированию, будет максимально простой. Важнее структура проекта и использование правильного набора инструментов.

В этой первой части серии мы разработаем и развернем наше серверное приложение. Во второй части мы разработаем и развернем наше внешнее приложение и будем запускать ежедневные сборки.

Серверная часть Node.js

Серверное приложение будет написано на Node.js (не обязательно, но для простоты), и все коммуникации будут осуществляться через REST API. Мы не будем собирать данные из внешнего интерфейса в этом проекте. (Если вы заинтересованы в этом, взгляните на Gatsby Forms.)

Во-первых, мы начнем с реализации простой серверной части REST API, которая предоставляет операции CRUD коллекции репозитория в нашей MongoDB. Затем мы запланируем задание cron, использующее GitHub API v4 (GraphQL), чтобы обновить документы в этой коллекции. Затем мы развернем все это в облаке Heroku. Наконец, мы запустим перестройку внешнего интерфейса в конце нашего задания cron.

Внешний интерфейс Gatsby.js

Во второй статье мы сосредоточимся на реализации API createPages . Мы соберем все репозитории из серверной части и создадим единую домашнюю страницу, содержащую список всех репозиториев, а также страницу для каждого возвращенного документа репозитория. Затем мы развернем наш внешний интерфейс на Netlify.

От AWS Lambda и AWS CloudWatch

Эта часть не является обязательной, если ваше приложение не будет спать. В противном случае вы должны быть уверены, что ваша серверная часть запущена и работает во время обновления репозиториев. В качестве решения вы можете создать расписание cron в AWS CloudWatch за 10 минут до ежедневного обновления и привязать его в качестве триггера к методу GET в AWS Lambda. Доступ к серверному приложению разбудит экземпляр Heroku. Подробнее будет в конце второй статьи.

Вот архитектура, которую мы будем реализовывать:

Схема архитектуры, показывающая, как AWS Lambda и CloudWatch пингуют серверную часть Node.js, которая получает ежедневные обновления, используя API GitHub, а затем создает клиентскую часть на основе Gatsby, которая использует внутренние API для обновления своих статических страниц и развертывания в Netlify. Серверная часть также развертывается в Heroku с бесплатным планом.

Предположения

Я предполагаю, что читатели этой статьи обладают знаниями в следующих областях:

  • HTML
  • CSS
  • JavaScript
  • REST API
  • MongoDB
  • Гит
  • Node.js

Также хорошо, если вы знаете:

  • Express.js
  • Мангуста
  • GitHub API v4 (GraphQL)
  • Heroku, AWS или любая другая облачная платформа
  • Реагировать

Давайте углубимся в реализацию бэкенда. Мы разделим его на две задачи. Первый — это подготовка конечных точек REST API и их привязка к нашей коллекции репозитория. Второй реализует задание cron, которое использует GitHub API и обновляет коллекцию.

Разработка серверной части генератора статических сайтов Node.js, шаг 1: простой REST API

Мы будем использовать Express для нашего фреймворка веб-приложений и Mongoose для подключения к MongoDB. Если вы знакомы с Express и Mongoose, вы можете перейти к шагу 2.

(С другой стороны, если вам нужно больше познакомиться с Express, вы можете ознакомиться с официальным начальным руководством по Express; если вы не знакомы с Mongoose, вам должно помочь официальное начальное руководство по Mongoose.)

Структура проекта

Иерархия файлов/папок нашего проекта будет простой:

Список папок в корне проекта, показывающий папки config, controller, model и node_modules, а также несколько стандартных корневых файлов, таких как index.js и package.json. Файлы первых трех папок следуют соглашению об именовании, согласно которому имя папки повторяется в каждом имени файла в данной папке.

Подробнее:

  • env.config.js — это файл конфигурации переменных среды.
  • routes.config.js предназначен для сопоставления конечных точек отдыха.
  • repository.controller.js содержит методы для работы с нашей моделью репозитория.
  • repository.model.js содержит схему репозитория MongoDB и операции CRUD.
  • index.js — это класс инициализатора.
  • package.json содержит зависимости и свойства проекта

Реализация

Запустите npm install (или yarn , если у вас установлен Yarn) после добавления этих зависимостей в 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" } // ... }

В нашем файле env.config.js на данный момент есть только свойства port , environment ( dev или prod ) и mongoDbUri :

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

routes.config.js содержит сопоставления запросов и вызовет соответствующий метод нашего контроллера:

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

Файл repository.controller.js — это наш сервисный слой. В его обязанности входит вызов соответствующего метода нашей модели репозитория:

 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 обрабатывает соединение MongoDb и операции CRUD для модели репозитория. Поля модели:

  • owner : владелец репозитория (компания или пользователь)
  • name : имя репозитория
  • createdAt : Дата создания последнего релиза
  • resourcePath : путь последнего выпуска
  • tagName : Тег последнего выпуска
  • releaseDescription : Примечания к выпуску
  • homepageUrl : домашний URL-адрес проекта.
  • repositoryDescription : описание репозитория.
  • avatarUrl : URL-адрес аватара владельца проекта.
 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 }); };

Это то, что у нас есть после нашей первой фиксации: соединение с MongoDB и наши операции REST.

Мы можем запустить наше приложение с помощью следующей команды:

 node index.js

Тестирование

Для тестирования отправьте запросы на localhost:3000 (используя, например, Postman или cURL):

Вставить репозиторий (только обязательные поля)

Сообщение: http://localhost:3000/repositories

Тело:

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

Получить репозитории

Получите: http://localhost:3000/repositories

Получить по идентификатору

Получите: http://localhost:3000/repositories/:id

Патч по ID

Патч: http://localhost:3000/repositories/:id

Тело:

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

После этого пришло время автоматизировать обновления.

Разработка серверной части генератора статических сайтов Node.js, шаг 2: задание Cron для обновления выпусков репозитория

В этой части мы настроим простое задание cron (которое начнется в полночь по Гринвичу) для обновления репозиториев GitHub, которые мы вставили в нашу базу данных. Мы добавили только параметры owner и name только в нашем примере выше, но этих двух полей нам достаточно для доступа к общей информации о данном репозитории.

Чтобы обновить наши данные, мы должны использовать GitHub API. Для этой части лучше всего быть знакомым с GraphQL и v4 GitHub API.

Нам также необходимо создать токен доступа GitHub. Минимальные необходимые области для этого:

Нам нужны следующие области действия токена GitHub: repo:status, repo_deployment, public_repo, read:org и read:user.

Это сгенерирует токен, и с его помощью мы сможем отправлять запросы на GitHub.

Теперь вернемся к нашему коду.

У нас есть две новые зависимости в package.json :

  • "axios": "^0.18.0" — это HTTP-клиент, поэтому мы можем делать запросы к GitHub API.
  • "cron": "^1.7.0" — планировщик заданий cron.

Как обычно, запустите npm install или yarn после добавления зависимостей.

Нам также понадобятся два новых свойства в config.js :

  • "githubEndpoint": "https://api.github.com/graphql"
  • "githubAccessToken": process.env.GITHUB_ACCESS_TOKEN (вам нужно будет установить переменную среды GITHUB_ACCESS_TOKEN с вашим личным токеном доступа)

Создайте новый файл в папке controller с именем cron.controller.js . Он просто вызовет метод updateResositories repository.controller.js в запланированное время:

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

Окончательные изменения для этой части будут в repository.controller.js . Для краткости мы разработаем его для одновременного обновления всех репозиториев. Но если у вас большое количество репозиториев, вы можете превысить ограничения ресурсов GitHub API. Если это так, вам нужно будет изменить это, чтобы запускать ограниченные партии, распределенные по времени.

Комплексная реализация функционала обновления будет выглядеть так:

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

Наконец, мы вызовем конечную точку и обновим модель репозитория.

Функция getLatestRelease создаст запрос GraphQL и вызовет API GitHub. Затем ответ на этот запрос будет обработан в функции 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); }); }

После нашего второго коммита мы внедрим планировщик cron для получения ежедневных обновлений из наших репозиториев GitHub.

Мы почти закончили с задней частью. Но последний шаг должен быть сделан после реализации внешнего интерфейса, поэтому мы рассмотрим его в следующей статье.

Развертывание серверной части генератора статических сайтов Node в Heroku

На этом этапе мы развернем наше приложение на Heroku, поэтому вам нужно будет создать учетную запись, если у вас ее еще нет. Если мы привяжем нашу учетную запись Heroku к GitHub, нам будет намного проще иметь непрерывное развертывание. С этой целью я размещаю свой проект на GitHub.

После входа в свою учетную запись Heroku добавьте новое приложение с панели инструментов:

Выбор «Создать новое приложение» в меню «Создать» на панели инструментов Heroku.

Дайте ему уникальное имя:

Название вашего приложения в Heroku.

Вы будете перенаправлены в раздел развертывания. Выберите GitHub в качестве метода развертывания, найдите свой репозиторий и нажмите кнопку «Подключиться»:

Связывание вашего нового репозитория GitHub с вашим приложением Heroku.

Для простоты можно включить автоматическое развертывание. Он будет развернут всякий раз, когда вы отправляете фиксацию в свой репозиторий GitHub:

Включение автоматического развертывания в Heroku.

Теперь нам нужно добавить MongoDB в качестве ресурса. Перейдите на вкладку «Ресурсы» и нажмите «Найти дополнительные дополнения». (Лично я использую mLab mongoDB.)

Добавление ресурса MongoDB в ваше приложение Heroku.

Установите его и введите имя своего приложения в поле ввода «Приложение для предоставления»:

Страница предоставления надстройки mlab MongoDB в Heroku.

Наконец, мы должны создать файл с именем Procfile на корневом уровне нашего проекта, в котором указаны команды, которые выполняются приложением при его запуске Heroku.

Наш Procfile такой же простой:

 web: node index.js

Создайте файл и зафиксируйте его. Как только вы отправите фиксацию, Heroku автоматически развернет ваше приложение, которое будет доступно как https://[YOUR_UNIQUE_APP_NAME].herokuapp.com/ .

Чтобы проверить, работает ли он, мы можем отправить те же запросы, что и на localhost .

Node.js, Express, MongoDB, Cron и Heroku: мы на полпути!

Вот как будет выглядеть наш репозиторий после третьего коммита.

До сих пор мы реализовали REST API на основе Node.js/Express на нашей серверной части, средство обновления, использующее API GitHub, и задание cron для его активации. Затем мы развернули нашу серверную часть, которая позже будет предоставлять данные для нашего генератора статического веб-контента, используя Heroku с хуком для непрерывной интеграции. Теперь вы готовы ко второй части, где мы реализуем внешний интерфейс и завершаем приложение!

Связанный: 10 самых распространенных ошибок, которые совершают разработчики Node.js