Bagian Belakang: Menggunakan Gatsby.js dan Node.js untuk Pembaruan Situs Statis

Diterbitkan: 2022-03-11

Dalam seri artikel ini, kami akan mengembangkan prototipe situs web konten statis. Ini akan menghasilkan halaman HTML statis sederhana yang diperbarui setiap hari untuk repositori GitHub populer untuk melacak rilis terbaru mereka. Kerangka kerja pembuatan halaman web statis memiliki fitur hebat untuk mencapai itu—kami akan menggunakan Gatsby.js, salah satu yang paling populer.

Di Gatsby, ada banyak cara untuk mengumpulkan data untuk front end tanpa harus back end (serverless), platform Headless CMS dan plugin sumber Gatsby di antaranya. Namun kami akan menerapkan back end untuk menyimpan informasi dasar tentang repositori GitHub dan rilis terbarunya. Dengan demikian, kita akan memiliki kendali penuh atas ujung belakang dan ujung depan kita.

Juga, saya akan membahas seperangkat alat untuk memicu pembaruan harian aplikasi Anda. Anda juga dapat memicunya secara manual atau kapan pun beberapa peristiwa tertentu terjadi.

Aplikasi front-end kami akan dijalankan di Netlify, dan aplikasi back-end akan bekerja di Heroku menggunakan paket gratis. Ini akan tidur secara berkala: "Ketika seseorang mengakses aplikasi, manajer dyno akan secara otomatis membangunkan dyno web untuk menjalankan jenis proses web." Jadi, kita bisa membangunkannya melalui AWS Lambda dan AWS CloudWatch. Pada tulisan ini, ini adalah cara yang paling hemat biaya untuk memiliki prototipe online 24/7.

Contoh Situs Web Statis Node Kami: Apa yang Diharapkan

Agar artikel ini tetap fokus pada satu topik, saya tidak akan membahas otentikasi, validasi, skalabilitas, atau topik umum lainnya. Bagian pengkodean artikel ini akan sesederhana mungkin. Struktur proyek dan penggunaan seperangkat alat yang benar lebih penting.

Di bagian pertama dari seri ini, kami akan mengembangkan dan menerapkan aplikasi back-end kami. Di bagian kedua, kami akan mengembangkan dan men-deploy aplikasi front-end kami, dan memicu build harian.

Bagian Belakang Node.js

Aplikasi back-end akan ditulis dalam Node.js (tidak wajib, tetapi untuk kesederhanaan) dan semua komunikasi akan melalui REST API. Kami tidak akan mengumpulkan data dari front end dalam proyek ini. (Jika Anda tertarik untuk melakukannya, lihat Formulir Gatsby.)

Pertama, kita akan mulai dengan mengimplementasikan REST API back end sederhana yang mengekspos operasi CRUD dari koleksi repositori di MongoDB kita. Kemudian kami akan menjadwalkan pekerjaan cron yang menggunakan GitHub API v4 (GraphQL) untuk memperbarui dokumen dalam koleksi ini. Kemudian kami akan menyebarkan semua ini ke cloud Heroku. Akhirnya, kami akan memicu pembangunan kembali ujung depan di akhir tugas cron kami.

Bagian Depan Gatsby.js

Pada artikel kedua, kita akan fokus pada implementasi createPages API. Kami akan mengumpulkan semua repositori dari bagian belakang dan akan menghasilkan satu halaman beranda yang berisi daftar semua repositori, ditambah halaman untuk setiap dokumen repositori yang dikembalikan. Kemudian kita akan menyebarkan front end kita ke Netlify.

Dari AWS Lambda dan AWS CloudWatch

Bagian ini tidak wajib jika aplikasi Anda tidak bisa tidur. Jika tidak, Anda harus memastikan bahwa back end Anda aktif dan berjalan pada saat memperbarui repositori. Sebagai solusi, Anda dapat membuat jadwal cron di AWS CloudWatch 10 menit sebelum pembaruan harian dan mengikatnya sebagai pemicu untuk metode GET Anda di AWS Lambda. Mengakses aplikasi back-end akan membangunkan instance Heroku. Selengkapnya akan ada di akhir artikel kedua.

Berikut adalah arsitektur yang akan kami implementasikan:

Diagram arsitektur yang menunjukkan AWS Lambda & CloudWatch melakukan ping ke back end Node.js, yang mendapatkan pembaruan harian dengan menggunakan GitHub API dan kemudian membangun front end berbasis Gatsby, yang menggunakan API back end untuk memperbarui halaman statisnya dan menerapkannya ke Netlify. Bagian belakang juga disebarkan ke Heroku dengan paket gratis.

Asumsi

Saya berasumsi bahwa pembaca artikel ini memiliki pengetahuan di bidang-bidang berikut:

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

Ada baiknya juga jika Anda tahu:

  • Express.js
  • Luwak
  • GitHub API v4 (GraphQL)
  • Heroku, AWS, atau platform cloud lainnya
  • Reaksi

