Membangun Aplikasi Pemberitahu Harga Saham Menggunakan React, Apollo GraphQL, dan Hasura

Diterbitkan: 2022-03-10
Ringkasan cepat Dalam artikel ini, kita akan mempelajari cara membuat aplikasi berbasis peristiwa dan mengirim pemberitahuan push web saat peristiwa tertentu dipicu. Kami akan menyiapkan tabel database, peristiwa, dan pemicu terjadwal pada mesin Hasura GraphQL dan menghubungkan titik akhir GraphQL ke aplikasi front-end untuk mencatat preferensi harga saham pengguna.

Konsep mendapatkan pemberitahuan ketika peristiwa pilihan Anda telah terjadi telah menjadi populer dibandingkan dengan terpaku pada aliran data yang berkelanjutan untuk menemukan sendiri kejadian tertentu itu. Orang-orang lebih suka mendapatkan email/pesan yang relevan ketika acara yang mereka sukai telah terjadi daripada terpaku pada layar untuk menunggu acara itu terjadi. Terminologi berbasis peristiwa juga cukup umum di dunia perangkat lunak.

Betapa hebatnya jika Anda bisa mendapatkan pembaruan harga saham favorit Anda di ponsel Anda?

Pada artikel ini, kita akan membangun aplikasi Stocks Price Notifier dengan menggunakan engine React, Apollo GraphQL, dan Hasura GraphQL. Kita akan memulai proyek dari kode boilerplate create-react-app dan akan membangun semuanya dari awal. Kita akan mempelajari cara mengatur tabel database, dan event di konsol Hasura. Kami juga akan mempelajari cara menghubungkan acara Hasura untuk mendapatkan pembaruan harga saham menggunakan pemberitahuan web-push.

Berikut sekilas tentang apa yang akan kami bangun:

Gambaran Umum Aplikasi Pemberitahu Harga Saham
Aplikasi Pemberitahuan Harga Saham

Mari kita pergi!

Lebih banyak setelah melompat! Lanjutkan membaca di bawah ini

Ikhtisar Tentang Apa Proyek Ini

Data stok (termasuk metrik seperti tinggi , rendah , buka , tutup , volume ) akan disimpan dalam database Postgres yang didukung Hasura. Pengguna akan dapat berlangganan saham tertentu berdasarkan beberapa nilai atau dia dapat memilih untuk mendapatkan pemberitahuan setiap jam. Pengguna akan mendapatkan pemberitahuan web-push setelah kriteria langganannya terpenuhi.

Ini terlihat seperti banyak hal dan jelas akan ada beberapa pertanyaan terbuka tentang bagaimana kita akan membangun potongan-potongan ini.

Berikut adalah rencana tentang bagaimana kami akan menyelesaikan proyek ini dalam empat langkah:

  1. Mengambil data stok menggunakan skrip NodeJs
    Kita akan mulai dengan mengambil data stok menggunakan skrip NodeJs sederhana dari salah satu penyedia API stok — Alpha Vantage. Skrip ini akan mengambil data untuk stok tertentu dalam interval 5 menit. Respon dari API termasuk tinggi , rendah , buka , tutup dan volume . Data ini kemudian akan dimasukkan ke dalam database Postgres yang terintegrasi dengan back-end Hasura.
  2. Menyiapkan mesin Hasura GraphQL
    Kami kemudian akan menyiapkan beberapa tabel di database Postgres untuk merekam titik data. Hasura secara otomatis menghasilkan skema GraphQL, kueri, dan mutasi untuk tabel ini.
  3. Front-end menggunakan React dan Apollo Client
    Langkah selanjutnya adalah mengintegrasikan lapisan GraphQL menggunakan klien Apollo dan Penyedia Apollo (titik akhir GraphQL disediakan oleh Hasura). Data-poin akan ditampilkan sebagai grafik di front-end. Kami juga akan membangun opsi berlangganan dan akan mengaktifkan mutasi yang sesuai pada lapisan GraphQL.
  4. Menyiapkan pemicu Acara/Terjadwal
    Hasura menyediakan perkakas yang sangat baik di sekitar pemicu. Kami akan menambahkan pemicu acara & terjadwal pada tabel data saham. Pemicu ini akan diatur jika pengguna tertarik untuk mendapatkan pemberitahuan saat harga saham mencapai nilai tertentu (pemicu peristiwa). Pengguna juga dapat memilih untuk mendapatkan pemberitahuan tentang stok tertentu setiap jam (pemicu terjadwal).

Sekarang rencana sudah siap, mari kita lakukan!

Inilah repositori GitHub untuk proyek ini. Jika Anda tersesat di mana pun dalam kode di bawah ini, lihat repositori ini dan kembalilah ke kecepatan!

Mengambil Data Saham Menggunakan Skrip NodeJs

Ini tidak serumit kedengarannya! Kita harus menulis fungsi yang mengambil data menggunakan titik akhir Alpha Vantage dan panggilan pengambilan ini harus diaktifkan dalam interval 5 menit (Anda dapat menebaknya dengan benar, kita harus meletakkan panggilan fungsi ini di setInterval ).

Jika Anda masih bertanya-tanya apa itu Alpha Vantage dan hanya ingin menghilangkan ini dari kepala Anda sebelum melompat ke bagian pengkodean, maka ini dia:

Alpha Vantage Inc. adalah penyedia terkemuka API gratis untuk data realtime dan historis tentang saham, forex (FX), dan digital/cryptocurrency.

Kami akan menggunakan titik akhir ini untuk mendapatkan metrik yang diperlukan dari saham tertentu. API ini mengharapkan kunci API sebagai salah satu parameter. Anda bisa mendapatkan kunci API gratis dari sini. Sekarang kita sudah bisa masuk ke bagian yang menarik — mari kita mulai menulis beberapa kode!

Menginstal Dependensi

Buat direktori stocks-app dan buat direktori server di dalamnya. Inisialisasi sebagai proyek simpul menggunakan npm init dan kemudian instal dependensi ini:

 npm i isomorphic-fetch pg nodemon --save

