Crearea unei aplicații de notificare a prețurilor acțiunilor folosind React, Apollo GraphQL și Hasura
Publicat: 2022-03-10Conceptul de a fi notificat atunci când a avut loc evenimentul ales de dvs. a devenit popular în comparație cu a fi lipit pe fluxul continuu de date pentru a găsi acel apariție particulară. Oamenii preferă să primească e-mailuri/mesaje relevante atunci când a avut loc evenimentul lor preferat, decât să fie agățați de ecran pentru a aștepta ca evenimentul să aibă loc. Terminologia bazată pe evenimente este, de asemenea, destul de comună în lumea software-ului.
Cât de minunat ar fi acest lucru dacă ați putea obține actualizările prețului stocului dvs. preferat pe telefonul dvs.?
În acest articol, vom construi o aplicație de notificare a prețurilor stocurilor utilizând React, Apollo Graphql și Hasura GraphQL. Vom începe proiectul dintr-un cod de cazane de create-react-app
și vom construi totul în sus. Vom învăța cum să setăm tabelele bazei de date și evenimentele pe consola Hasura. Vom învăța, de asemenea, cum să conectăm evenimentele lui Hasura pentru a obține actualizări ale prețului acțiunilor utilizând notificări web-push.
Iată o privire rapidă asupra a ceea ce am construi:
Haide să mergem!
O privire de ansamblu despre ce este acest proiect
Datele stocurilor (inclusiv valori precum ridicate , scăzute , deschise , close , volum ) vor fi stocate într-o bază de date postgres Hasuras. Utilizatorul ar putea să se aboneze la un anumit stoc pe baza unei anumite valori sau poate opta pentru a primi notificări la fiecare oră. Utilizatorul va primi o notificare web-push odată ce criteriile sale de abonament sunt îndeplinite.
Acestea arată o mulțime de lucruri și, evident, ar fi câteva întrebări deschise despre cum vom construi aceste piese.
Iată un plan despre cum am realiza acest proiect în patru etape:
- Preluarea datelor stocurilor folosind un script NodeJs
Vom începe prin a prelua datele stocurilor folosind un script NodeJs simplu de la unul dintre furnizorii de API-uri de stocuri — Alpha Vantage. Acest script va prelua datele pentru un anumit stoc la intervale de 5 minute. Răspunsul API include ridicat , scăzut , deschis , închidere și volum . Aceste date vor fi apoi inserate în baza de date Postgres care este integrată cu back-end-ul Hasura. - Configurarea motorului Hasura GraphQL
Apoi vom configura câteva tabele în baza de date Postgres pentru a înregistra punctele de date. Hasura generează automat schemele, interogările și mutațiile GraphQL pentru aceste tabele. - Front-end folosind React și Apollo Client
Următorul pas este integrarea stratului GraphQL utilizând clientul Apollo și furnizorul Apollo (punctul de punct grafic furnizat de Hasura). Punctele de date vor fi afișate ca diagrame pe front-end. De asemenea, vom construi opțiunile de abonament și vom declanșa mutațiile corespunzătoare pe stratul GraphQL. - Configurarea declanșatoarelor de evenimente/programate
Hasura oferă un instrument excelent în jurul declanșatorilor. Vom adăuga evenimente și declanșatori programați pe tabelul de date Stocks. Aceste declanșatoare vor fi stabilite dacă utilizatorul este interesat să primească o notificare atunci când prețurile acțiunilor ajung la o anumită valoare (declanșatorul evenimentului). De asemenea, utilizatorul poate opta pentru a primi o notificare a unui anumit stoc în fiecare oră (declanșare programată).
Acum că planul este gata, să-l punem în practică!
Iată depozitul GitHub pentru acest proiect. Dacă vă rătăciți în codul de mai jos, consultați acest depozit și reveniți la viteză!
Preluarea datelor bursiere folosind un script NodeJs
Acest lucru nu este atât de complicat pe cât pare! Va trebui să scriem o funcție care iau datele utilizând punctul final alfa Vantage și acest apel de preluare ar trebui să fie concediat într-un interval de 5 minute (ați ghicit-o drept, va trebui să punem această funcție funcțională în setInterval
).
Dacă încă mai întrebi ce este Alpha Vantage și doriți doar să scoateți acest lucru din capul dvs. înainte de a sări peste partea de codificare, atunci aici este:
Alpha Vantage Inc. este un furnizor principal de API-uri gratuite pentru date în timp real și istorice despre stocuri, Forex (FX) și digital / criptocurrens.
Am folosi acest punct final pentru a obține valorile necesare pentru un anumit stoc. Acest API așteaptă o cheie API ca unul dintre parametri. Puteți obține cheia API gratuită de aici. Acum suntem bine să trecem la partea interesantă - să începem să scriem ceva cod!
Instalarea dependențelor
Creați un director stocks-app
și creați un director de server
în interiorul acestuia. Inițializați-l ca proiect nod folosind npm init
și apoi instalați aceste dependențe:
npm i isomorphic-fetch pg nodemon --save
Acestea sunt singurele trei dependențe pe care trebuiau să le scriem acest scenariu de a prelua prețurile acțiunilor și de a le depozita în baza de date postgres.
Iată o scurtă explicație a acestor dependențe:
-
isomorphic-fetch
Face ușoară utilizareafetch
ului izomorf (în aceeași formă) atât pe client, cât și pe server. -
pg
Este un client non-blocant postgreSQL pentru NODEJS. -
nodemon
Repornește automat serverul la orice modificare a fișierelor din director.
Configurarea configurației
Adăugați un fișier config.js
la nivel de rădăcină. Adăugați fragmentul de cod de mai jos în acel fișier pentru moment:
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
, baza de database
, ssl
sunt legate de configurația Postgres. Vom reveni pentru a edita acest lucru în timp ce am înființat partea motorului Hasura!
Inițializarea bazei de conexiuni postgres pentru a interoga baza de date
O connection pool
este un termen comun în domeniul informaticii și veți auzi adesea acest termen în timp ce se ocupă de bazele de date.
În timp ce interogați date în bazele de date, va trebui mai întâi să stabiliți o conexiune la baza de date. Această conexiune preia acreditările bazei de date și vă oferă un cârlig pentru a interoga oricare dintre tabelele din baza de date.
Notă : stabilirea conexiunilor la baze de date este costisitoare și, de asemenea, irosește resurse semnificative. Un pool de conexiuni memorează în cache conexiunile la baza de date și le reutiliza la interogările ulterioare. Dacă toate conexiunile deschise sunt utilizate, atunci se stabilește o nouă conexiune și apoi este adăugată la piscină.
Acum că este clar ce este pool-ul de conexiuni și pentru ce este folosit, să începem prin a crea o instanță a pool-ului de conexiuni pg
pentru această aplicație:
Adăugați fișierul pool.js
la nivelul rădăcinii și creați o instanță de piscină ca:
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;
Linile de cod de mai sus creează o instanță de Pool
cu opțiunile de configurare ca setate în fișierul config. Încă nu vom finaliza fișierul de configurare, dar nu vor exista modificări legate de opțiunile de configurare.
Acum am pregătit terenul și suntem gata să începem să facem niște apeluri API către punctul final Alpha Vantage.
Să trecem la partea interesantă!
Preluarea datelor stocurilor
În această secțiune, vom prelua datele stocurilor de la punctul final Alpha Vantage. Iată fișierul 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); }); }); }) })()
În scopul acestui proiect, vom interoga prețurile numai pentru aceste stocuri — NFLX (Netflix), MSFT (Microsoft), AMZN (Amazon), W (Wayfair), FB (Facebook).
Consultați acest fișier pentru opțiunile de configurare. Funcția IIFE getStocksData
nu face mare lucru! Îmbunătățește aceste simboluri și interogări ale lui Alpha Vantage ${host}query/?function=${timeSeriesFunction}&symbol=${symbol}&interval=${interval}&apikey=${key}
pentru a obține valorile pentru aceste stocuri.
Funcția insertStocksData
pune aceste puncte de date în baza de date Postgres. Iată funcția 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); }); };
Asta este! Am preluat puncte de date ale stocului din API-ul Alpha Vantage și am scris o funcție pentru a le pune în baza de date Postgres în tabelul stock_data
. Lipsește doar o singură piesă pentru ca toate acestea să funcționeze! Trebuie să populam valorile corecte în fișierul de configurare. Vom obține aceste valori după configurarea motorului Hasura. Să ajungem la asta imediat!
Vă rugăm să consultați directorul server
pentru codul complet despre preluarea punctelor de date de la punctul final Alpha Vantage și popularea acestora în baza de date Hasura Postgres.
Dacă această abordare de a seta conexiuni, opțiuni de configurare și inserarea datelor folosind interogarea brută pare puțin dificilă, vă rugăm să nu vă faceți griji pentru asta! Vom învăța cum să facem tot acest mod ușor cu o mutație grafică odată ce motorul Hasura este configurat!
Configurarea motorului Hasura GraphQL
Este foarte simplu să configurați motorul Hasura și să începeți să rulați cu schemele GraphQL, interogări, mutații, abonamente, declanșatoare de evenimente și multe altele!
Faceți clic pe Încercați Hasura și introduceți numele proiectului:
Folosesc baza de date Postgres găzduită pe Heroku. Creați o bază de date pe Heroku și conectați-o la acest proiect. Ar trebui să fiți gata să experimentați puterea consolei Hasura, bogată în interogări.
Vă rugăm să copiați adresa URL Postgres DB pe care o veți obține după crearea proiectului. Va trebui să punem asta în fișierul de configurare.
Faceți clic pe Consola de lansare și veți fi redirecționat în această privință:
Să începem să construim schema tabelului de care avem nevoie pentru acest proiect.
Crearea unei scheme de tabele în baza de date Postgres
Accesați fila Date și faceți clic pe Adăugați tabel! Să începem să creăm câteva dintre tabele:
tabelul de symbol
Acest tabel ar fi folosit pentru stocarea informațiilor despre simboluri. Deocamdată, am păstrat două câmpuri aici — id
și company
. id
-ul câmpului este o cheie primară, iar company
este de tip varchar
. Să adăugăm câteva dintre simbolurile din acest tabel:
tabelul de stock_data
Tabelul stock_data
stochează id
-ul, symbol
, time
și valorile precum high
, low
, open
, close
, volume
. Scriptul NodeJs pe care l-am scris mai devreme în această secțiune va fi folosit pentru a popula acest tabel special.
Iată cum arată tabelul:
Îngrijit! Să ajungem la celălalt tabel din schema bazei de date!
tabelul user_subscription
Tabelul user_subscription
stochează obiectul de abonament pe ID-ul utilizatorului. Acest obiect de abonament este utilizat pentru a trimite notificări web-push către utilizatori. Vom afla mai târziu în articol cum să generăm acest obiect de abonament.
Există două câmpuri în acest tabel — id
este cheia primară de tip uuid
și câmpul de abonament este de tip jsonb
.
tabelul de events
Acesta este cel important și este folosit pentru stocarea opțiunilor de eveniment de notificare. Când un utilizator optează pentru actualizările de preț ale unui anumit stoc, stochează aceste informații despre eveniment în acest tabel. Acest tabel conține aceste coloane:
-
id
: este o cheie primară cu proprietatea de auto-increment. -
symbol
: este un câmp de text. -
user_id
: este de tipuuid
. -
trigger_type
: este folosit pentru stocarea tipului de declanșare a evenimentului —time/event
. -
trigger_value
: este folosit pentru stocarea valorii de declanșare. De exemplu, dacă un utilizator a optat pentru declanșatorul evenimentului bazat pe prețuri - dorește actualizări dacă prețul stocului a ajuns la 1000, atuncitrigger_value
ar fi 1000, iartrigger_type
ar fievent
.
Acestea sunt toate mesele de care am avea nevoie pentru acest proiect. De asemenea, trebuie să stabilim relații între aceste tabele pentru a avea un flux de date și conexiuni fluide. Hai să facem asta!
Crearea relațiilor între tabele
Tabelul de events
este folosit pentru a trimite notificări web-push pe baza valorii evenimentului. Deci, este logic să conectați această masă cu tabelul user_subscription
pentru a putea trimite notificări push pe abonamentele stocate în acest tabel.
events.user_id → user_subscription.id
Tabelul stock_data
este legat de tabelul de simboluri ca:
stock_data.symbol → symbol.id
De asemenea, trebuie să construim câteva relații pe tabelul symbol
ca:
stock_data.symbol → symbol.id events.symbol → symbol.id
Am creat acum tabelele necesare și, de asemenea, am stabilit relațiile dintre ele! Să trecem la fila GRAPHIQL
pe consola pentru a vedea magia!
Hasura a configurat deja interogările GraphQL pe baza acestor tabele:
Este simplu să interogați aceste tabele și puteți, de asemenea, să aplicați oricare dintre aceste filtre/proprietăți ( distinct_on
, limit
, offset
, order_by
, where
) pentru a obține datele dorite.
Toate acestea arată bine, dar încă nu ne-am conectat codul serverului la consola Hasura. Să completăm acel pic!
Conectarea scriptului NodeJs la baza de date Postgres
Vă rugăm să puneți opțiunile necesare în fișierul config.js
din directorul server
ca:
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;
Vă rugăm să puneți aceste opțiuni din șirul bazei de date care a fost generat când am creat baza de date Postgres pe Heroku.
apiHostOptions
constă din opțiunile legate de API, cum ar fi host
, key
, timeSeriesFunction
de timp și interval
.
Veți obține câmpul graphqlURL
în fila GRAPHIQL de pe consola Hasura.
Funcția getConfig
este utilizată pentru a returna valoarea cerută de la obiectul de configurare. Am folosit deja acest lucru în index.js
în directorul server
.
Este timpul să rulați serverul și să populați câteva date în baza de date. Am adăugat un script în package.json
ca:
"scripts": { "start": "nodemon index.js" }
Rulați npm start
pe terminal și punctele de date ale matricei de simboluri din index.js
ar trebui să fie populate în tabele.
Refacerea interogării brute în scriptul NODEJS la mutația GraphQL
Acum că motorul Hasura este înființat, să vedem cât de ușor poate fi să apelați o mutație pe tabelul stock_data
.
Funcția insertStocksData
în queries.js
utilizează o interogare brută:
const query = 'INSERT INTO stock_data (symbol, high, low, open, close, volume, time) VALUES ($1, $2, $3, $4, $5, $6, $7)';
Să refactorăm această interogare și să folosim mutația alimentată de motorul Hasura. Iată queries.js
refactorizat în directorul serverului:
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 }
Vă rugăm să rețineți: trebuie să adăugăm graphqlURL
în fișierul config.js
.
Modulul apollo-fetch
returnează o funcție de preluare care poate fi utilizată pentru a interoga/muta data la punctul final GraphQL. Destul de ușor, nu?
Singura schimbare pe care trebuie să o facem în index.js
este de a returna obiectul stocurilor în format, așa cum este necesar de funcția insertStocksData
. Consultați index2.js
și queries2.js
pentru codul complet cu această abordare.
Acum că am realizat partea de date a proiectului, să trecem la bitul front-end și să construim câteva componente interesante!
Notă : Nu trebuie să păstrăm opțiunile de configurare a bazei de date cu această abordare!
Front-end folosind React și Apollo Client
Proiectul front-end se află în același depozit și este creat folosind pachetul create-react-app
. Lucrătorul de servicii generat utilizând acest pachet acceptă cache-ul de active, dar nu permite ca mai multe personalizări să fie adăugate la fișierul lucrătorului de service. Există deja câteva probleme deschise pentru a adăuga suport pentru opțiunile personalizate pentru lucrătorii de servicii. Există modalități de a scăpa de această problemă și de a adăuga suport pentru un lucrător de servicii personalizate.
Să începem prin a ne uita la structura proiectului front-end:
Vă rugăm să verificați directorul src
! Nu vă faceți griji cu privire la fișierele legate de lucrătorul de service pentru moment. Vom afla mai multe despre aceste fișiere mai târziu în această secțiune. Restul structurii proiectului pare simplu. Dosarul components
va avea componentele (încărcătoare, diagramă); folderul de services
conține unele dintre funcțiile/serviciile helper utilizate pentru transformarea obiectelor în structura necesară; styles
, după cum sugerează și numele, conține fișierele sass utilizate pentru stilarea proiectului; views
este directorul principal și conține componentele stratului de vizualizare.
Avem nevoie de doar două componente de vizualizare pentru acest proiect - Lista de simboluri și Seria de simboluri. Vom construi seria temporală folosind componenta Chart din biblioteca highcharts. Să începem să adăugăm cod în aceste fișiere pentru a construi piesele pe front-end!
Instalarea dependențelor
Iată lista dependențelor pe care o avem nevoie de:
-
apollo-boost
Apollo Boost este o modalitate zero-config de a începe să utilizați client Apollo. Vine la pachet cu opțiunile de configurare implicite. -
reactstrap
șibootstrap
.
Componentele sunt construite folosind aceste două pachete. -
graphql
șigraphql-type-json
graphql
este o dependență necesară pentru utilizarea Apollo-apollo-boost
șigraphql-type-json
este utilizat pentru susținerea datei de date JSON utilizate în schemajson
. highcharts
șihighcharts-react-official
Și aceste două pachete vor fi folosite pentru construirea diagramei:node-sass
Acesta este adăugat pentru a susține fișierele sass pentru stil.uuid
Acest pachet este folosit pentru a genera valori aleatorii puternice.
Toate aceste dependențe vor avea sens odată ce vom începe să le folosim în proiect. Să trecem la următorul pas!
Configurarea clientului Apollo
Creați un apolloClient.js
în folderul src
ca:
import ApolloClient from 'apollo-boost'; const apolloClient = new ApolloClient({ uri: '<HASURA_CONSOLE_URL>' }); export default apolloClient;
Codul de mai sus instanțiază ApolloClient și preia uri
în opțiunile de configurare. uri
-ul este adresa URL a consolei tale Hasura. Veți obține acest câmp uri
în fila GRAPHIQL
din secțiunea GraphQL Endpoint .
Codul de mai sus pare simplu, dar are grijă de partea principală a proiectului! Conectează schema GraphQL construită pe Hasura cu proiectul curent.
De asemenea, trebuie să transmitem acest obiect client Apollo la ApolloProvider
și să încapsulăm componenta rădăcină în ApolloProvider
. Acest lucru va permite tuturor componentelor imbricate din componenta principală să utilizeze propul client
și să declanșeze interogări pe acest obiect client.
Să modificăm fișierul index.js
ca:
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') );
Vă rugăm să ignorați codul legat de insertSubscription
. Vom înțelege acest lucru în detaliu mai târziu. Restul codului ar trebui să fie ușor de folosit. Funcția de render
preia componenta rădăcină și elementId ca parametri. Observați că client
(instanța ApolloClient) este transmis ca suport către ApolloProvider
. Puteți verifica fișierul complet index.js
aici.
Configurarea lucrătorului de service personalizat
Un lucrător de serviciu este un fișier JavaScript care are capacitatea de a intercepta solicitările de rețea. Se utilizează pentru interogarea cache-ului pentru a verifica dacă activul solicitat este deja prezent în memoria cache, în loc să facă o plimbare pe server. Lucrătorii de servicii sunt, de asemenea, utilizați pentru a trimite notificări web-push către dispozitivele abonate.
Trebuie să trimitem notificări web-push pentru actualizările prețului acțiunilor către utilizatorii abonați. Să punem terenul și să construim acest fișier de lucrător al serviciului!
insertSubscription
legată de fișierul index.js
face munca de înregistrare a lucrătorului de servicii și de a pune obiectul abonamentului în baza de date folosind subscriptionMutation
.
Consultați queries.js pentru toate interogările și mutațiile utilizate în proiect.
serviceWorker.register(insertSubscription);
Invocă funcția de register
scrisă în fișierul serviceWorker.js
. Iată-l:
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(); }) } }
Funcția de mai sus verifică mai întâi dacă serviceWorker
este acceptat de browser și apoi înregistrează fișierul de lucrător de service găzduit pe swUrl
URL. Vom verifica acest fișier într-un moment!
Funcția getSubscription
face munca de a obține obiectul abonament folosind metoda subscribe
pe obiectul pushManager
. Acest obiect de abonament este apoi stocat în tabelul user_subscription
cu un userId. Vă rugăm să rețineți că uuid
este generat utilizând funcția UUID. Să verificăm funcția 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 })); }) } }) }
Puteți verifica fișierul serviceWorker.js
pentru codul complet!
Notification.requestPermission()
a invocat această fereastră pop-up care cere utilizatorului permisiunea de a trimite notificări. Odată ce utilizatorul face clic pe Permite, un obiect de abonament este generat de serviciul push. Noi stochează acest obiect în localists ca:
Punctul endpoint
al câmpului din obiectul de mai sus este utilizat pentru identificarea dispozitivului, iar serverul utilizează acest punct final pentru a trimite notificări Web Push către utilizator.
Am făcut munca de inițializare și înregistrare a lucrătorului de service. Avem si obiectul de abonament al utilizatorului! Acest lucru funcționează bine din cauza fișierului serviceWorker.js
prezent în folderul public
. Acum să setăm lucrătorul de service pentru a pregăti lucrurile!
Acest lucru este un subiect puțin dificil, dar hai să o facem bine! După cum am menționat mai devreme, utilitarul create-react-app
nu acceptă personalizări în mod implicit pentru lucrătorul de servicii. Putem realiza implementarea lucrătorilor de servicii pentru clienți folosind workbox-build
.
De asemenea, trebuie să ne asigurăm că comportamentul implicit al fișierelor de pre-caching este intact. Vom modifica partea în care lucrătorul de service este construit în proiect. Și, construirea de lucru ajută la realizarea exact asta! Chestii frumoase! Să rămânem simplu și să enumeram tot ce trebuie să facem pentru ca lucrătorul de servicii personalizate să funcționeze:
- Gestionați pre-memorizarea în cache a activelor folosind
workboxBuild
. - Creați un șablon de lucrător de service pentru stocarea în cache a activelor.
- Creați fișierul
sw-precache-config.js
pentru a oferi opțiuni de configurare personalizate. - Adăugați scriptul build service worker în pasul de compilare din
package.json
.
Nu vă faceți griji dacă toate acestea sună confuz! Articolul nu se concentrează pe explicarea semanticii din spatele fiecăruia dintre aceste puncte. Ne-am concentra pe partea de implementare pentru moment! Voi încerca să acoperim raționamentul din spatele acestei lucrări pentru a face un lucrător de service personalizat într-un alt articol.
Să creăm două fișiere sw-build.js
și sw-custom.js
în directorul src
. Vă rugăm să consultați linkurile către aceste fișiere și să adăugați codul la proiectul dvs.
Să creăm acum fișierul sw-precache-config.js
la nivel rădăcină și să adăugăm următorul cod în acel fișier:
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' }] }
Să modificăm, de asemenea, fișierul package.json
pentru a face loc pentru construirea fișierului de serviciu personalizat:
Adăugați aceste afirmații în secțiunea de scripts
:
"build-sw": "node ./src/sw-build.js", "clean-cra-sw": "rm -f build/precache-manifest.*.js && rm -f build/service-worker.js",
Și modificați build
de compilare ca:
"build": "react-scripts build && npm run build-sw && npm run clean-cra-sw",
Configurarea se face în cele din urmă! Acum trebuie să adăugăm un fișier personalizat de serviciu în interiorul folderului 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)); })
Tocmai am adăugat un ascultător push
pentru a asculta notificările push trimise de server. Funcția showNotification
este utilizată pentru afișarea notificărilor push web către utilizator.
Asta este! Am terminat cu toată munca grea de înființare a unui lucrător de service personalizat pentru a gestiona notificările de împingere web. Vom vedea aceste notificări în acțiune odată ce construim interfețele cu utilizatorul!
Ne apropiem de construirea pieselor principale de cod. Să începem acum cu prima vedere!
Vizualizare listă de simboluri
Componenta App
utilizată în secțiunea anterioară arată astfel:
import React from 'react'; import SymbolList from './views/symbolList'; const App = () => { return <SymbolList />; }; export default App;
Este o componentă simplă care returnează vizualizarea SymbolList
și SymbolList
face tot ridicarea goală de afișare a simbolurilor într-o interfață cu utilizatorul legată.
Să ne uităm la symbolList.js
din dosarul views
:
Vă rugăm să consultați fișierul aici!
Componenta returnează rezultatele funcției renderSymbols
. Și, aceste date sunt preluate din baza de date folosind cârligul useQuery
ca:
const { loading, error, data } = useQuery(symbolsQuery, {variables: { userId }});
symbolsQuery
este definită ca:
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 } } } } } `;
userId
și preia evenimentele abonate ale acelui utilizator pentru a afișa starea corectă a pictogramei de notificare (pictograma clopoțel care este afișată împreună cu titlul). Interogarea preia, de asemenea, valorile maxime și minime ale stocului. Observați utilizarea aggregate
în interogarea de mai sus. Interogările de agregare ale lui Hasura fac lucrarea din spatele scenei pentru a aduce valorile agregate cum ar fi count
, sum
, avg
, max
, min
, etc.
Pe baza răspunsului de la apelul GrafQL de mai sus, iată lista de carduri afișate pe front-end:
Structura HTML a cardului arată așa ceva:
<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>
Folosim componenta Card
reactivi pentru a face aceste carduri. Componenta Popover
este utilizată pentru afișarea opțiunilor bazate pe abonament:
Când utilizatorul face clic pe pictograma bell
pentru un anumit stoc, el poate opta pentru a fi notificat în fiecare oră sau când prețul stocului a atins valoarea introdusă. Vom vedea acest lucru în acțiune în secțiunea Evenimente / Time declanșatoare.
Notă : Vom ajunge la componenta StockTimeseries
în secțiunea următoare!
Vă rugăm să consultați symbolList.js
pentru codul complet legat de componenta listei de stocuri.
Vizualizare stoc Times
Componenta de StockTimeseries
utilizează stocurile de 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 } } `;
Interogarea de mai sus preia cele 25 de puncte de date recente ale stocului selectat. De exemplu, aici este graficul pentru stocul de stoc deschis :
Aceasta este o componentă simplă în care trecem în unele opțiuni de diagramă la componenta [ HighchartsReact
]. Iată opțiunile de diagramă:
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') } ] }
Axa X arată ora, iar axa Y arată valoarea metrică la acel moment. Funcția getDataPoints
este folosită pentru a genera o serie de puncte pentru fiecare dintre serii.
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; }
Simplu! Așa se generează componenta Chart! Consultați fișierele Chart.js și stockTimeseries.js
pentru codul complet al seriei temporale stoc.
Acum ar trebui să fiți gata cu datele și cu interfețele utilizator din proiect. Să trecem acum la partea interesantă - configurarea declanșatoarelor de eveniment/timp pe baza intrării utilizatorului.
Configurarea evenimentelor/declanșatoarelor programate
În această secțiune, vom învăța cum să setăm declanșatoarele pe consola Hasura și cum să trimitem notificări push web către utilizatorii selectați. Să începem!
Evenimente declanșează pe consola Hasura
Să creăm un eveniment declanșator stock_value
pe tabelul stock_data
și să insert
ca operație de declanșare. Webhook-ul va rula de fiecare dată când există o inserare în tabelul stock_data
.
Vom crea un proiect glitch pentru URL-ul webhook. Permiteți-mi să scriu puțin despre webhook-uri pentru a fi ușor de înțeles:
Webhook-urile sunt folosite pentru a trimite date de la o aplicație la alta cu privire la apariția unui anumit eveniment. Când este declanșat un eveniment, se efectuează un apel HTTP POST către adresa URL a webhook-ului cu datele evenimentului ca sarcină utilă.
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!
Preluarea utilizatorilor abonați
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()); }
În funcția handleStockValueTrigger
de mai sus, primim mai întâi utilizatorii abonați folosind funcția getSubscribedUsers
. Apoi trimitem notificări web-push fiecăruia dintre acești utilizatori. Funcția sendWebpush
este utilizată pentru trimiterea notificării. Ne vom uita la implementarea web-push într-un moment.
Funcția getSubscribedUsers
folosește interogarea:
query getSubscribedUsers($symbol: String, $triggerValue: numeric) { events(where: {symbol: {_eq: $symbol}, trigger_type: {_eq: "event"}, trigger_value: {_gte: $triggerValue}}) { user_id user_subscription { subscription } } }
Această interogare preia simbolul bursier și valoarea și preia detaliile utilizatorului, inclusiv user-id
și user_subscription
care îndeplinesc aceste condiții:
-
symbol
egal cu cel trecut în sarcina utilă. -
trigger_type
este egal cuevent
. -
trigger_value
este mai mare sau egală cu cea transmisă acestei funcție (close
în acest caz).
Odată ce obținem lista de utilizatori, singurul lucru care rămâne este să le trimitem notificări web-push! Să facem asta imediat!
Trimiterea notificărilor Web-Push către utilizatorii abonați
Mai întâi trebuie să obținem cheile VAID publice și private pentru a trimite notificări web-push. Stocați aceste chei în fișierul .env
și setați aceste detalii în index.js
ca:
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)) }
Funcția sendNotification
este utilizată pentru trimiterea web-push pe punctul final de abonament furnizat ca prim parametru.
Atât este necesar pentru a trimite cu succes notificări web-push către utilizatorii abonați. Iată codul complet definit în 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"); });
Să testăm acest flux abonându-ne la stoc cu o anumită valoare și inserând manual acea valoare în tabel (pentru testare)!
M-am abonat la AMZN
cu valoarea 2000
și apoi am inserat un punct de date în tabel cu această valoare. Iată cum m-a notificat aplicația de notificare a stocurilor imediat după inserare:
Îngrijit! De asemenea, puteți verifica jurnalul de invocare a evenimentelor aici:
Webhook-ul face treaba așa cum era de așteptat! Suntem pregătiți pentru declanșatorii evenimentului acum!
Declanșatoare programate/Cron
Putem realiza un declanșator bazat pe timp pentru a notifica utilizatorii abonați în fiecare oră folosind declanșatorul evenimentului Cron ca:
Putem folosi aceeași adresă URL webhook și gestionăm utilizatorii abonați pe baza tipului de eveniment de declanșare ca stock_price_time_based_trigger
. Implementarea este similară cu declanșatorul bazat pe evenimente.
Concluzie
În acest articol, am creat o aplicație de notificare a prețului acțiunilor. Am învățat cum să obținem prețuri folosind API-urile Alpha Vantage și să stocăm punctele de date în baza de date Postgres susținută de Hasura. De asemenea, am învățat cum să configuram motorul Hasura GraphQL și să creăm declanșatoare programate și bazate pe evenimente. Am construit un proiect glitch pentru trimiterea de notificări web-push către utilizatorii abonați.