Mari selami implementasi back end. Kami akan membaginya menjadi dua tugas. Yang pertama adalah menyiapkan titik akhir REST API dan mengikatnya ke koleksi repositori kami. Yang kedua adalah mengimplementasikan tugas cron yang menggunakan GitHub API dan memperbarui koleksi.

Mengembangkan Back End Generator Situs Statis Node.js, Langkah 1: API REST Sederhana

Kami akan menggunakan Express untuk kerangka aplikasi web kami dan Mongoose untuk koneksi MongoDB kami. Jika Anda sudah familiar dengan Express dan Mongoose, Anda mungkin bisa melompat ke Langkah 2.

(Di sisi lain, jika Anda membutuhkan lebih banyak keakraban dengan Express, Anda dapat melihat panduan starter Express resmi; jika Anda tidak memahami Mongoose, panduan starter resmi Mongoose akan membantu.)

Struktur Proyek

Hirarki file/folder proyek kami akan sederhana:

Daftar folder root proyek, menampilkan folder config, controller, model, dan node_modules, ditambah beberapa file root standar seperti index.js dan package.json. File dari tiga folder pertama mengikuti konvensi penamaan dengan mengulang nama folder di setiap nama file dalam folder tertentu.

Lebih detail:

  • env.config.js adalah file konfigurasi variabel lingkungan
  • routes.config.js adalah untuk memetakan titik akhir istirahat
  • repository.controller.js berisi metode untuk bekerja pada model repositori kami
  • repository.model.js berisi skema MongoDB dari repositori dan operasi CRUD
  • index.js adalah kelas penginisialisasi
  • package.json berisi dependensi dan properti proyek

Penerapan

Jalankan npm install (atau yarn , jika Anda telah menginstal Yarn) setelah menambahkan dependensi ini ke 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" } // ... }

File env.config.js kami hanya memiliki port , environment ( dev atau prod ), dan properti mongoDbUri untuk saat ini:

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

routes.config.js berisi pemetaan permintaan dan akan memanggil metode yang sesuai dari pengontrol kami:

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

File repository.controller.js adalah lapisan layanan kami. Tanggung jawabnya adalah memanggil metode yang sesuai dari model repositori kami:

 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 menangani koneksi MongoDb dan operasi CRUD untuk model repositori. Bidang model adalah:

  • owner : Pemilik repositori (perusahaan atau pengguna)
  • name : Nama repositori
  • createdAt : Tanggal pembuatan rilis terakhir
  • resourcePath : Jalur rilis terakhir
  • tagName : Tag rilis terakhir
  • releaseDescription : Catatan rilis
  • homepageUrl : URL beranda proyek
  • repositoryDescription : Deskripsi repositori
  • avatarUrl : URL avatar pemilik proyek
 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 }); };

Inilah yang kami miliki setelah komit pertama kami: Koneksi MongoDB dan operasi REST kami.

Kita dapat menjalankan aplikasi kita dengan perintah berikut:

 node index.js

Pengujian

Untuk pengujian, kirim permintaan ke localhost:3000 (menggunakan misalnya Postman atau cURL):

Sisipkan Repositori (Hanya Kolom yang Diperlukan)

Posting: http://localhost:3000/repositories

Tubuh:

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

Dapatkan Repositori

Dapatkan: http://localhost:3000/repositories

Dapatkan dengan ID

Dapatkan: http://localhost:3000/repositories/:id

Tambalan berdasarkan ID

Patch: http://localhost:3000/repositories/:id

Tubuh:

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

Dengan itu berfungsi, saatnya untuk mengotomatiskan pembaruan.

Mengembangkan Back End Generator Situs Statis Node.js, Langkah 2: Pekerjaan Cron untuk Memperbarui Rilis Repositori

Di bagian ini, kami akan mengonfigurasi tugas cron sederhana (yang akan dimulai pada tengah malam UTC) untuk memperbarui repositori GitHub yang kami masukkan ke database kami. Kami hanya menambahkan parameter owner dan name hanya dalam contoh kami di atas, tetapi dua bidang ini cukup bagi kami untuk mengakses informasi umum tentang repositori yang diberikan.

Untuk memperbarui data kami, kami harus menggunakan API GitHub. Untuk bagian ini, yang terbaik adalah mengenal GraphQL dan v4 dari GitHub API.

Kita juga perlu membuat token akses GitHub. Lingkup minimum yang diperlukan untuk itu adalah:

Lingkup token GitHub yang kita butuhkan adalah repo:status, repo_deployment, public_repo, read:org, dan read:user.

Itu akan menghasilkan token, dan kami dapat mengirim permintaan ke GitHub dengannya.

Sekarang mari kita kembali ke kode kita.

Kami memiliki dua dependensi baru di package.json :

  • "axios": "^0.18.0" adalah klien HTTP, jadi kami dapat membuat permintaan ke GitHub API
  • "cron": "^1.7.0" adalah penjadwal tugas cron

