个性化阅读
专注于IT技术分析

后端:使用Gatsby.js和Node.js进行静态网站更新

本文概述

在本系列文章中, 我们将开发一个静态内容网站原型。它将为流行的GitHub存储库生成每日更新的简单静态HTML页面, 以跟踪其最新版本。静态网页生成框架具有实现此目标的强大功能-我们将使用最受欢迎的Gatsby.js。

在Gatsby中, 有很多方法可以收集前端数据, 而无需后端(无服务器), Headless CMS平台和Gatsby源插件。但是, 我们将实现一个后端, 以存储有关GitHub存储库及其最新版本的基本信息。因此, 我们将完全控制后端和前端。

另外, 我将介绍一组触发你的应用程序每日更新的工具。你也可以手动触发它, 或者在发生某些特定事件时触发它。

我们的前端应用程序将在Netlify上运行, 而后端应用程序将使用免费计划在Heroku上运行。它会定期进入睡眠状态:”当有人访问该应用程序时, dyno管理器将自动唤醒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对Node.js后端执行ping操作,该后端通过使用GitHub API进行每日更新,然后构建基于Gatsby的前端,该前端使用后端API来更新其静态页面并部署到Netlify。后端也以免费计划部署到Heroku。

假设条件

我认为本文的读者具有以下方面的知识:

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

如果你知道, 这也很好:

  • Express.js
  • 猫鼬
  • GitHub API v4(GraphQL)
  • Heroku, AWS或任何其他云平台
  • React

让我们深入研究后端的实现。我们将其分为两个任务。第一个是准备REST API端点并将其绑定到我们的存储库集合。第二个是实现使用GitHub API并更新集合的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。前三个文件夹的文件遵循在给定文件夹内的每个文件名中重复文件夹名称的命名约定。

更详细地:

  • env.config.js是环境变量配置文件
  • route.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文件目前仅具有端口, 环境(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:存储库所有者(公司或用户)
  • 名称:存储库名称
  • createdAt:上次发布的创建日期
  • resourcePath:最后的发布路径
  • tagName:最近发布的标签
  • releaseDescription:发行说明
  • homepageUrl:项目的首页网址
  • repositoryDe​​scription:存储库描述
  • 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存储库。在上面的示例中, 我们仅添加了owner和name参数, 但是这两个字段足以让我们访问有关给定存储库的常规信息。

为了更新我们的数据, 我们必须使用GitHub API。对于这一部分, 最好熟悉GraphQL和GitHub API的v4。

我们还需要创建一个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环境变量)

在控制器文件夹下创建一个名为cron.controller.js的新文件。它只会在计划的时间简单地调用repository.controller.js的updateResositories方法:

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。)

在你的Heroku应用中添加MongoDB资源。

安装它, 然后在”供应给以下应用的应用”输入框中输入你的应用名称:

Heroku中的mLab MongoDB附加设置页面。

最后, 我们必须在项目的根目录下创建一个名为Procfile的文件, 该文件指定在Heroku启动时由应用程序执行的命令。

我们的Procfile就是这样简单:

web: node index.js

创建文件并提交。推送提交后, Heroku将自动部署你的应用程序, 该应用程序可以通过https:// [YOUR_UNIQUE_APP_NAME] .herokuapp.com /访问。

要检查其是否正常运行, 我们可以发送与发送给本地主机相同的请求。

Node.js, Express, MongoDB, Cron和Heroku:我们已经完成了一半!

在第三次提交之后, 这就是我们的仓库外观。

到目前为止, 我们已经在后端实现了基于Node.js / Express的REST API, 使用GitHub API的更新程序以及激活它的cron作业。然后, 我们部署了后端, 该后端稍后将使用带有连续集成挂钩的Heroku为静态Web内容生成器提供数据。现在你已经准备好进行第二部分的工作, 我们将在其中实现前端并完成应用程序!

相关:Node.js开发人员最常犯的十大错误

赞(0)
未经允许不得转载:srcmini » 后端:使用Gatsby.js和Node.js进行静态网站更新

评论 抢沙发

评论前必须登录!