後端:使用 Gatsby.js 和 Node.js 進行靜態站點更新

已發表: 2022-03-11

在本系列文章中,我們將開發一個靜態內容網站原型。 它將為流行的 GitHub 存儲庫生成每日更新的簡單靜態 HTML 頁面,以跟踪其最新版本。 靜態網頁生成框架有很好的特性來實現這一點——我們將使用 Gatsby.js,它是最流行的之一。

在 Gatsby 中,有很多方法可以在沒有後端(無服務器)、無頭 CMS 平台和 Gatsby 源插件的情況下為前端收集數據。 但我們將實現一個後端來存儲有關 GitHub 存儲庫及其最新版本的基本信息。 因此,我們將完全控制我們的後端和前端。

此外,我將介紹一組工具來觸發您的應用程序的每日更新。 您也可以手動觸發它或在發生某些特定事件時觸發它。

我們的前端應用程序將在 Netlify 上運行,後端應用程序將使用免費計劃在 Heroku 上運行。 它會定期休眠:“當有人訪問應用程序時,dyno manager 會自動喚醒 web dyno 以運行 web 進程類型。” 因此,我們可以通過 AWS Lambda 和 AWS CloudWatch 喚醒它。 在撰寫本文時,這是讓原型 24/7 在線的最具成本效益的方式。

我們的節點靜態網站示例:期待什麼

為了使這些文章專注於一個主題,我不會涉及身份驗證、驗證、可伸縮性或其他一般主題。 本文的編碼部分將盡可能簡單。 項目的結構和正確的工具集的使用更為重要。

在本系列的第一部分中,我們將開發和部署我們的後端應用程序。 在第二部分,我們將開發和部署我們的前端應用程序,並觸發日常構建。

Node.js 後端

後端應用程序將用 Node.js 編寫(不是強制性的,但為了簡單起見),所有通信都將通過 REST API。 我們不會在這個項目中從前端收集數據。 (如果您對此感興趣,請查看 Gatsby Forms。)

首先,我們將從實現一個簡單的 REST API 後端開始,該後端公開 MongoDB 中存儲庫集合的 CRUD 操作。 然後我們將安排一個使用 GitHub API v4 (GraphQL) 的 cron 作業,以更新此集合中的文檔。 然後我們將所有這些部署到 Heroku 雲。 最後,我們將在 cron 作業結束時觸發前端的重建。

Gatsby.js 前端

在第二篇文章中,我們將重點介紹createPages API 的實現。 我們將從後端收集所有存儲庫,並將生成一個主頁,其中包含所有存儲庫的列表,以及返回的每個存儲庫文檔的頁面。 然後我們將前端部署到 Netlify。

來自 AWS Lambda 和 AWS CloudWatch

如果您的應用程序不會休眠,這部分不是強制性的。 否則,您需要確保您的後端在更新存儲庫時已啟動並正在運行。 作為一種解決方案,您可以在每日更新前 10 分鐘在 AWS CloudWatch 上創建一個 cron 計劃,並將其作為觸發器綁定到 AWS Lambda 中的GET方法。 訪問後端應用程序將喚醒 Heroku 實例。 更多細節將在第二篇文章的末尾。

這是我們將要實現的架構:

顯示 AWS Lambda 和 CloudWatch ping Node.js 後端的架構圖,該後端通過使用 GitHub API 獲取每日更新,然後構建基於 Gatsby 的前端,該前端使用後端 API 來更新其靜態頁面並部署到 Netlify。後端還通過免費計劃部署到 Heroku。

假設

我假設本文的讀者俱有以下領域的知識:

  • HTML
  • CSS
  • JavaScript
  • REST API
  • MongoDB
  • 吉特
  • 節點.js

如果您知道以下內容也很好:

  • Express.js
  • 貓鼬
  • GitHub API v4 (GraphQL)
  • Heroku、AWS 或任何其他雲平台
  • 反應

讓我們深入了解後端的實現。 我們將把它分成兩個任務。 第一個是準備 REST API 端點並將它們綁定到我們的存儲庫集合。 第二個是實現一個使用 GitHub API 並更新集合的 cron 作業。