Ini adalah satu-satunya tiga dependensi yang kita perlukan untuk menulis skrip ini untuk mengambil harga saham dan menyimpannya di database Postgres.

Berikut penjelasan singkat tentang dependensi ini:

  • isomorphic-fetch
    Itu membuatnya mudah untuk menggunakan fetch secara isomorfik (dalam bentuk yang sama) pada klien dan server.
  • pg
    Ini adalah klien PostgreSQL yang tidak memblokir untuk NodeJs.
  • nodemon
    Secara otomatis me-restart server pada setiap perubahan file dalam direktori.

Menyiapkan konfigurasi

Tambahkan file config.js di tingkat root. Tambahkan potongan kode di bawah ini dalam file itu untuk saat ini:

 const config = { user: '<DATABASE_USER>', password: '<DATABASE_PASSWORD>', host: '<DATABASE_HOST>', port: '<DATABASE_PORT>', database: '<DATABASE_NAME>', ssl: '<IS_SSL>', apiHost: 'https://www.alphavantage.co/', }; module.exports = config;

user , password , host , port , database , ssl terkait dengan konfigurasi Postgres. Kami akan kembali untuk mengedit ini saat kami menyiapkan bagian mesin Hasura!

Menginisialisasi Kumpulan Koneksi Postgres Untuk Menanyakan Basis Data

Kumpulan connection pool adalah istilah umum dalam ilmu komputer dan Anda akan sering mendengar istilah ini saat berurusan dengan database.

Saat melakukan kueri data dalam database, Anda harus terlebih dahulu membuat koneksi ke database. Koneksi ini mengambil kredensial database dan memberi Anda pengait untuk menanyakan tabel mana pun dalam database.

Catatan : Membangun koneksi database mahal dan juga membuang-buang sumber daya yang signifikan. Kumpulan koneksi menyimpan koneksi database dan menggunakannya kembali pada kueri berikutnya. Jika semua koneksi terbuka sedang digunakan, maka koneksi baru dibuat dan kemudian ditambahkan ke kumpulan.

Sekarang setelah jelas apa kumpulan koneksi itu dan untuk apa digunakan, mari kita mulai dengan membuat instance kumpulan koneksi pg untuk aplikasi ini:

Tambahkan file pool.js di tingkat root dan buat instance pool sebagai:

 const { Pool } = require('pg'); const config = require('./config'); const pool = new Pool({ user: config.user, password: config.password, host: config.host, port: config.port, database: config.database, ssl: config.ssl, }); module.exports = pool;

Baris kode di atas membuat instance Pool dengan opsi konfigurasi seperti yang diatur dalam file konfigurasi. Kami belum menyelesaikan file konfigurasi tetapi tidak akan ada perubahan apa pun terkait dengan opsi konfigurasi.

Kami sekarang telah menetapkan dasar dan siap untuk mulai membuat beberapa panggilan API ke titik akhir Alpha Vantage.

Mari kita masuk ke bagian yang menarik!

Mengambil Data Saham

Di bagian ini, kita akan mengambil data stok dari titik akhir Alpha Vantage. Berikut file index.js :

 const fetch = require('isomorphic-fetch'); const getConfig = require('./config'); const { insertStocksData } = require('./queries'); const symbols = [ 'NFLX', 'MSFT', 'AMZN', 'W', 'FB' ]; (function getStocksData () { const apiConfig = getConfig('apiHostOptions'); const { host, timeSeriesFunction, interval, key } = apiConfig; symbols.forEach((symbol) => { fetch(`${host}query/?function=${timeSeriesFunction}&symbol=${symbol}&interval=${interval}&apikey=${key}`) .then((res) => res.json()) .then((data) => { const timeSeries = data['Time Series (5min)']; Object.keys(timeSeries).map((key) => { const dataPoint = timeSeries[key]; const payload = [ symbol, dataPoint['2. high'], dataPoint['3. low'], dataPoint['1. open'], dataPoint['4. close'], dataPoint['5. volume'], key, ]; insertStocksData(payload); }); }); }) })()

Untuk tujuan proyek ini, kami akan menanyakan harga hanya untuk saham ini — NFLX (Netflix), MSFT (Microsoft), AMZN (Amazon), W (Wayfair), FB (Facebook).

Lihat file ini untuk opsi konfigurasi. Fungsi getStocksData getStocksData tidak melakukan banyak hal! Itu mengulang simbol-simbol ini dan menanyakan titik akhir Alpha Vantage ${host}query/?function=${timeSeriesFunction}&symbol=${symbol}&interval=${interval}&apikey=${key} untuk mendapatkan metrik untuk saham ini.

Fungsi insertStocksData menempatkan titik data ini di database Postgres. Inilah fungsi insertStocksData :

 const insertStocksData = async (payload) => { const query = 'INSERT INTO stock_data (symbol, high, low, open, close, volume, time) VALUES ($1, $2, $3, $4, $5, $6, $7)'; pool.query(query, payload, (err, result) => { console.log('result here', err); }); };

Ini dia! Kami telah mengambil titik data stok dari Alpha Vantage API dan telah menulis sebuah fungsi untuk meletakkannya di database Postgres di tabel stock_data . Hanya ada satu bagian yang hilang untuk membuat semua ini berhasil! Kami telah mengisi nilai yang benar dalam file konfigurasi. Kami akan mendapatkan nilai-nilai ini setelah menyiapkan mesin Hasura. Mari kita lakukan itu segera!

Silakan merujuk ke direktori server untuk kode lengkap dalam mengambil titik data dari titik akhir Alpha Vantage dan mengisinya ke database Hasura Postgres.

Jika pendekatan pengaturan koneksi, opsi konfigurasi, dan penyisipan data menggunakan kueri mentah ini terlihat agak sulit, jangan khawatir tentang itu! Kita akan belajar bagaimana melakukan semua ini dengan mudah dengan mutasi GraphQL setelah mesin Hasura diatur!

