Membangun Aplikasi Pemberitahu Harga Saham Menggunakan React, Apollo GraphQL, dan Hasura
Diterbitkan: 2022-03-10Konsep 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:
Mari kita pergi!
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:
- 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. - 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. - 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. - 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 menggunakanfetch
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:
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:
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 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:
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
: bertipeuuid
. -
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, makatrigger_value
akan menjadi 1000 dantrigger_type
akan menjadievent
.
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:
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:
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
danbootstrap
Komponen dibangun menggunakan dua paket ini. -
graphql
dangraphql-type-json
graphql
adalah ketergantungan yang diperlukan untuk menggunakanapollo-boost
dangraphql-type-json
digunakan untuk mendukung tipe datajson
yang digunakan dalam skema GraphQL. highcharts
danhighcharts-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!
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:
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:
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:
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:
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
.
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 denganevent
. -
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:
Rapi! Anda juga dapat memeriksa log permintaan acara di sini:
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:
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.