Creazione di un'app di notifica del prezzo delle azioni utilizzando React, Apollo GraphQL e Hasura
Pubblicato: 2022-03-10Il concetto di ricevere una notifica quando si è verificato l'evento di tua scelta è diventato popolare rispetto all'essere incollati al flusso continuo di dati per trovare tu stesso quel particolare evento. Le persone preferiscono ricevere e-mail/messaggi pertinenti quando si è verificato il loro evento preferito invece di essere agganciati allo schermo per aspettare che si verifichi quell'evento. La terminologia basata sugli eventi è abbastanza comune anche nel mondo del software.
Quanto sarebbe fantastico se potessi ricevere gli aggiornamenti del prezzo del tuo titolo preferito sul tuo telefono?
In questo articolo, creeremo un'applicazione Notifier del prezzo delle azioni utilizzando il motore React, Apollo GraphQL e Hasura GraphQL. Inizieremo il progetto da un codice boilerplate create-react-app
e costruiremo tutto da zero. Impareremo come impostare le tabelle del database e gli eventi sulla console Hasura. Impareremo anche come collegare gli eventi di Hasura per ottenere aggiornamenti sui prezzi delle azioni utilizzando le notifiche push web.
Ecco una rapida occhiata a cosa costruiremmo:
Muoviamoci!
Una panoramica di cosa tratta questo progetto
I dati sulle azioni (comprese metriche come alto , basso , aperto , chiuso , volume ) sarebbero archiviati in un database Postgres supportato da Hasura. L'utente sarebbe in grado di iscriversi a un determinato titolo in base a un valore oppure può scegliere di ricevere una notifica ogni ora. L'utente riceverà una notifica web-push una volta soddisfatti i suoi criteri di abbonamento.
Sembra un sacco di cose e ovviamente ci sarebbero alcune domande aperte su come costruiremo questi pezzi.
Ecco un piano su come realizzare questo progetto in quattro passaggi:
- Recupero dei dati sulle azioni utilizzando uno script NodeJs
Inizieremo recuperando i dati sulle azioni utilizzando un semplice script NodeJs da uno dei fornitori di API delle azioni: Alpha Vantage. Questo script recupererà i dati per un particolare stock a intervalli di 5 minuti. La risposta dell'API include alta , bassa , apertura , chiusura e volume . Questi dati verranno poi inseriti nel database di Postgres che è integrato con il back-end Hasura. - Configurazione del motore Hasura GraphQL
Quindi imposteremo alcune tabelle sul database di Postgres per registrare i punti dati. Hasura genera automaticamente gli schemi, le query e le mutazioni GraphQL per queste tabelle. - Front-end che utilizza React e Apollo Client
Il passaggio successivo consiste nell'integrare il livello GraphQL utilizzando il client Apollo e Apollo Provider (l'endpoint GraphQL fornito da Hasura). I punti dati verranno visualizzati come grafici sul front-end. Creeremo anche le opzioni di abbonamento e attiveremo le mutazioni corrispondenti sul livello GraphQL. - Impostazione di trigger evento/programmati
Hasura fornisce un eccellente strumento per i trigger. Aggiungeremo eventi e trigger programmati nella tabella dei dati sulle azioni. Questi trigger verranno impostati se l'utente è interessato a ricevere una notifica quando i prezzi delle azioni raggiungono un valore particolare (trigger di eventi). L'utente può anche optare per ricevere una notifica di un determinato titolo ogni ora (trigger programmato).
Ora che il piano è pronto, mettiamolo in atto!
Ecco il repository GitHub per questo progetto. Se ti perdi da qualche parte nel codice qui sotto, fai riferimento a questo repository e torna alla velocità!
Recupero dei dati sulle azioni utilizzando uno script NodeJs
Non è così complicato come sembra! Dovremo scrivere una funzione che recuperi i dati utilizzando l'endpoint Alpha Vantage e questa chiamata di recupero dovrebbe essere attivata in un intervallo di 5 minuti (hai indovinato, dovremo inserire questa chiamata di funzione in setInterval
).
Se ti stai ancora chiedendo cosa sia Alpha Vantage e vuoi solo toglierti questo dalla testa prima di saltare alla parte di codifica, allora eccolo qui:
Alpha Vantage Inc. è un fornitore leader di API gratuite per dati storici e in tempo reale su azioni, forex (FX) e digitali/criptovalute.
Utilizzeremmo questo endpoint per ottenere le metriche richieste di un determinato titolo. Questa API prevede una chiave API come uno dei parametri. Puoi ottenere la tua chiave API gratuita da qui. Ora siamo pronti per passare al bit interessante: iniziamo a scrivere del codice!
Installazione delle dipendenze
Crea una directory stocks-app
e crea una directory del server
al suo interno. Inizializzalo come progetto nodo usando npm init
e quindi installa queste dipendenze:
npm i isomorphic-fetch pg nodemon --save
Queste sono le uniche tre dipendenze di cui avremmo bisogno per scrivere questo script per recuperare i prezzi delle azioni e archiviarli nel database di Postgres.
Ecco una breve spiegazione di queste dipendenze:
-
isomorphic-fetch
Semplifica l'uso delfetch
in modo isomorfico (nella stessa forma) sia sul client che sul server. -
pg
È un client PostgreSQL non bloccante per NodeJs. -
nodemon
Riavvia automaticamente il server in caso di modifiche ai file nella directory.
Impostazione della configurazione
Aggiungi un file config.js
a livello di root. Aggiungi il seguente frammento di codice in quel file per ora:
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;
L' user
, la password
, host
, la port
, il database
, ssl
sono correlati alla configurazione di Postgres. Torneremo per modificarlo mentre impostiamo la parte del motore Hasura!
Inizializzazione del pool di connessioni Postgres per eseguire query sul database
Un connection pool
è un termine comune nell'informatica e sentirai spesso questo termine quando hai a che fare con i database.
Durante la query dei dati nei database, dovrai prima stabilire una connessione al database. Questa connessione accetta le credenziali del database e fornisce un hook per interrogare qualsiasi tabella nel database.
Nota : stabilire connessioni al database è costoso e comporta anche uno spreco di risorse significative. Un pool di connessioni memorizza nella cache le connessioni al database e le riutilizza nelle query successive. Se tutte le connessioni aperte sono in uso, viene stabilita una nuova connessione che viene quindi aggiunta al pool.
Ora che è chiaro cos'è il pool di connessioni e a cosa serve, iniziamo creando un'istanza del pool di connessioni pg
per questa applicazione:
Aggiungi il file pool.js
a livello di root e crea un'istanza del pool come:
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;
Le righe di codice precedenti creano un'istanza di Pool
con le opzioni di configurazione impostate nel file di configurazione. Dobbiamo ancora completare il file di configurazione ma non ci saranno modifiche relative alle opzioni di configurazione.
Ora abbiamo impostato il terreno e siamo pronti per iniziare a effettuare alcune chiamate API all'endpoint Alpha Vantage.
Passiamo alla parte interessante!
Recupero dei dati sulle azioni
In questa sezione, recupereremo i dati sulle azioni dall'endpoint Alpha Vantage. Ecco il 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); }); }); }) })()
Ai fini di questo progetto, interrogheremo i prezzi solo per questi titoli: NFLX (Netflix), MSFT (Microsoft), AMZN (Amazon), W (Wayfair), FB (Facebook).
Fare riferimento a questo file per le opzioni di configurazione. La funzione IIFE getStocksData
non sta facendo molto! Scorre questi simboli e interroga l'endpoint Alpha Vantage ${host}query/?function=${timeSeriesFunction}&symbol=${symbol}&interval=${interval}&apikey=${key}
per ottenere le metriche per questi titoli.
La funzione insertStocksData
inserisce questi punti dati nel database di Postgres. Ecco la funzione 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); }); };
Questo è! Abbiamo recuperato i punti dati del titolo dall'API Alpha Vantage e abbiamo scritto una funzione per inserirli nel database Postgres nella tabella stock_data
. Manca solo un pezzo per far funzionare tutto questo! Dobbiamo popolare i valori corretti nel file di configurazione. Otterremo questi valori dopo aver impostato il motore Hasura. Arriviamo subito a questo!
Fare riferimento alla directory del server
per il codice completo sul recupero dei punti dati dall'endpoint Alpha Vantage e sul popolamento del database Hasura Postgres.
Se questo approccio di impostazione di connessioni, opzioni di configurazione e inserimento di dati utilizzando la query grezza sembra un po' difficile, non preoccuparti! Impareremo come fare tutto questo nel modo più semplice con una mutazione GraphQL una volta impostato il motore Hasura!
Configurazione del motore Hasura GraphQL
È davvero semplice configurare il motore Hasura e iniziare a utilizzare gli schemi GraphQL, le query, le mutazioni, le sottoscrizioni, i trigger di eventi e molto altro!
Clicca su Prova Hasura e inserisci il nome del progetto:
Sto usando il database Postgres ospitato su Heroku. Crea un database su Heroku e collegalo a questo progetto. Dovresti quindi essere pronto per sperimentare la potenza della console Hasura ricca di query.
Copia l'URL del DB Postgres che otterrai dopo aver creato il progetto. Dovremo metterlo nel file di configurazione.
Fai clic su Avvia Console e verrai reindirizzato a questa vista:
Iniziamo a costruire lo schema della tabella di cui avremmo bisogno per questo progetto.
Creazione di schemi di tabelle sul database Postgres
Vai alla scheda Dati e fai clic su Aggiungi tabella! Iniziamo a creare alcune delle tabelle:
tabella dei symbol
Questa tabella verrebbe utilizzata per memorizzare le informazioni dei simboli. Per ora, ho mantenuto due campi qui: id
e company
. L' id
campo è una chiave primaria e l' company
è di tipo varchar
. Aggiungiamo alcuni dei simboli in questa tabella:
tabella stock_data
La tabella stock_data
memorizza l' id
, il symbol
, il time
e le metriche come high
, low
, open
, close
, volume
. Lo script NodeJs che abbiamo scritto in precedenza in questa sezione verrà utilizzato per popolare questa particolare tabella.
Ecco come appare la tabella:
Pulito! Passiamo all'altra tabella nello schema del database!
tabella user_subscription
La tabella user_subscription
archivia l'oggetto sottoscrizione rispetto all'ID utente. Questo oggetto di abbonamento viene utilizzato per inviare notifiche push Web agli utenti. Impareremo più avanti nell'articolo come generare questo oggetto di sottoscrizione.
Ci sono due campi in questa tabella: id
è la chiave primaria di tipo uuid
e il campo di sottoscrizione è di tipo jsonb
.
tabella events
Questo è quello importante e viene utilizzato per memorizzare le opzioni degli eventi di notifica. Quando un utente accetta gli aggiornamenti dei prezzi di un determinato titolo, memorizziamo le informazioni sull'evento in questa tabella. Questa tabella contiene queste colonne:
-
id
: è una chiave primaria con la proprietà di incremento automatico. -
symbol
: è un campo di testo. -
user_id
: è di tipouuid
. -
trigger_type
: viene utilizzato per memorizzare il tipo di trigger dell'evento —time/event
. -
trigger_value
: viene utilizzato per memorizzare il valore del trigger. Ad esempio, se un utente ha attivato l'attivazione dell'evento basato sul prezzo, desidera aggiornamenti se il prezzo dell'azione ha raggiunto 1000, iltrigger_value
sarebbe 1000 e iltrigger_type
sarebbeevent
.
Queste sono tutte le tabelle di cui avremmo bisogno per questo progetto. Dobbiamo anche stabilire relazioni tra queste tabelle per avere un flusso di dati e connessioni fluide. Facciamolo!
Stabilire relazioni tra tabelle
La tabella events
viene utilizzata per inviare notifiche push Web in base al valore dell'evento. Quindi, ha senso collegare questa tabella con la tabella user_subscription
per poter inviare notifiche push sugli abbonamenti archiviati in questa tabella.
events.user_id → user_subscription.id
La tabella stock_data
è correlata alla tabella dei simboli come:
stock_data.symbol → symbol.id
Dobbiamo anche costruire alcune relazioni sulla tabella dei symbol
come:
stock_data.symbol → symbol.id events.symbol → symbol.id
Ora abbiamo creato le tabelle richieste e stabilito anche le relazioni tra di loro! Passiamo alla scheda GRAPHIQL
sulla console per vedere la magia!
Hasura ha già impostato le query GraphQL sulla base di queste tabelle:
È semplicemente semplice eseguire query su queste tabelle e puoi anche applicare uno qualsiasi di questi filtri/proprietà ( distinct_on
, limit
, offset
, order_by
, where
) per ottenere i dati desiderati.
Sembra tutto a posto, ma non abbiamo ancora collegato il nostro codice lato server alla console Hasura. Completiamo quel po'!
Collegamento dello script NodeJs al database Postgres
Inserisci le opzioni richieste nel file config.js
nella directory del server
come:
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;
Inserisci queste opzioni dalla stringa del database che è stata generata quando abbiamo creato il database Postgres su Heroku.
apiHostOptions
è costituito dalle opzioni relative all'API come host
, key
, timeSeriesFunction
e interval
.
Otterrai il campo graphqlURL
nella scheda GRAPHIQL sulla console Hasura.
La funzione getConfig
viene utilizzata per restituire il valore richiesto dall'oggetto config. L'abbiamo già usato in index.js
nella directory del server
.
È ora di eseguire il server e popolare alcuni dati nel database. Ho aggiunto uno script in package.json
come:
"scripts": { "start": "nodemon index.js" }
Esegui npm start
sul terminale e i punti dati dell'array di simboli in index.js
dovrebbero essere popolati nelle tabelle.
Refactoring della query grezza nello script NodeJs per la mutazione GraphQL
Ora che il motore Hasura è impostato, vediamo quanto può essere facile chiamare una mutazione nella tabella stock_data
.
La funzione insertStocksData
in queries.js
utilizza una query grezza:
const query = 'INSERT INTO stock_data (symbol, high, low, open, close, volume, time) VALUES ($1, $2, $3, $4, $5, $6, $7)';
Eseguiamo il refactoring di questa query e utilizziamo la mutazione basata sul motore Hasura. Ecco il refactored queries.js
nella directory del 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 }
Nota: dobbiamo aggiungere graphqlURL
nel file config.js
.
Il modulo apollo-fetch
restituisce una funzione di recupero che può essere utilizzata per interrogare/mutare la data sull'endpoint GraphQL. Abbastanza facile, giusto?
L'unica modifica che dobbiamo fare in index.js
è restituire l'oggetto stock nel formato richiesto dalla funzione insertStocksData
. Si prega di controllare index2.js
e queries2.js
per il codice completo con questo approccio.
Ora che abbiamo completato il lato dati del progetto, passiamo al bit front-end e costruiamo alcuni componenti interessanti!
Nota : non è necessario mantenere le opzioni di configurazione del database con questo approccio!
Front-end che utilizza React e il client Apollo
Il progetto front-end si trova nello stesso repository e viene creato utilizzando il pacchetto create-react-app
. Il ruolo di lavoro del servizio generato utilizzando questo pacchetto supporta la memorizzazione nella cache delle risorse ma non consente l'aggiunta di ulteriori personalizzazioni al file di lavoro del servizio. Ci sono già alcuni problemi aperti per aggiungere il supporto per le opzioni del lavoratore del servizio personalizzato. Ci sono modi per farla franca con questo problema e aggiungere il supporto per un addetto al servizio personalizzato.
Iniziamo osservando la struttura del progetto front-end:
Si prega di controllare la directory src
! Per ora non preoccuparti dei file relativi ai lavoratori del servizio. Impareremo di più su questi file più avanti in questa sezione. Il resto della struttura del progetto sembra semplice. La cartella dei components
conterrà i componenti (Loader, Chart); la cartella dei services
contiene alcune delle funzioni/servizi di supporto utilizzati per trasformare gli oggetti nella struttura richiesta; styles
come suggerisce il nome contiene i file sass utilizzati per lo stile del progetto; views
è la directory principale e contiene i componenti del livello di visualizzazione.
Avremmo bisogno solo di due componenti di visualizzazione per questo progetto: The Symbol List e Symbol Timeseries. Costruiremo le serie temporali utilizzando il componente Grafico dalla libreria highcharts. Iniziamo ad aggiungere codice in questi file per costruire i pezzi sul front-end!
Installazione delle dipendenze
Ecco l'elenco delle dipendenze di cui avremo bisogno:
-
apollo-boost
Apollo boost è un modo senza configurazione per iniziare a utilizzare Apollo Client. Viene fornito in bundle con le opzioni di configurazione predefinite. -
reactstrap
ebootstrap
I componenti vengono creati utilizzando questi due pacchetti. -
graphql
egraphql-type-json
graphql
è una dipendenza richiesta per l'utilizzo diapollo-boost
egraphql-type-json
viene utilizzato per supportare il tipo di datijson
utilizzato nello schema GraphQL. highcharts
ehighcharts-react-official
E questi due pacchetti verranno utilizzati per costruire il grafico:node-sass
Questo viene aggiunto per supportare i file sass per lo styling.uuid
Questo pacchetto viene utilizzato per generare forti valori casuali.
Tutte queste dipendenze avranno senso una volta che inizieremo a usarle nel progetto. Passiamo al prossimo pezzo!
Configurazione del client Apollo
Crea un apolloClient.js
all'interno della cartella src
come:
import ApolloClient from 'apollo-boost'; const apolloClient = new ApolloClient({ uri: '<HASURA_CONSOLE_URL>' }); export default apolloClient;
Il codice sopra istanzia ApolloClient e accetta uri
nelle opzioni di configurazione. L' uri
è l'URL della tua console Hasura. Otterrai questo campo uri
nella scheda GRAPHIQL
nella sezione GraphQL Endpoint .
Il codice sopra sembra semplice ma si occupa della parte principale del progetto! Collega lo schema GraphQL costruito su Hasura con il progetto corrente.
Dobbiamo anche passare questo oggetto client Apollo ad ApolloProvider
e avvolgere il componente root all'interno di ApolloProvider
. Ciò consentirà a tutti i componenti nidificati all'interno del componente principale di utilizzare il prop client
e di attivare query su questo oggetto client.
Modifichiamo il file index.js
come:
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') );
Si prega di ignorare il codice relativo insertSubscription
. Lo capiremo in dettaglio più avanti. Il resto del codice dovrebbe essere semplice da aggirare. La funzione di render
accetta il componente radice e l'elementoId come parametri. Avviso client
(istanza ApolloClient) viene passato come prop ad ApolloProvider
. Puoi controllare il file index.js
completo qui.
Configurazione dell'operatore del servizio personalizzato
Un Service worker è un file JavaScript che ha la capacità di intercettare le richieste di rete. Viene utilizzato per interrogare la cache per verificare se l'asset richiesto è già presente nella cache invece di fare un giro verso il server. Gli operatori del servizio vengono utilizzati anche per inviare notifiche web-push ai dispositivi sottoscritti.
Dobbiamo inviare notifiche web-push per gli aggiornamenti del prezzo delle azioni agli utenti iscritti. Prepariamo il terreno e costruiamo questo file di service worker!
Il relativo insertSubscription
nel file index.js
sta eseguendo il lavoro di registrazione del service worker e inserendo l'oggetto di sottoscrizione nel database subscriptionMutation
.
Fare riferimento a query.js per tutte le query e le mutazioni utilizzate nel progetto.
serviceWorker.register(insertSubscription);
richiama la funzione register
scritta nel file serviceWorker.js
. Ecco qui:
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(); }) } }
La funzione precedente verifica prima se serviceWorker
è supportato dal browser e quindi registra il file di lavoro del servizio ospitato sull'URL swUrl
. Controlleremo questo file tra un momento!
La funzione getSubscription
esegue il lavoro di ottenere l'oggetto di sottoscrizione utilizzando il metodo di subscribe
sull'oggetto pushManager
. Questo oggetto di sottoscrizione viene quindi archiviato nella tabella user_subscription
rispetto a un ID utente. Si noti che lo userId viene generato utilizzando la funzione uuid
. Diamo un'occhiata alla funzione 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 })); }) } }) }
Puoi controllare il file serviceWorker.js
per il codice completo!
Notification.requestPermission()
ha richiamato questo popup che chiede all'utente l'autorizzazione per l'invio di notifiche. Dopo che l'utente fa clic su Consenti, il servizio push genera un oggetto di sottoscrizione. Stiamo archiviando quell'oggetto in localStorage come:
L' endpoint
del campo nell'oggetto precedente viene utilizzato per identificare il dispositivo e il server utilizza questo endpoint per inviare notifiche push Web all'utente.
Abbiamo svolto il lavoro di inizializzazione e registrazione del service worker. Abbiamo anche l'oggetto della sottoscrizione dell'utente! Funziona tutto bene a causa del file serviceWorker.js
presente nella cartella public
. Ora impostiamo l'operatore di servizio per preparare le cose!
Questo è un argomento un po' difficile, ma cerchiamo di capire bene! Come accennato in precedenza, l'utilità create-react-app
non supporta le personalizzazioni per impostazione predefinita per il lavoratore del servizio. Possiamo ottenere l'implementazione dell'operatore del servizio clienti utilizzando il modulo workbox-build
.
Dobbiamo anche assicurarci che il comportamento predefinito dei file di pre-caching sia intatto. Modificheremo la parte in cui l'operatore del servizio viene compilato nel progetto. E, la creazione della casella di lavoro aiuta a raggiungere esattamente questo! Roba ordinata! Manteniamo le cose semplici ed elenchiamo tutto ciò che dobbiamo fare per far funzionare l'operatore del servizio personalizzato:
- Gestisci la memorizzazione nella cache delle risorse utilizzando
workboxBuild
. - Crea un modello di lavoratore del servizio per la memorizzazione nella cache delle risorse.
- Crea il file
sw-precache-config.js
per fornire opzioni di configurazione personalizzate. - Aggiungi lo script di lavoro del servizio di compilazione nel passaggio di compilazione in
package.json
.
Non preoccuparti se tutto questo suona confuso! L'articolo non si concentra sulla spiegazione della semantica dietro ciascuno di questi punti. Per ora dobbiamo concentrarci sulla parte di implementazione! Cercherò di coprire il ragionamento alla base di tutto il lavoro per creare un addetto ai servizi doganali in un altro articolo.
Creiamo due file sw-build.js
e sw-custom.js
nella directory src
. Fare riferimento ai collegamenti a questi file e aggiungere il codice al progetto.
Creiamo ora il file sw-precache-config.js
a livello di root e aggiungiamo il seguente codice in quel file:
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' }] }
Modifichiamo anche il file package.json
per fare spazio alla creazione del file di lavoro del servizio personalizzato:
Aggiungi queste affermazioni nella sezione degli scripts
:
"build-sw": "node ./src/sw-build.js", "clean-cra-sw": "rm -f build/precache-manifest.*.js && rm -f build/service-worker.js",
E modifica lo script di build
come:
"build": "react-scripts build && npm run build-sw && npm run clean-cra-sw",
L'installazione è finalmente fatta! Ora dobbiamo aggiungere un file di lavoro del servizio personalizzato all'interno della cartella 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)); })
Abbiamo appena aggiunto un listener push
per ascoltare le notifiche push inviate dal server. La funzione showNotification
viene utilizzata per visualizzare le notifiche push web all'utente.
Questo è! Abbiamo finito con tutto il duro lavoro di configurazione di un addetto ai servizi personalizzato per gestire le notifiche push web. Vedremo queste notifiche in azione una volta create le interfacce utente!
Ci stiamo avvicinando alla costruzione dei pezzi di codice principali. Iniziamo ora con la prima visualizzazione!
Visualizzazione dell'elenco dei simboli
Il componente App
utilizzato nella sezione precedente ha il seguente aspetto:
import React from 'react'; import SymbolList from './views/symbolList'; const App = () => { return <SymbolList />; }; export default App;
È un componente semplice che restituisce la vista SymbolList
e SymbolList
fa tutto il lavoro pesante della visualizzazione di simboli in un'interfaccia utente ben collegata.
Diamo un'occhiata a symbolList.js
all'interno della cartella views
:
Si prega di fare riferimento al file qui!
Il componente restituisce i risultati della funzione renderSymbols
. E questi dati vengono recuperati dal database utilizzando l'hook useQuery
come:
const { loading, error, data } = useQuery(symbolsQuery, {variables: { userId }});
La symbolsQuery
è definita come:
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 } } } } } `;
Prende l' userId
utente e recupera gli eventi sottoscritti di quel particolare utente per visualizzare lo stato corretto dell'icona di notifica (icona a forma di campana che viene visualizzata insieme al titolo). La query recupera anche i valori massimo e minimo dello stock. Si noti l'uso aggregate
nella query precedente. Le query di aggregazione di Hasura svolgono il lavoro dietro le quinte per recuperare i valori aggregati come count
, sum
, avg
, max
, min
, ecc.
Sulla base della risposta alla chiamata GraphQL sopra, ecco l'elenco delle carte visualizzate sul front-end:
La struttura HTML della scheda è simile a questa:
<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>
Stiamo usando il componente Card
di ReactStrap per renderizzare queste carte. Il componente Popover
viene utilizzato per visualizzare le opzioni basate sull'abbonamento:
Quando l'utente fa clic sull'icona della bell
per un determinato titolo, può scegliere di ricevere una notifica ogni ora o quando il prezzo del titolo ha raggiunto il valore inserito. Lo vedremo in azione nella sezione Eventi/Time Trigger.
Nota : nella prossima sezione arriveremo al componente StockTimeseries
!
Fare riferimento a symbolList.js
per il codice completo relativo al componente dell'elenco delle azioni.
Visualizzazione delle serie storiche di magazzino
Il componente StockTimeseries
utilizza la query 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 } } `;
La query precedente recupera i 25 punti dati recenti del titolo selezionato. Ad esempio, ecco il grafico per la metrica di apertura delle azioni di Facebook:
Questo è un componente semplice in cui passiamo alcune opzioni del grafico al componente [ HighchartsReact
]. Ecco le opzioni del grafico:
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') } ] }
L'asse X mostra l'ora e l'asse Y mostra il valore della metrica in quel momento. La funzione getDataPoints
viene utilizzata per generare una serie di punti per ciascuna delle serie.
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; }
Semplice! Ecco come viene generato il componente Grafico! Fare riferimento ai file Chart.js e stockTimeseries.js
per il codice completo sulle serie temporali di stock.
Ora dovresti essere pronto con i dati e le interfacce utente che fanno parte del progetto. Passiamo ora alla parte interessante: impostare i trigger di evento/tempo in base all'input dell'utente.
Impostazione di eventi/trigger programmati
In questa sezione impareremo come impostare i trigger sulla console Hasura e come inviare notifiche push web agli utenti selezionati. Iniziamo!
Trigger di eventi sulla console Hasura
Creiamo un trigger di evento stock_value
sulla tabella stock_data
e insert
come operazione di trigger. Il webhook verrà eseguito ogni volta che è presente un inserto nella tabella stock_data
.
Creeremo un progetto glitch per l'URL del webhook. Consentitemi di parlare un po' dei webhook per renderli facilmente comprensibili:
I webhook vengono utilizzati per inviare dati da un'applicazione all'altra al verificarsi di un particolare evento. Quando viene attivato un evento, viene effettuata una chiamata HTTP POST all'URL del webhook con i dati dell'evento come 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!
Recupero degli utenti iscritti
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()); }
Nella funzione handleStockValueTrigger
sopra, stiamo prima recuperando gli utenti iscritti utilizzando la funzione getSubscribedUsers
. Invieremo quindi notifiche push web a ciascuno di questi utenti. La funzione sendWebpush
viene utilizzata per inviare la notifica. Tra un momento esamineremo l'implementazione del web-push.
La funzione getSubscribedUsers
utilizza la 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 } } }
Questa query prende il simbolo del titolo e il valore e recupera i dettagli dell'utente inclusi user-id
e user_subscription
che soddisfano queste condizioni:
-
symbol
uguale a quello passato nel carico utile. -
trigger_type
è uguale aevent
. -
trigger_value
è maggiore o uguale a quello passato a questa funzione (close
in questo caso).
Una volta ottenuto l'elenco degli utenti, l'unica cosa che rimane è inviare loro notifiche web-push! Facciamolo subito!
Invio di notifiche Web-Push agli utenti iscritti
Dobbiamo prima ottenere le chiavi VAPID pubbliche e private per inviare notifiche push web. Conserva queste chiavi nel file .env
e imposta questi dettagli in index.js
come:
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)) }
La funzione sendNotification
viene utilizzata per inviare il web-push sull'endpoint della sottoscrizione fornito come primo parametro.
Questo è tutto ciò che serve per inviare correttamente le notifiche web-push agli utenti iscritti. Ecco il codice completo definito in 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"); });
Testiamo questo flusso iscrivendoci a stock con un certo valore e inserendo manualmente quel valore nella tabella (per il test)!
Mi sono iscritto ad AMZN
con valore 2000
e quindi ho inserito un punto dati nella tabella con questo valore. Ecco come l'app di notifica delle azioni mi ha notificato subito dopo l'inserimento:
Pulito! Puoi anche controllare il registro delle chiamate degli eventi qui:
Il webhook sta facendo il lavoro come previsto! Ora siamo pronti per l'attivazione dell'evento!
Trigger programmati/cron
Possiamo ottenere un trigger basato sul tempo per notificare agli utenti abbonati ogni ora utilizzando il trigger di eventi Cron come:
Possiamo utilizzare lo stesso URL webhook e gestire gli utenti iscritti in base al tipo di evento trigger come stock_price_time_based_trigger
. L'implementazione è simile al trigger basato su eventi.
Conclusione
In questo articolo, abbiamo creato un'applicazione di notifica del prezzo delle azioni. Abbiamo imparato come recuperare i prezzi utilizzando le API Alpha Vantage e archiviare i punti dati nel database Postgres supportato da Hasura. Abbiamo anche imparato come impostare il motore Hasura GraphQL e creare trigger basati su eventi e pianificati. Abbiamo creato un progetto glitch per l'invio di notifiche web-push agli utenti iscritti.