Menyiapkan Mesin Hasura GraphQL

Sangat mudah untuk menyiapkan mesin Hasura dan memulai dan menjalankan dengan skema GraphQL, kueri, mutasi, langganan, pemicu peristiwa, dan banyak lagi!

Klik Coba Hasura dan masukkan nama proyek:

Membuat Proyek Hasura
Membuat Proyek Hasura. (Pratinjau besar)

Saya menggunakan database Postgres yang dihosting di Heroku. Buat database di Heroku dan tautkan ke proyek ini. Anda kemudian harus siap untuk mengalami kekuatan konsol Hasura yang kaya kueri.

Silakan salin URL DB Postgres yang akan Anda dapatkan setelah membuat proyek. Kita harus meletakkan ini di file konfigurasi.

Klik Launch Console dan Anda akan diarahkan ke tampilan ini:

Konsol Hasura
Konsol Hasura. (Pratinjau besar)

Mari kita mulai membangun skema tabel yang kita perlukan untuk proyek ini.

Membuat Skema Tabel Pada Database Postgres

Silakan buka tab Data dan klik Tambah Tabel! Mari kita mulai membuat beberapa tabel:

tabel symbol

Tabel ini akan digunakan untuk menyimpan informasi dari simbol-simbol. Untuk saat ini, saya menyimpan dua bidang di sini — id dan company . id bidang adalah kunci utama dan company bertipe varchar . Mari tambahkan beberapa simbol dalam tabel ini:

tabel simbol
tabel symbol . (Pratinjau besar)

tabel stock_data

Tabel stock_data menyimpan id , symbol , time dan metrik seperti high , low , open , close , volume . Skrip NodeJs yang kita tulis sebelumnya di bagian ini akan digunakan untuk mengisi tabel khusus ini.

Berikut tampilan tabelnya:

tabel stock_data
tabel stock_data . (Pratinjau besar)

Rapi! Mari kita beralih ke tabel lain dalam skema database!

user_subscription

Tabel user_subscription menyimpan objek langganan terhadap ID pengguna. Objek langganan ini digunakan untuk mengirimkan pemberitahuan web-push kepada pengguna. Kita akan belajar nanti di artikel cara membuat objek langganan ini.

Ada dua bidang dalam tabel ini — id adalah kunci utama dari tipe uuid dan bidang langganan bertipe jsonb .

meja events

Ini adalah yang penting dan digunakan untuk menyimpan opsi acara notifikasi. Saat pengguna memilih ikut serta untuk pembaruan harga saham tertentu, kami menyimpan informasi peristiwa tersebut di tabel ini. Tabel ini berisi kolom berikut:

  • id : adalah kunci utama dengan properti peningkatan otomatis.
  • symbol : adalah bidang teks.
  • user_id : bertipe uuid .
  • trigger_type : digunakan untuk menyimpan jenis pemicu acara — time/event .
  • trigger_value : digunakan untuk menyimpan nilai trigger. Misalnya, jika pengguna telah memilih untuk ikut serta dalam pemicu peristiwa berbasis harga — dia menginginkan pembaruan jika harga saham telah mencapai 1000, maka trigger_value akan menjadi 1000 dan trigger_type akan menjadi event .

Ini semua tabel yang kita perlukan untuk proyek ini. Kami juga harus mengatur hubungan antara tabel-tabel ini untuk memiliki aliran data dan koneksi yang lancar. Ayo lakukan itu!

Menyiapkan hubungan antar tabel

Tabel events digunakan untuk mengirim pemberitahuan web-push berdasarkan nilai peristiwa. Jadi, masuk akal untuk menghubungkan tabel ini dengan tabel user_subscription untuk dapat mengirim pemberitahuan push pada langganan yang disimpan dalam tabel ini.

 events.user_id → user_subscription.id

Tabel stock_data terkait dengan tabel simbol sebagai:

 stock_data.symbol → symbol.id

Kita juga harus membangun beberapa relasi pada tabel symbol sebagai berikut:

 stock_data.symbol → symbol.id events.symbol → symbol.id

Kami sekarang telah membuat tabel yang diperlukan dan juga menetapkan hubungan di antara mereka! Mari beralih ke tab GRAPHIQL di konsol untuk melihat keajaibannya!

Hasura telah menyiapkan kueri GraphQL berdasarkan tabel berikut:

Kueri/Mutasi GraphQL di konsol Hasura
Kueri/Mutasi GraphQL di konsol Hasura. (Pratinjau besar)

Sangat mudah untuk melakukan kueri pada tabel ini dan Anda juga dapat menerapkan salah satu dari filter/properti ini ( order_by , limit , offset , distinct_on , where ) untuk mendapatkan data yang diinginkan.

Ini semua terlihat bagus tetapi kami masih belum menghubungkan kode sisi server kami ke konsol Hasura. Mari kita selesaikan sedikit itu!

Menghubungkan Skrip NodeJs Ke Database Postgres

Silakan masukkan opsi yang diperlukan dalam file config.js di direktori server sebagai:

 const config = { databaseOptions: { user: '<DATABASE_USER>', password: '<DATABASE_PASSWORD>', host: '<DATABASE_HOST>', port: '<DATABASE_PORT>', database: '<DATABASE_NAME>', ssl: true, }, apiHostOptions: { host: 'https://www.alphavantage.co/', key: '<API_KEY>', timeSeriesFunction: 'TIME_SERIES_INTRADAY', interval: '5min' }, graphqlURL: '<GRAPHQL_URL>' }; const getConfig = (key) => { return config[key]; }; module.exports = getConfig;

Silakan masukkan opsi ini dari string database yang dihasilkan saat kami membuat database Postgres di Heroku.

apiHostOptions terdiri dari opsi terkait API seperti host , key , timeSeriesFunction dan interval .

Anda akan mendapatkan bidang graphqlURL di tab GRAPHIQL di konsol Hasura.

Fungsi getConfig digunakan untuk mengembalikan nilai yang diminta dari objek konfigurasi. Kami telah menggunakan ini di index.js di direktori server .

