Creazione di un'app di notifica del prezzo delle azioni utilizzando React, Apollo GraphQL e Hasura

Pubblicato: 2022-03-10
Riepilogo rapido ↬ In questo articolo impareremo come creare un'applicazione basata su eventi e inviare una notifica push Web quando viene attivato un particolare evento. Imposteremo tabelle di database, eventi e trigger pianificati sul motore Hasura GraphQL e collegheremo l'endpoint GraphQL all'applicazione front-end per registrare la preferenza del prezzo delle azioni dell'utente.

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

Panoramica dell'applicazione Notifier del prezzo delle azioni
Domanda di notifica del prezzo delle azioni

Muoviamoci!

Altro dopo il salto! Continua a leggere sotto ↓

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:

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

Creazione di un progetto Hasura
Creazione di un progetto Hasura. (Grande anteprima)

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:

Hasura Console
Hasura Console. (Grande anteprima)

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 dei simboli
tabella dei symbol . (Grande anteprima)

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:

tabella stock_data
tabella stock_data . (Grande anteprima)

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 tipo uuid .
  • 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, il trigger_value sarebbe 1000 e il trigger_type sarebbe event .

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:

Query/Mutazioni di GraphQL sulla console Hasura
Query/Mutazioni di GraphQL sulla console Hasura. (Grande anteprima)

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

Directory di progetto
Directory di progetto. (Grande anteprima)

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 e bootstrap
    I componenti vengono creati utilizzando questi due pacchetti.
  • graphql e graphql-type-json
    graphql è una dipendenza richiesta per l'utilizzo di apollo-boost e graphql-type-json viene utilizzato per supportare il tipo di dati json utilizzato nello schema GraphQL.
  • highcharts e highcharts-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!

Popup di notifica
Popup di notifica. (Grande anteprima)

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:

Oggetto Abbonamenti Webpush
Oggetto Abbonamenti Webpush. (Grande anteprima)

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:

Carte d'archivio
Carte d'archivio. (Grande anteprima)

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:

Opzioni di notifica
Opzioni di notifica. (Grande anteprima)

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:

Cronologia dei prezzi delle azioni
Cronologia dei prezzi delle azioni. (Grande anteprima)

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 .

Configurazione dei trigger di evento
Configurazione dei trigger di evento. (Grande anteprima)

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

Inserimento di una riga nella tabella stock_data per il test
Inserimento di una riga nella tabella stock_data per il test. (Grande anteprima)

Pulito! Puoi anche controllare il registro delle chiamate degli eventi qui:

Registro eventi
Registro eventi. (Grande anteprima)

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:

Configurazione cron/trigger programmato
Configurazione cron/trigger programmato. (Grande anteprima)

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.