開發 Node.js 靜態站點生成器後端,第 1 步:簡單的 REST API

我們將使用 Express 作為我們的 Web 應用程序框架,使用 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包含存儲庫和 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文件目前只有portenvironmentdevprod )和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

通過 ID 獲取

獲取:http://localhost:3000/repositories/:id

按 ID 打補丁

補丁:http://localhost:3000/repositories/:id

身體:

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

有了這個工作,是時候自動更新了。

開發 Node.js 靜態站點生成器後端,第 2 步:更新存儲庫版本的 Cron 作業

在這一部分中,我們將配置一個簡單的 cron 作業(將從 UTC 午夜開始)來更新我們插入數據庫的 GitHub 存儲庫。 我們僅在上面的示例中添加了ownername參數,但這兩個字段足以讓我們訪問有關給定存儲庫的一般信息。

為了更新我們的數據,我們必須使用 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 installyarn

我們還需要config.js中的兩個新屬性:

  • "githubEndpoint": "https://api.github.com/graphql"
  • "githubAccessToken": process.env.GITHUB_ACCESS_TOKEN (您需要使用您自己的個人訪問令牌設置GITHUB_ACCESS_TOKEN環境變量)

controller文件夾下創建一個名為cron.controller.js的新文件。 它只會在預定時間調用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 查詢並調用 GitHub API。 然後,該請求的響應將在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 存儲庫獲取每日更新。

我們幾乎完成了後端。 但是最後一步應該在實現前端之後完成,所以我們將在下一篇文章中介紹它。

將節點靜態站點生成器後端部署到 Heroku

在這一步中,我們會將我們的應用程序部署到 Heroku,因此如果您還沒有帳戶,則需要在他們那裡設置一個帳戶。 如果我們將我們的 Heroku 賬戶綁定到 GitHub,我們進行持續部署會容易得多。 為此,我將我的項目託管在 GitHub 上。

登錄您的 Heroku 帳戶後,從儀表板添加一個新應用程序:

從 Heroku 儀表板的新建菜單中選擇“創建新應用程序”。

給它一個獨特的名字:

在 Heroku 中命名您的應用程序。

您將被重定向到部署部分。 選擇 GitHub 作為部署方式,搜索你的倉庫,然後點擊“連接”按鈕:

將您的新 GitHub 存儲庫鏈接到您的 Heroku 應用程序。

為簡單起見,您可以啟用自動部署。 每當您將提交推送到 GitHub 存儲庫時,它都會部署:

在 Heroku 中啟用自動部署。

現在我們必須將 MongoDB 添加為資源。 轉到“資源”選項卡,然後單擊“查找更多附加組件”。 (我個人使用 mLab mongoDB。)

將 MongoDB 資源添加到您的 Heroku 應用程序。

安裝它並在“App to provision to”輸入框中輸入您的應用名稱:

Heroku 中的 mLab MongoDB 插件配置頁面。

最後,我們必須在項目的根級別創建一個名為Procfile的文件,該文件指定應用程序在 Heroku 啟動時執行的命令。

我們的Procfile就這麼簡單:

 web: node index.js

創建文件並提交。 推送提交後,Heroku 將自動部署您的應用程序,該應用程序可通過https://[YOUR_UNIQUE_APP_NAME].herokuapp.com/訪問。

要檢查它是否正常工作,我們可以發送我們發送到localhost的相同請求。

Node.js、Express、MongoDB、Cron 和 Heroku:我們已經成功了一半!

在我們第三次提交之後,這就是我們的 repo 的樣子。

到目前為止,我們已經在後端實現了基於 Node.js/Express 的 REST API、使用 GitHub 的 API 的更新程序以及用於激活它的 cron 作業。 然後我們部署了後端,稍後將使用 Heroku 為我們的靜態 Web 內容生成器提供數據,並帶有一個用於持續集成的鉤子。 現在您已為第二部分做好準備,我們將在其中實現前端並完成應用程序!

相關: Node.js 開發人員最常犯的 10 個錯誤