Saatnya menjalankan server dan mengisi beberapa data dalam database. Saya telah menambahkan satu skrip di package.json sebagai:

 "scripts": { "start": "nodemon index.js" }

Jalankan npm start di terminal dan titik data array simbol di index.js harus diisi dalam tabel.

Memfaktorkan Ulang Permintaan Mentah Dalam Skrip NodeJs Menjadi Mutasi GraphQL

Sekarang Hasura engine sudah diatur, mari kita lihat betapa mudahnya memanggil mutasi pada tabel stock_data .

Fungsi insertStocksData di queries.js menggunakan kueri mentah:

 const query = 'INSERT INTO stock_data (symbol, high, low, open, close, volume, time) VALUES ($1, $2, $3, $4, $5, $6, $7)';

Mari kita perbaiki kueri ini dan gunakan mutasi yang diberdayakan oleh mesin Hasura. Berikut adalah queries.js refactored di direktori server:

 const { createApolloFetch } = require('apollo-fetch'); const getConfig = require('./config'); const GRAPHQL_URL = getConfig('graphqlURL'); const fetch = createApolloFetch({ uri: GRAPHQL_URL, }); const insertStocksData = async (payload) => { const insertStockMutation = await fetch({ query: `mutation insertStockData($objects: [stock_data_insert_input!]!) { insert_stock_data (objects: $objects) { returning { id } } }`, variables: { objects: payload, }, }); console.log('insertStockMutation', insertStockMutation); }; module.exports = { insertStocksData }

Harap dicatat: Kami telah menambahkan graphqlURL di file config.js .

Modul apollo-fetch mengembalikan fungsi fetch yang dapat digunakan untuk mengkueri/memutasi tanggal pada titik akhir GraphQL. Cukup mudah, bukan?

Satu-satunya perubahan yang harus kita lakukan di index.js adalah mengembalikan objek saham dalam format seperti yang diminta oleh fungsi insertStocksData . Silakan periksa index2.js dan queries2.js untuk kode lengkap dengan pendekatan ini.

Sekarang setelah kita menyelesaikan sisi data proyek, mari beralih ke bit front-end dan membangun beberapa komponen yang menarik!

Catatan : Kita tidak perlu menyimpan opsi konfigurasi database dengan pendekatan ini!

Front-end Menggunakan React Dan Klien Apollo

Proyek front-end berada di repositori yang sama dan dibuat menggunakan paket create-react-app . Pekerja layanan yang dibuat menggunakan paket ini mendukung cache aset tetapi tidak mengizinkan lebih banyak penyesuaian ditambahkan ke file pekerja layanan. Sudah ada beberapa masalah terbuka untuk menambahkan dukungan untuk opsi pekerja layanan kustom. Ada beberapa cara untuk mengatasi masalah ini dan menambahkan dukungan untuk pekerja layanan kustom.

Mari kita mulai dengan melihat struktur untuk proyek front-end:

Direktori Proyek
Direktori Proyek. (Pratinjau besar)

Silakan periksa direktori src ! Jangan khawatir tentang file terkait pekerja layanan untuk saat ini. Kita akan mempelajari lebih lanjut tentang file-file ini nanti di bagian ini. Sisa struktur proyek terlihat sederhana. Folder components akan memiliki komponen (Loader, Bagan); folder services berisi beberapa fungsi/layanan pembantu yang digunakan untuk mengubah objek dalam struktur yang diperlukan; styles seperti namanya berisi file sass yang digunakan untuk menata proyek; views adalah direktori utama dan berisi komponen view layer.

Kami hanya membutuhkan dua komponen tampilan untuk proyek ini — Daftar Simbol dan Rangkaian Waktu Simbol. Kami akan membangun deret waktu menggunakan komponen Bagan dari pustaka grafik tinggi. Mari mulai menambahkan kode dalam file-file ini untuk membangun bagian-bagian di front-end!

Menginstal Dependensi

Berikut daftar dependensi yang kita perlukan:

  • apollo-boost
    Apollo boost adalah cara tanpa konfigurasi untuk mulai menggunakan Apollo Client. Itu datang dibundel dengan opsi konfigurasi default.
  • reactstrap dan bootstrap
    Komponen dibangun menggunakan dua paket ini.
  • graphql dan graphql-type-json
    graphql adalah ketergantungan yang diperlukan untuk menggunakan apollo-boost dan graphql-type-json digunakan untuk mendukung tipe data json yang digunakan dalam skema GraphQL.
  • highcharts dan highcharts-react-official
    Dan kedua paket ini akan digunakan untuk membangun grafik:

  • node-sass
    Ini ditambahkan untuk mendukung file sass untuk penataan.

  • uuid
    Paket ini digunakan untuk menghasilkan nilai acak yang kuat.

Semua dependensi ini akan masuk akal setelah kita mulai menggunakannya dalam proyek. Mari kita ke bagian berikutnya!

Menyiapkan Klien Apollo

Buat apolloClient.js di dalam folder src sebagai:

 import ApolloClient from 'apollo-boost'; const apolloClient = new ApolloClient({ uri: '<HASURA_CONSOLE_URL>' }); export default apolloClient;

Kode di atas memberi contoh ApolloClient dan mengambil uri dalam opsi konfigurasi. uri adalah URL konsol Hasura Anda. Anda akan mendapatkan bidang uri ini pada tab GRAPHIQL di bagian Titik Akhir GraphQL .

Kode di atas terlihat sederhana tetapi menangani bagian utama proyek! Ini menghubungkan skema GraphQL yang dibangun di Hasura dengan proyek saat ini.

Kita juga harus meneruskan objek klien apollo ini ke ApolloProvider dan membungkus komponen root di dalam ApolloProvider . Ini akan memungkinkan semua komponen bersarang di dalam komponen utama untuk menggunakan prop client dan menjalankan kueri pada objek klien ini.

