النهاية الخلفية: استخدام Gatsby.js و Node.js لتحديثات الموقع الثابتة

نشرت: 2022-03-11

في سلسلة المقالات هذه ، سنطور نموذجًا أوليًا لموقع ويب للمحتوى الثابت. سينشئ صفحات HTML ثابتة ومحدثة يوميًا لمستودعات GitHub الشهيرة لتتبع أحدث إصداراتها. تتمتع أطر إنشاء صفحات الويب الثابتة بميزات رائعة لتحقيق ذلك - سنستخدم Gatsby.js ، وهو أحد أكثر الأطر شيوعًا.

في Gatsby ، هناك العديد من الطرق لجمع البيانات لواجهة أمامية دون وجود نهاية خلفية (بدون خادم) ، ومنصات بدون رأس CMS ومكونات إضافية لمصدر Gatsby فيما بينها. لكننا سنقوم بتطبيق نهاية خلفية لتخزين المعلومات الأساسية حول مستودعات GitHub وأحدث إصداراتها. وبالتالي ، سيكون لدينا سيطرة كاملة على كل من نهايتنا الخلفية والأمامية.

أيضًا ، سأغطي مجموعة من الأدوات لبدء تحديث يومي لتطبيقك. يمكنك أيضًا تشغيله يدويًا أو عند حدوث حدث معين.

سيتم تشغيل تطبيق الواجهة الأمامية الخاص بنا على Netlify ، وسيعمل التطبيق الخلفي على Heroku باستخدام خطة مجانية. سوف ينام بشكل دوري: "عندما يصل شخص ما إلى التطبيق ، سيقوم مدير dyno تلقائيًا بإيقاظ دينو الويب لتشغيل نوع عملية الويب." لذلك ، يمكننا إيقاظه عبر AWS Lambda و AWS CloudWatch. حتى كتابة هذه السطور ، هذه هي الطريقة الأكثر فعالية من حيث التكلفة للحصول على نموذج أولي عبر الإنترنت على مدار الساعة طوال أيام الأسبوع.

مثال على موقع Node Static على الويب: ما يمكن توقعه

للحفاظ على تركيز هذه المقالات على موضوع واحد ، لن أغطي المصادقة أو التحقق من الصحة أو قابلية التوسع أو الموضوعات العامة الأخرى. سيكون جزء الترميز في هذه المقالة بسيطًا قدر الإمكان. هيكل المشروع واستخدام مجموعة الأدوات الصحيحة أكثر أهمية.

في هذا الجزء الأول من السلسلة ، سنطور وننشر تطبيقنا الخلفي. في الجزء الثاني ، سنقوم بتطوير ونشر تطبيق الواجهة الأمامية الخاص بنا ، وتشغيل عمليات الإنشاء اليومية.

الواجهة الخلفية لـ Node.js

سيتم كتابة التطبيق الخلفي في Node.js (ليس إلزاميًا ، ولكن من أجل البساطة) وستكون جميع الاتصالات عبر REST APIs. لن نقوم بجمع البيانات من الواجهة الأمامية في هذا المشروع. (إذا كنت مهتمًا بفعل ذلك ، فقم بإلقاء نظرة على نماذج Gatsby.)

أولاً ، سنبدأ بتنفيذ واجهة خلفية بسيطة لـ REST API تعرض عمليات CRUD لمجموعة المستودعات في MongoDB الخاص بنا. ثم سنقوم بجدولة مهمة cron تستهلك GitHub API v4 (GraphQL) لتحديث المستندات في هذه المجموعة. ثم سننشر كل هذا في سحابة Heroku. أخيرًا ، سنقوم بإعادة بناء الواجهة الأمامية في نهاية مهمة cron الخاصة بنا.

واجهة Gatsby.js الأمامية

في المقالة الثانية ، سنركز على تنفيذ createPages API. سنجمع جميع المستودعات من النهاية الخلفية وسننشئ صفحة رئيسية واحدة تحتوي على قائمة بجميع المستودعات ، بالإضافة إلى صفحة لكل مستند مستودع يتم إرجاعه. ثم سننشر واجهتنا الأمامية في Netlify.

من AWS Lambda و AWS CloudWatch

هذا الجزء ليس إلزاميًا إذا كان التطبيق الخاص بك لا ينام. وإلا ، فأنت بحاجة إلى التأكد من أن الواجهة الخلفية الخاصة بك تعمل في وقت تحديث المستودعات. كحل ، يمكنك إنشاء جدول cron على AWS CloudWatch قبل 10 دقائق من التحديث اليومي الخاص بك وربطه كمحفز لطريقة GET الخاصة بك في AWS Lambda. سيؤدي الوصول إلى تطبيق النهاية إلى تنبيه مثيل Heroku. مزيد من التفاصيل ستكون في نهاية المقال الثاني.

