バックエンド:静的サイトの更新にGatsby.jsとNode.jsを使用する

公開: 2022-03-11

この一連の記事では、静的コンテンツのWebサイトのプロトタイプを開発します。 人気のあるGitHubリポジトリの最新リリースを追跡するために、毎日更新される単純な静的HTMLページを生成します。 静的Webページ生成フレームワークには、それを実現するための優れた機能があります。最も人気のあるGatsby.jsを使用します。

Gatsbyには、バックエンド(サーバーレス)、ヘッドレスCMSプラットフォーム、Gatsbyソースプラグインを含まずにフロントエンドのデータを収集する方法がたくさんあります。 ただし、GitHubリポジトリとその最新リリースに関する基本情報を保存するためのバックエンドを実装します。 したがって、バックエンドとフロントエンドの両方を完全に制御できます。

また、アプリケーションの毎日の更新をトリガーするための一連のツールについても説明します。 手動で、または特定のイベントが発生したときにトリガーすることもできます。

フロントエンドアプリケーションはNetlifyで実行され、バックエンドアプリケーションは無料プランを使用してHerokuで動作します。 定期的にスリープします。「誰かがアプリにアクセスすると、dynoマネージャーは自動的にWeb dynoを起動して、Webプロセスタイプを実行します。」 そのため、AWSLambdaとAWSCloudWatchを介してウェイクアップできます。 これを書いている時点で、これはプロトタイプを24時間年中無休でオンラインにするための最も費用効果の高い方法です。

私たちのノード静的ウェブサイトの例:何を期待するか

これらの記事を1つのトピックに集中させるために、認証、検証、スケーラビリティ、またはその他の一般的なトピックについては説明しません。 この記事のコーディング部分は、可能な限り単純になります。 プロジェクトの構造と正しいツールセットの使用がより重要です。

シリーズのこの最初のパートでは、バックエンドアプリケーションを開発してデプロイします。 第2部では、フロントエンドアプリケーションを開発してデプロイし、デイリービルドをトリガーします。

Node.jsバックエンド

バックエンドアプリケーションはNode.jsで記述され(必須ではありませんが、簡単にするため)、すべての通信はRESTAPIを介して行われます。 このプロジェクトでは、フロントエンドからデータを収集しません。 (これに興味がある場合は、Gatsby Formsをご覧ください。)

まず、MongoDBのリポジトリコレクションのCRUD操作を公開する単純なRESTAPIバックエンドを実装することから始めます。 次に、このコレクションのドキュメントを更新するために、GitHub API v4(GraphQL)を使用するcronジョブをスケジュールします。 次に、これらすべてをHerokuクラウドにデプロイします。 最後に、cronジョブの最後にフロントエンドの再構築をトリガーします。

Gatsby.jsフロントエンド

2番目の記事では、 createPagesの実装に焦点を当てます。 バックエンドからすべてのリポジトリを収集し、すべてのリポジトリのリストと、返された各リポジトリドキュメントのページを含む単一のホームページを生成します。 次に、フロントエンドをNetlifyにデプロイします。

AWSLambdaおよびAWSCloudWatchから

アプリケーションがスリープしない場合、この部分は必須ではありません。 それ以外の場合は、リポジトリの更新時にバックエンドが稼働していることを確認する必要があります。 解決策として、毎日更新する10分前にAWS CloudWatchでcronスケジュールを作成し、それをトリガーとしてAWSLambdaのGETメソッドにバインドできます。 バックエンドアプリケーションにアクセスすると、Herokuインスタンスがウェイクアップします。 詳細については、2番目の記事の最後に記載されています。

実装するアーキテクチャは次のとおりです。

AWSLambdaとCloudWatchがNode.jsバックエンドにpingを実行するアーキテクチャ図。GitHubAPIを使用して毎日更新を取得し、Gatsbyベースのフロントエンドを構築します。このフロントエンドはバックエンドAPIを使用して静的ページを更新し、Netlifyにデプロイします。バックエンドも無料プランでHerokuにデプロイされます。

仮定

この記事の読者は、次の分野の知識があると思います。

  • HTML
  • CSS
  • JavaScript
  • REST API
  • MongoDB
  • ギット
  • Node.js

あなたが知っているならそれも良いです:

  • Express.js
  • マングース
  • GitHub API v4(GraphQL)
  • Heroku、AWS、またはその他のクラウドプラットフォーム
  • React

バックエンドの実装について詳しく見ていきましょう。 これを2つのタスクに分割します。 1つ目は、REST APIエンドポイントを準備し、それらをリポジトリコレクションにバインドすることです。 2つ目は、GitHubAPIを使用してコレクションを更新するcronジョブを実装することです。

Node.js静的サイトジェネレーターバックエンドの開発、ステップ1:シンプルなREST API

WebアプリケーションフレームワークにはExpressを使用し、MongoDB接続にはMongooseを使用します。 ExpressとMongooseに精通している場合は、手順2にスキップできる場合があります。

(一方、Expressに精通している必要がある場合は、公式のExpressスターターガイドを確認できます。Mongooseを使用していない場合は、公式のMongooseスターターガイドが役立ちます。)

プロジェクト構造

プロジェクトのファイル/フォルダー階層は単純になります。

プロジェクトルートのフォルダーリスト。config、controller、model、node_modulesフォルダーに加えて、index.jsやpackage.jsonなどのいくつかの標準ルートファイルが表示されます。最初の3つのフォルダーのファイルは、特定のフォルダー内の各ファイル名でフォルダー名を繰り返すという命名規則に従います。