Mari kita ubah file index.js menjadi:

 const Wrapper = () => { /* some service worker logic - ignore for now */ const [insertSubscription] = useMutation(subscriptionMutation); useEffect(() => { serviceWorker.register(insertSubscription); }, []) /* ignore the above snippet */ return <App />; } ReactDOM.render( <ApolloProvider client={apolloClient}> <Wrapper /> </ApolloProvider>, document.getElementById('root') );

Harap abaikan kode terkait insertSubscription . Kami akan memahaminya secara detail nanti. Sisa kode harus mudah untuk dijelajahi. Fungsi render mengambil komponen root dan elementId sebagai parameter. Perhatikan client (contoh ApolloClient) sedang diteruskan sebagai prop ke ApolloProvider . Anda dapat memeriksa file index.js lengkap di sini.

Menyiapkan Pekerja Layanan Kustom

Service worker adalah file JavaScript yang memiliki kemampuan untuk mencegat permintaan jaringan. Ini digunakan untuk menanyakan cache untuk memeriksa apakah aset yang diminta sudah ada di cache alih-alih melakukan perjalanan ke server. Pekerja layanan juga digunakan untuk mengirim pemberitahuan web-push ke perangkat berlangganan.

Kami telah mengirimkan pemberitahuan web-push untuk pembaruan harga saham kepada pengguna yang berlangganan. Mari kita mulai dan membangun file service worker ini!

Potongan terkait insertSubscription dalam file index.js melakukan pekerjaan mendaftarkan pekerja layanan dan menempatkan objek langganan dalam database menggunakan subscriptionMutation .

Silakan lihat query.js untuk semua kueri dan mutasi yang digunakan dalam proyek.

serviceWorker.register(insertSubscription); memanggil fungsi register yang ditulis dalam file serviceWorker.js . Ini dia:

 export const register = (insertSubscription) => { if ('serviceWorker' in navigator) { const swUrl = `${process.env.PUBLIC_URL}/serviceWorker.js` navigator.serviceWorker.register(swUrl) .then(() => { console.log('Service Worker registered'); return navigator.serviceWorker.ready; }) .then((serviceWorkerRegistration) => { getSubscription(serviceWorkerRegistration, insertSubscription); Notification.requestPermission(); }) } }

Fungsi di atas pertama-tama memeriksa apakah serviceWorker didukung oleh browser dan kemudian mendaftarkan file service worker yang dihosting di URL swUrl . Kami akan memeriksa file ini sebentar lagi!

Fungsi getSubscription melakukan pekerjaan untuk mendapatkan objek langganan menggunakan metode subscribe pada objek pushManager . Objek langganan ini kemudian disimpan dalam tabel user_subscription terhadap userId. Harap dicatat bahwa userId sedang dibuat menggunakan fungsi uuid . Mari kita periksa fungsi getSubscription :

 const getSubscription = (serviceWorkerRegistration, insertSubscription) => { serviceWorkerRegistration.pushManager.getSubscription() .then ((subscription) => { const userId = uuidv4(); if (!subscription) { const applicationServerKey = urlB64ToUint8Array('<APPLICATION_SERVER_KEY>') serviceWorkerRegistration.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey }).then (subscription => { insertSubscription({ variables: { userId, subscription } }); localStorage.setItem('serviceWorkerRegistration', JSON.stringify({ userId, subscription })); }) } }) }

Anda dapat memeriksa file serviceWorker.js untuk kode lengkapnya!

Munculan Pemberitahuan
Munculan Pemberitahuan. (Pratinjau besar)

Notification.requestPermission() memanggil popup ini yang meminta izin kepada pengguna untuk mengirim notifikasi. Setelah pengguna mengklik Izinkan, objek langganan dibuat oleh layanan push. Kami menyimpan objek itu di localStorage sebagai:

Objek Langganan Webpush
Objek Langganan Webpush. (Pratinjau besar)

Titik endpoint bidang pada objek di atas digunakan untuk mengidentifikasi perangkat dan server menggunakan titik akhir ini untuk mengirim pemberitahuan push web kepada pengguna.

Kami telah melakukan pekerjaan inisialisasi dan mendaftarkan pekerja layanan. Kami juga memiliki objek langganan pengguna! Ini berfungsi dengan baik karena file serviceWorker.js ada di folder public . Sekarang mari kita siapkan service worker untuk menyiapkan semuanya!

Ini adalah topik yang agak sulit tetapi mari kita selesaikan dengan benar! Seperti disebutkan sebelumnya, utilitas create-react-app tidak mendukung kustomisasi secara default untuk service worker. Kami dapat mencapai implementasi pekerja layanan pelanggan menggunakan modul workbox-build .

Kami juga harus memastikan bahwa perilaku default file pra-caching tetap utuh. Kami akan memodifikasi bagian di mana service worker akan dibangun dalam proyek. Dan, workbox-build membantu dalam mencapai hal itu! Barang rapi! Mari kita buat tetap sederhana dan buat daftar semua yang harus kita lakukan untuk membuat pekerja layanan kustom bekerja:

  • Tangani pra-caching aset menggunakan workboxBuild .
  • Buat template service worker untuk aset caching.
  • Buat file sw-precache-config.js untuk menyediakan opsi konfigurasi khusus.
  • Tambahkan skrip pekerja layanan build di langkah build di package.json .

Jangan khawatir jika semua ini terdengar membingungkan! Artikel ini tidak fokus menjelaskan semantik di balik masing-masing poin ini. Kami sudah fokus pada bagian implementasi untuk saat ini! Saya akan mencoba untuk menutupi alasan di balik melakukan semua pekerjaan untuk membuat pekerja layanan kustom di artikel lain.

Mari kita buat dua file sw-build.js dan sw-custom.js di direktori src . Silakan merujuk ke tautan ke file-file ini dan tambahkan kode ke proyek Anda.