ها هي البنية التي سننفذها:

مخطط معماري يُظهر AWS Lambda & CloudWatch ping على النهاية الخلفية Node.js ، والتي تحصل على تحديثات يومية عن طريق استهلاك واجهة برمجة تطبيقات GitHub ثم تبني الواجهة الأمامية المستندة إلى Gatsby ، والتي تستهلك واجهات برمجة التطبيقات الخلفية لتحديث صفحاتها الثابتة ونشرها على Netlify. يتم نشر النهاية الخلفية أيضًا في Heroku بخطة مجانية.

الافتراضات

أفترض أن قراء هذا المقال لديهم معرفة في المجالات التالية:

  • لغة البرمجة
  • CSS
  • جافا سكريبت
  • واجهات برمجة تطبيقات REST
  • MongoDB
  • شخص سخيف
  • Node.js

من الجيد أيضًا أن تعرف:

  • Express.js
  • النمس
  • GitHub API v4 (GraphQL)
  • Heroku أو AWS أو أي منصة سحابية أخرى
  • تتفاعل

دعنا نتعمق في تنفيذ النهاية الخلفية. سنقسمها إلى مهمتين. الأول هو إعداد نقاط نهاية REST API وربطها بمجموعة المستودعات الخاصة بنا. والثاني هو تنفيذ مهمة cron التي تستهلك GitHub API وتقوم بتحديث المجموعة.

تطوير Node.js Static Site Generator Back End ، الخطوة 1: واجهة برمجة تطبيقات REST بسيطة

سوف نستخدم Express لإطار تطبيق الويب الخاص بنا و Mongoose لاتصال MongoDB الخاص بنا. إذا كنت معتادًا على Express و Mongoose ، فقد تتمكن من التخطي إلى الخطوة 2.

(من ناحية أخرى ، إذا كنت بحاجة إلى مزيد من الإلمام بـ Express ، فيمكنك الاطلاع على دليل Express Starter الرسمي ؛ إذا لم تكن على موقع 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 (باستخدام ساعي البريد أو cURL):

إدخال مستودع (الحقول المطلوبة فقط)

Post: http: // localhost: 3000 / repositories

الجسم:

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

احصل على مستودعات

احصل على: http: // localhost: 3000 / repositories

احصل على معرف

احصل على: http: // localhost: 3000 / repositories /: id

التصحيح حسب المعرف

التصحيح: http: // localhost: 3000 / repositories /: id

الجسم:

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

مع هذا العمل ، حان الوقت لأتمتة التحديثات.

تطوير الواجهة الخلفية لمولِّد الموقع الثابت Node.js ، الخطوة 2: مهمة Cron لتحديث إصدارات المستودع

في هذا الجزء ، سنقوم بتهيئة وظيفة cron بسيطة (ستبدأ في منتصف الليل بالتوقيت العالمي المنسق) لتحديث مستودعات GitHub التي أدخلناها في قاعدة البيانات الخاصة بنا. لقد أضفنا معلمات owner name فقط في المثال أعلاه ، ولكن هذين الحقلين كافيين بالنسبة لنا للوصول إلى المعلومات العامة حول مستودع معين.

لتحديث بياناتنا ، يتعين علينا استهلاك واجهة برمجة تطبيقات GitHub. في هذا الجزء ، من الأفضل أن تكون على دراية بـ GraphQL و v4 من GitHub API.

نحتاج أيضًا إلى إنشاء رمز وصول إلى GitHub. الحد الأدنى من النطاقات المطلوبة لذلك هي:

نطاقات الرمز المميز لـ GitHub التي نحتاجها هي repo: status و repo_deployment و public_repo وقراءة: org وقراءة: 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. إذا كان الأمر كذلك ، فستحتاج إلى تعديل هذا للتشغيل على دفعات محدودة ، موزعة بمرور الوقت.

سيبدو التنفيذ الشامل لوظيفة التحديث كما يلي:

 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. ستتم معالجة الاستجابة من هذا الطلب بعد ذلك في وظيفة 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 Static Site Generator Back End إلى 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 المستندة إلى Node.js / Express على نهايتنا الخلفية ، والمُحدث الذي يستهلك واجهة برمجة تطبيقات GitHub ، ووظيفة cron لتنشيطها. ثم قمنا بنشر النهاية الخلفية الخاصة بنا والتي ستوفر لاحقًا بيانات لمولد محتوى الويب الثابت الخاص بنا باستخدام Heroku مع خطاف للتكامل المستمر. أنت الآن جاهز للجزء الثاني ، حيث نقوم بتنفيذ الواجهة الأمامية وإكمال التطبيق!

الموضوعات ذات الصلة: الأخطاء العشرة الأكثر شيوعًا التي يرتكبها مطورو Node.js