さらに詳細に:

  • env.config.jsは、環境変数の構成ファイルです。
  • routes.config.jsは、残りのエンドポイントをマッピングするためのものです
  • repository.controller.jsには、リポジトリモデルで動作するメソッドが含まれています
  • repository.model.jsには、リポジトリとCRUD操作のMongoDBスキーマが含まれています
  • index.jsは初期化クラスです
  • package.jsonには、依存関係とプロジェクトのプロパティが含まれています

実装

これらの依存関係をpackage.jsonに追加した後、 npm install (またはYarnがインストールされている場合はyarn )を実行します。

 { // ... "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ファイルには、現時点では、 portenvironmentdevまたは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 / repository

体:

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

リポジトリを取得する

取得:http:// localhost:3000 / repository

IDで取得

取得:http:// localhost:3000 / repository /:id

IDによるパッチ

パッチ:http:// localhost:3000 / repository /:id

体:

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

それが機能しているので、更新を自動化する時が来ました。

Node.js静的サイトジェネレーターバックエンドの開発、ステップ2:リポジトリリリースを更新するためのcronジョブ

このパートでは、データベースに挿入したGitHubリポジトリを更新するための単純なcronジョブ(UTCの午前0時に開始)を構成します。 上記の例では、 ownernameのパラメーターのみを追加しましたが、これら2つのフィールドは、特定のリポジトリに関する一般的な情報にアクセスするのに十分です。

データを更新するには、GitHubAPIを使用する必要があります。 この部分では、GitHubAPIのGraphQLとv4に精通していることが最善です。

また、GitHubアクセストークンを作成する必要があります。 そのために最低限必要なスコープは次のとおりです。

必要なGitHubトークンスコープは、repo:status、repo_deployment、public_repo、read:org、およびread:userです。

これによりトークンが生成され、それを使用してGitHubにリクエストを送信できます。

それでは、コードに戻りましょう。

package.jsonに2つの新しい依存関係があります:

  • "axios": "^0.18.0"はHTTPクライアントであるため、GitHubAPIにリクエストを送信できます
  • "cron": "^1.7.0"はcronジョブスケジューラです

いつものように、依存関係を追加した後、 npm installまたはyarnを実行します。

config.jsにも2つの新しいプロパティが必要です。

  • "githubEndpoint": "https://api.github.com/graphql"
  • "githubAccessToken": process.env.GITHUB_ACCESS_TOKENGITHUB_ACCESS_TOKEN環境変数を独自の個人用アクセストークンで設定する必要があります)

cron.controller.jsという名前のcontrollerフォルダーの下に新しいファイルを作成します。 スケジュールされた時間にrepository.controller.jsupdateResositoriesメソッドを呼び出すだけです。

 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クエリを生成し、GitHubAPIを呼び出します。 そのリクエストからの応答は、 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); }); }

2回目のコミット後、GitHubリポジトリから毎日更新を取得するためのcronスケジューラを実装します。

バックエンドはほぼ完成です。 ただし、フロントエンドを実装した後に最後のステップを実行する必要があるため、次の記事で説明します。

Node StaticSiteGeneratorバックエンドをHerokuにデプロイする

このステップでは、アプリケーションをHerokuにデプロイするため、アカウントをまだ持っていない場合は、アカウントを設定する必要があります。 HerokuアカウントをGitHubにバインドすると、継続的なデプロイがはるかに簡単になります。 そのために、GitHubでプロジェクトをホストしています。

Herokuアカウントにログインした後、ダッシュボードから新しいアプリを追加します。

Herokuダッシュボードの[新規]メニューから[新しいアプリの作成]を選択します。

一意の名前を付けます。

Herokuでアプリに名前を付けます。

デプロイメントセクションにリダイレクトされます。 デプロイ方法としてGitHubを選択し、リポジトリを検索して、[接続]ボタンをクリックします。

新しいGitHubレポジトリをHerokuアプリにリンクします。

簡単にするために、自動デプロイを有効にすることができます。 GitHubリポジトリにコミットをプッシュするたびにデプロイされます。

Herokuで自動デプロイを有効にします。

次に、MongoDBをリソースとして追加する必要があります。 [リソース]タブに移動し、[その他のアドオンを検索]をクリックします。 (私は個人的にmLab mongoDBを使用しています。)

HerokuアプリにMongoDBリソースを追加します。

それをインストールし、「プロビジョニングするアプリ」入力ボックスにアプリの名前を入力します。

HerokuのmLabMongoDBアドオンプロビジョニングページ。

最後に、プロジェクトのルートレベルにProcfileという名前のファイルを作成する必要があります。このファイルは、Herokuの起動時にアプリによって実行されるコマンドを指定します。

私たちのProcfileはこれと同じくらい簡単です:

 web: node index.js

ファイルを作成してコミットします。 コミットをプッシュすると、Herokuはアプリケーションを自動的にデプロイします。アプリケーションにはhttps://[YOUR_UNIQUE_APP_NAME].herokuapp.com/としてアクセスできます。

それが機能しているかどうかを確認するために、 localhostに送信したのと同じリクエストを送信できます。

Node.js、Express、MongoDB、Cron、Heroku:途中です!

3回目のコミット後、これはリポジトリがどのようになるかを示します。

これまで、Node.js /ExpressベースのRESTAPI、GitHubのAPIを使用するアップデーター、およびそれをアクティブ化するためのcronジョブを実装してきました。 次に、バックエンドをデプロイしました。バックエンドは、継続的インテグレーション用のフックを備えたHerokuを使用して、静的Webコンテンツジェネレーターにデータを後で提供します。 これで、フロントエンドを実装してアプリを完成させる第2部の準備が整いました。

関連: Node.js開発者が犯す最も一般的な間違いトップ10