Sekarang mari kita buat file sw-precache-config.js di tingkat root dan tambahkan kode berikut di file itu:

 module.exports = { staticFileGlobs: [ 'build/static/css/**.css', 'build/static/js/**.js', 'build/index.html' ], swFilePath: './build/serviceWorker.js', stripPrefix: 'build/', handleFetch: false, runtimeCaching: [{ urlPattern: /this\\.is\\.a\\.regex/, handler: 'networkFirst' }] }

Mari kita juga memodifikasi file package.json untuk memberikan ruang bagi pembuatan file pekerja layanan kustom:

Tambahkan pernyataan ini di bagian scripts :

 "build-sw": "node ./src/sw-build.js", "clean-cra-sw": "rm -f build/precache-manifest.*.js && rm -f build/service-worker.js",

Dan ubah skrip build sebagai:

 "build": "react-scripts build && npm run build-sw && npm run clean-cra-sw",

Pengaturan akhirnya selesai! Kita sekarang harus menambahkan file pekerja layanan kustom di dalam folder public :

 function showNotification (event) { const eventData = event.data.json(); const { title, body } = eventData self.registration.showNotification(title, { body }); } self.addEventListener('push', (event) => { event.waitUntil(showNotification(event)); })

Kami baru saja menambahkan satu pendengar push untuk mendengarkan pemberitahuan push yang dikirim oleh server. Fungsi showNotification digunakan untuk menampilkan pemberitahuan push web kepada pengguna.

Ini dia! Kami sudah selesai dengan semua kerja keras menyiapkan pekerja layanan khusus untuk menangani pemberitahuan push web. Kami akan melihat pemberitahuan ini beraksi setelah kami membangun antarmuka pengguna!

Kami semakin dekat untuk membangun potongan kode utama. Sekarang mari kita mulai dengan tampilan pertama!

Tampilan Daftar Simbol

Komponen App yang digunakan di bagian sebelumnya terlihat seperti ini:

 import React from 'react'; import SymbolList from './views/symbolList'; const App = () => { return <SymbolList />; }; export default App;

Ini adalah komponen sederhana yang mengembalikan tampilan SymbolList dan SymbolList melakukan semua tugas berat menampilkan simbol dalam antarmuka pengguna yang terikat rapi.

Mari kita lihat symbolList.js di dalam folder views :

Silakan merujuk ke file di sini!

Komponen mengembalikan hasil fungsi renderSymbols . Dan, data ini diambil dari database menggunakan kait useQuery sebagai:

 const { loading, error, data } = useQuery(symbolsQuery, {variables: { userId }});

symbolsQuery didefinisikan sebagai:

 export const symbolsQuery = gql` query getSymbols($userId: uuid) { symbol { id company symbol_events(where: {user_id: {_eq: $userId}}) { id symbol trigger_type trigger_value user_id } stock_symbol_aggregate { aggregate { max { high volume } min { low volume } } } } } `;

Dibutuhkan di userId dan mengambil acara berlangganan dari pengguna tertentu untuk menampilkan status ikon notifikasi yang benar (ikon lonceng yang ditampilkan bersama dengan judul). Kueri juga mengambil nilai maks dan min stok. Perhatikan penggunaan aggregate dalam kueri di atas. Kueri Agregasi Hasura melakukan pekerjaan di belakang layar untuk mengambil nilai agregat seperti count , sum , avg , max , min , dll.

Berdasarkan respon dari panggilan GraphQL di atas, berikut daftar kartu yang ditampilkan di front-end:

Kartu Stok
Kartu Stok. (Pratinjau besar)

Struktur HTML kartu terlihat seperti ini:

 <div key={id}> <div className="card-container"> <Card> <CardBody> <CardTitle className="card-title"> <span className="company-name">{company} </span> <Badge color="dark" pill>{id}</Badge> <div className={classNames({'bell': true, 'disabled': isSubscribed})} id={`subscribePopover-${id}`}> <FontAwesomeIcon icon={faBell} title="Subscribe" /> </div> </CardTitle> <div className="metrics"> <div className="metrics-row"> <span className="metrics-row--label">High:</span> <span className="metrics-row--value">{max.high}</span> <span className="metrics-row--label">{' '}(Volume: </span> <span className="metrics-row--value">{max.volume}</span>) </div> <div className="metrics-row"> <span className="metrics-row--label">Low: </span> <span className="metrics-row--value">{min.low}</span> <span className="metrics-row--label">{' '}(Volume: </span> <span className="metrics-row--value">{min.volume}</span>) </div> </div> <Button className="timeseries-btn" outline onClick={() => toggleTimeseries(id)}>Timeseries</Button>{' '} </CardBody> </Card> <Popover className="popover-custom" placement="bottom" target={`subscribePopover-${id}`} isOpen={isSubscribePopoverOpen === id} toggle={() => setSubscribeValues(id, symbolTriggerData)} > <PopoverHeader> Notification Options <span className="popover-close"> <FontAwesomeIcon icon={faTimes} onClick={() => handlePopoverToggle(null)} /> </span> </PopoverHeader> {renderSubscribeOptions(id, isSubscribed, symbolTriggerData)} </Popover> </div> <Collapse isOpen={expandedStockId === id}> { isOpen(id) ? <StockTimeseries symbol={id}/> : null } </Collapse> </div>

Kami menggunakan komponen Card dari ReactStrap untuk merender kartu-kartu ini. Komponen Popover digunakan untuk menampilkan opsi berbasis langganan:

Opsi Pemberitahuan
Opsi Pemberitahuan. (Pratinjau besar)

Ketika pengguna mengklik ikon bell untuk saham tertentu, dia dapat memilih untuk menerima pemberitahuan setiap jam atau ketika harga saham telah mencapai nilai yang dimasukkan. Kita akan melihat ini beraksi di bagian Pemicu Peristiwa/Waktu.

Catatan : Kita akan masuk ke komponen StockTimeseries di bagian selanjutnya!

Silakan merujuk ke symbolList.js untuk kode lengkap yang terkait dengan komponen daftar saham.

Tampilan Deret Waktu Saham