Seperti biasa, jalankan npm install atau yarn setelah menambahkan dependensi.

Kami juga membutuhkan dua properti baru di config.js :

  • "githubEndpoint": "https://api.github.com/graphql"
  • "githubAccessToken": process.env.GITHUB_ACCESS_TOKEN (Anda harus menyetel variabel lingkungan GITHUB_ACCESS_TOKEN dengan token akses pribadi Anda sendiri)

Buat file baru di bawah folder controller dengan nama cron.controller.js . Ini hanya akan memanggil metode updateResositories dari repository.controller.js pada waktu yang dijadwalkan:

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

Perubahan terakhir untuk bagian ini akan berada di repository.controller.js . Untuk singkatnya, kami akan mendesainnya untuk memperbarui semua repositori sekaligus. Tetapi jika Anda memiliki banyak repositori, Anda mungkin melebihi batasan sumber daya API GitHub. Jika itu masalahnya, Anda harus memodifikasi ini untuk berjalan dalam batch terbatas, tersebar dari waktu ke waktu.

Implementasi sekaligus dari fungsionalitas pembaruan akan terlihat seperti ini:

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

Terakhir, kita akan memanggil titik akhir dan memperbarui model repositori.

Fungsi getLatestRelease akan menghasilkan kueri GraphQL dan akan memanggil API GitHub. Respon dari permintaan tersebut kemudian akan diproses dalam fungsi 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); }); }

Setelah komit kedua kami, kami akan menerapkan penjadwal cron untuk mendapatkan pembaruan harian dari repositori GitHub kami.

Kami hampir selesai dengan bagian belakang. Tapi langkah terakhir yang harus dilakukan setelah mengimplementasikan front end, jadi kita akan membahasnya di artikel berikutnya.

Menyebarkan Generator Situs Statis Node Back End ke Heroku

Pada langkah ini, kami akan menyebarkan aplikasi kami ke Heroku, jadi Anda harus menyiapkan akun dengan mereka jika Anda belum memilikinya. Jika kita mengikat akun Heroku kita ke GitHub, akan jauh lebih mudah bagi kita untuk melakukan penerapan berkelanjutan. Untuk itu, saya menghosting proyek saya di GitHub.

Setelah masuk ke akun Heroku Anda, tambahkan aplikasi baru dari dasbor:

Memilih "Buat aplikasi baru" dari menu Baru di dasbor Heroku.

Beri nama yang unik:

Memberi nama aplikasi Anda di Heroku.

Anda akan diarahkan ke bagian penerapan. Pilih GitHub sebagai metode penerapan, cari repositori Anda, lalu klik tombol "Hubungkan":

Menautkan repo GitHub baru Anda ke aplikasi Heroku Anda.

Untuk mempermudah, Anda dapat mengaktifkan penerapan otomatis. Ini akan digunakan setiap kali Anda mendorong komit ke repo GitHub Anda:

Mengaktifkan penyebaran otomatis di Heroku.

Sekarang kita harus menambahkan MongoDB sebagai sumber daya. Buka tab Sumber Daya dan klik "Temukan lebih banyak pengaya." (Saya pribadi menggunakan mLab mongoDB.)

Menambahkan sumber daya MongoDB ke aplikasi Heroku Anda.

Instal dan masukkan nama aplikasi Anda di kotak input "Aplikasi untuk penyediaan":

Halaman penyediaan add-on mLab MongoDB di Heroku.

Terakhir, kita harus membuat file bernama Procfile di tingkat root proyek kita, yang menentukan perintah yang dijalankan oleh aplikasi saat Heroku memulainya.

Procfile kami sesederhana ini:

 web: node index.js

Buat file dan komit. Setelah Anda mendorong komit, Heroku akan secara otomatis menerapkan aplikasi Anda, yang akan dapat diakses sebagai https://[YOUR_UNIQUE_APP_NAME].herokuapp.com/ .

Untuk memeriksa apakah itu berfungsi, kami dapat mengirim permintaan yang sama dengan yang kami kirim ke localhost .

Node.js, Express, MongoDB, Cron, dan Heroku: Kami Sudah Setengah Jalan!

Setelah komit ketiga kami, seperti inilah tampilan repo kami.

Sejauh ini, kami telah mengimplementasikan REST API berbasis Node.js/Express di bagian belakang kami, pembaru yang menggunakan API GitHub, dan tugas cron untuk mengaktifkannya. Kemudian kami telah menerapkan back end kami yang nantinya akan menyediakan data untuk generator konten web statis kami menggunakan Heroku dengan kait untuk integrasi berkelanjutan. Sekarang Anda siap untuk bagian kedua, di mana kami menerapkan ujung depan dan menyelesaikan aplikasi!

Terkait: 10 Kesalahan Paling Umum Yang Dilakukan Pengembang Node.js