Crearea unei aplicații de notificare a prețurilor acțiunilor folosind React, Apollo GraphQL și Hasura

Publicat: 2022-03-10
Rezumat rapid ↬ În acest articol, vom afla cum să construim o aplicație bazată pe evenimente și să trimitem o notificare web-push atunci când este declanșat un anumit eveniment. Vom configura tabele de baze de date, evenimente și declanșatoare programate pe motorul Hasura GraphQL și vom conecta punctul final GraphQL la aplicația front-end pentru a înregistra preferința de preț a acțiunilor a utilizatorului.

Conceptul 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:

Prezentare generală a aplicației de notificare a prețului acțiunilor
Aplicație de notificare a prețului acțiunilor

Haide să mergem!

Mai multe după săritură! Continuați să citiți mai jos ↓

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:

  1. 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.
  2. 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.
  3. 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.
  4. 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ă utilizarea fetch 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:

Crearea unui proiect Hasura
Crearea unui proiect Hasura. (Previzualizare mare)

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ță:

Consola Hasura
Consola Hasura. (Previzualizare mare)

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 simboluri
tabelul de symbol . (Previzualizare mare)

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:

tabelul de date_stoc
tabelul de stock_data . (Previzualizare mare)

Î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 tip uuid .
  • 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, atunci trigger_value ar fi 1000, iar trigger_type ar fi event .

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:

Interogări/Mutații GraphQL pe consola Hasura
Interogări grafice / mutații pe consola Hasura. (Previzualizare mare)

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:

Director de proiecte
Director de proiecte. (Previzualizare mare)

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 și bootstrap .
    Componentele sunt construite folosind aceste două pachete.
  • graphql și graphql-type-json
    graphql este o dependență necesară pentru utilizarea Apollo- apollo-boost și graphql-type-json este utilizat pentru susținerea datei de date JSON utilizate în schema json .
  • highcharts și highcharts-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!

Popup de notificare
Popup de notificare. (Previzualizare mare)

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:

Obiect Abonamente Webpush
Obiect Abonamente Webpush. (Previzualizare mare)

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:

Carduri de stoc
Carduri de stoc. (Previzualizare mare)

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:

Opțiuni de notificare
Opțiuni de notificare. (Previzualizare mare)

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 :

Cronologia prețurilor acțiunilor
Cronologia prețurilor acțiunilor. (Previzualizare mare)

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 .

Evenimentul declanșează configurarea
Evenimentul declanșează configurarea. (Previzualizare mare)

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 cu event .
  • 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:

Inserarea unui rând în tabelul stoc_data pentru testare
Inserarea unui rând în tabelul stoc_data pentru testare. (Previzualizare mare)

Îngrijit! De asemenea, puteți verifica jurnalul de invocare a evenimentelor aici:

Jurnalul evenimentelor
Jurnalul evenimentelor. (Previzualizare mare)

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:

Configurare cron/declanșare programată
Configurare cron/declanșare programată. (Previzualizare mare)

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.