Komponen StockTimeseries menggunakan kueri stocksDataQuery :

 export const stocksDataQuery = gql` query getStocksData($symbol: String) { stock_data(order_by: {time: desc}, where: {symbol: {_eq: $symbol}}, limit: 25) { high low open close volume time } } `;

Kueri di atas mengambil 25 titik data terbaru dari stok yang dipilih. Misalnya, berikut adalah bagan untuk metrik pembukaan saham Facebook:

Garis waktu harga saham
Garis waktu harga saham. (Pratinjau besar)

Ini adalah komponen langsung di mana kami meneruskan beberapa opsi bagan ke komponen [ HighchartsReact ]. Berikut adalah opsi grafik:

 const chartOptions = { title: { text: `${symbol} Timeseries` }, subtitle: { text: 'Intraday (5min) open, high, low, close prices & volume' }, yAxis: { title: { text: '#' } }, xAxis: { title: { text: 'Time' }, categories: getDataPoints('time') }, legend: { layout: 'vertical', align: 'right', verticalAlign: 'middle' }, series: [ { name: 'high', data: getDataPoints('high') }, { name: 'low', data: getDataPoints('low') }, { name: 'open', data: getDataPoints('open') }, { name: 'close', data: getDataPoints('close') }, { name: 'volume', data: getDataPoints('volume') } ] }

Sumbu X menunjukkan waktu dan Sumbu Y menunjukkan nilai metrik pada waktu itu. Fungsi getDataPoints digunakan untuk menghasilkan serangkaian poin untuk setiap seri.

 const getDataPoints = (type) => { const values = []; data.stock_data.map((dataPoint) => { let value = dataPoint[type]; if (type === 'time') { value = new Date(dataPoint['time']).toLocaleString('en-US'); } values.push(value); }); return values; }

Sederhana! Begitulah cara komponen Bagan dibuat! Silakan merujuk ke file stockTimeseries.js untuk kode lengkap deret waktu stok.

Anda sekarang harus siap dengan data dan antarmuka pengguna bagian dari proyek. Sekarang mari beralih ke bagian yang menarik — menyiapkan pemicu acara/waktu berdasarkan input pengguna.

Menyiapkan Pemicu Acara/Terjadwal

Di bagian ini, kita akan mempelajari cara mengatur pemicu di konsol Hasura dan cara mengirim pemberitahuan push web ke pengguna yang dipilih. Mari kita mulai!

Pemicu Peristiwa Di Konsol Hasura

Mari buat event trigger stock_value pada tabel stock_data dan insert sebagai operasi trigger. Webhook akan berjalan setiap kali ada sisipan di tabel stock_data .

Penyiapan pemicu acara
Penyiapan pemicu peristiwa. (Pratinjau besar)

Kami akan membuat proyek kesalahan untuk URL webhook. Izinkan saya menjelaskan sedikit tentang webhook agar mudah dipahami:

Webhook digunakan untuk mengirim data dari satu aplikasi ke aplikasi lain tentang terjadinya peristiwa tertentu. Saat suatu peristiwa dipicu, panggilan HTTP POST dibuat ke URL webhook dengan data peristiwa sebagai payload.

In this case, when there is an insert operation on the stock_data table, an HTTP post call will be made to the configured webhook URL (post call in the glitch project).

Glitch Project For Sending Web-push Notifications

We've to get the webhook URL to put in the above event trigger interface. Go to glitch.com and create a new project. In this project, we'll set up an express listener and there will be an HTTP post listener. The HTTP POST payload will have all the details of the stock datapoint including open , close , high , low , volume , time . We'll have to fetch the list of users subscribed to this stock with the value equal to the close metric.

These users will then be notified of the stock price via web-push notifications.

That's all we've to do to achieve the desired target of notifying users when the stock price reaches the expected value!

Let's break this down into smaller steps and implement them!

Installing Dependencies

We would need the following dependencies:

  • express : is used for creating an express server.
  • apollo-fetch : is used for creating a fetch function for getting data from the GraphQL endpoint.
  • web-push : is used for sending web push notifications.

Please write this script in package.json to run index.js on npm start command:

 "scripts": { "start": "node index.js" }

Setting Up Express Server

Let's create an index.js file as:

 const express = require('express'); const bodyParser = require('body-parser'); const app = express(); app.use(bodyParser.json()); const handleStockValueTrigger = (eventData, res) => { /* Code for handling this trigger */ } app.post('/', (req, res) => { const { body } = req const eventType = body.trigger.name const eventData = body.event switch (eventType) { case 'stock-value-trigger': return handleStockValueTrigger(eventData, res); } }); app.get('/', function (req, res) { res.send('Hello World - For Event Triggers, try a POST request?'); }); var server = app.listen(process.env.PORT, function () { console.log(`server listening on port ${process.env.PORT}`); });

In the above code, we've created post and get listeners on the route / . get is simple to get around! We're mainly interested in the post call. If the eventType is stock-value-trigger , we'll have to handle this trigger by notifying the subscribed users. Let's add that bit and complete this function!

Mengambil Pengguna Berlangganan

 const fetch = createApolloFetch({ uri: process.env.GRAPHQL_URL }); const getSubscribedUsers = (symbol, triggerValue) => { return fetch({ query: `query getSubscribedUsers($symbol: String, $triggerValue: numeric) { events(where: {symbol: {_eq: $symbol}, trigger_type: {_eq: "event"}, trigger_value: {_gte: $triggerValue}}) { user_id user_subscription { subscription } } }`, variables: { symbol, triggerValue } }).then(response => response.data.events) } const handleStockValueTrigger = async (eventData, res) => { const symbol = eventData.data.new.symbol; const triggerValue = eventData.data.new.close; const subscribedUsers = await getSubscribedUsers(symbol, triggerValue); const webpushPayload = { title: `${symbol} - Stock Update`, body: `The price of this stock is ${triggerValue}` } subscribedUsers.map((data) => { sendWebpush(data.user_subscription.subscription, JSON.stringify(webpushPayload)); }) res.json(eventData.toString()); }

Dalam fungsi handleStockValueTrigger di atas, pertama-tama kita mengambil pengguna yang berlangganan menggunakan fungsi getSubscribedUsers . Kami kemudian mengirimkan pemberitahuan web-push ke masing-masing pengguna ini. Fungsi sendWebpush digunakan untuk mengirim notifikasi. Kita akan melihat implementasi web-push sebentar lagi.

Fungsi getSubscribedUsers menggunakan kueri:

 query getSubscribedUsers($symbol: String, $triggerValue: numeric) { events(where: {symbol: {_eq: $symbol}, trigger_type: {_eq: "event"}, trigger_value: {_gte: $triggerValue}}) { user_id user_subscription { subscription } } }

Kueri ini mengambil simbol stok dan nilai serta mengambil detail pengguna termasuk user-id dan user_subscription yang cocok dengan kondisi ini:

  • symbol yang sama dengan yang dilewatkan dalam muatan.
  • trigger_type sama dengan event .
  • trigger_value lebih besar dari atau sama dengan yang diteruskan ke fungsi ini ( close dalam kasus ini).

Setelah kami mendapatkan daftar pengguna, satu-satunya yang tersisa adalah mengirimkan pemberitahuan web-push kepada mereka! Ayo lakukan itu segera!

Mengirim Pemberitahuan Web-Push Ke Pengguna Berlangganan

Kami harus terlebih dahulu mendapatkan kunci VAPID publik dan pribadi untuk mengirim pemberitahuan web-push. Harap simpan kunci ini di file .env dan atur detail ini di index.js sebagai:

 webPush.setVapidDetails( 'mailto:<YOUR_MAIL_ID>', process.env.PUBLIC_VAPID_KEY, process.env.PRIVATE_VAPID_KEY ); const sendWebpush = (subscription, webpushPayload) => { webPush.sendNotification(subscription, webpushPayload).catch(err => console.log('error while sending webpush', err)) }

Fungsi sendNotification digunakan untuk mengirim web-push pada endpoint langganan yang disediakan sebagai parameter pertama.

Itu saja yang diperlukan untuk berhasil mengirim pemberitahuan web-push ke pengguna yang berlangganan. Berikut kode lengkap yang didefinisikan dalam index.js :

 const express = require('express'); const bodyParser = require('body-parser'); const { createApolloFetch } = require('apollo-fetch'); const webPush = require('web-push'); webPush.setVapidDetails( 'mailto:<YOUR_MAIL_ID>', process.env.PUBLIC_VAPID_KEY, process.env.PRIVATE_VAPID_KEY ); const app = express(); app.use(bodyParser.json()); const fetch = createApolloFetch({ uri: process.env.GRAPHQL_URL }); const getSubscribedUsers = (symbol, triggerValue) => { return fetch({ query: `query getSubscribedUsers($symbol: String, $triggerValue: numeric) { events(where: {symbol: {_eq: $symbol}, trigger_type: {_eq: "event"}, trigger_value: {_gte: $triggerValue}}) { user_id user_subscription { subscription } } }`, variables: { symbol, triggerValue } }).then(response => response.data.events) } const sendWebpush = (subscription, webpushPayload) => { webPush.sendNotification(subscription, webpushPayload).catch(err => console.log('error while sending webpush', err)) } const handleStockValueTrigger = async (eventData, res) => { const symbol = eventData.data.new.symbol; const triggerValue = eventData.data.new.close; const subscribedUsers = await getSubscribedUsers(symbol, triggerValue); const webpushPayload = { title: `${symbol} - Stock Update`, body: `The price of this stock is ${triggerValue}` } subscribedUsers.map((data) => { sendWebpush(data.user_subscription.subscription, JSON.stringify(webpushPayload)); }) res.json(eventData.toString()); } app.post('/', (req, res) => { const { body } = req const eventType = body.trigger.name const eventData = body.event switch (eventType) { case 'stock-value-trigger': return handleStockValueTrigger(eventData, res); } }); app.get('/', function (req, res) { res.send('Hello World - For Event Triggers, try a POST request?'); }); var server = app.listen(process.env.PORT, function () { console.log("server listening"); });

Mari kita uji aliran ini dengan berlangganan saham dengan beberapa nilai dan secara manual memasukkan nilai itu ke dalam tabel (untuk pengujian)!

Saya berlangganan AMZN dengan nilai 2000 dan kemudian memasukkan titik data ke dalam tabel dengan nilai ini. Begini cara aplikasi pemberi tahu saham memberi tahu saya tepat setelah penyisipan:

Memasukkan baris dalam tabel stock_data untuk pengujian
Memasukkan baris dalam tabel stock_data untuk pengujian. (Pratinjau besar)

Rapi! Anda juga dapat memeriksa log permintaan acara di sini:

Catatan Acara
Catatan Acara. (Pratinjau besar)

Webhook melakukan pekerjaan seperti yang diharapkan! Kami siap untuk pemicu acara sekarang!

Pemicu Terjadwal/Cron

Kami dapat mencapai pemicu berbasis waktu untuk memberi tahu pengguna pelanggan setiap jam menggunakan pemicu acara Cron sebagai:

Pengaturan Pemicu Cron/Terjadwal
Pengaturan Pemicu Cron/Terjadwal. (Pratinjau besar)

Kami dapat menggunakan URL webhook yang sama dan menangani pengguna yang berlangganan berdasarkan jenis peristiwa pemicu sebagai stock_price_time_based_trigger . Implementasinya mirip dengan pemicu berbasis peristiwa.

Kesimpulan

Pada artikel ini, kami membangun aplikasi notifikasi harga saham. Kami mempelajari cara mengambil harga menggunakan API Alpha Vantage dan menyimpan titik data di database Postgres yang didukung Hasura. Kami juga mempelajari cara menyiapkan mesin Hasura GraphQL dan membuat pemicu berbasis peristiwa dan terjadwal. Kami membangun proyek kesalahan untuk mengirim pemberitahuan web-push ke pengguna yang berlangganan.