Erstellen einer Stock Price Notifier App mit React, Apollo GraphQL und Hasura
Veröffentlicht: 2022-03-10Das Konzept, benachrichtigt zu werden, wenn das Ereignis Ihrer Wahl eingetreten ist, ist im Vergleich dazu, sich auf den kontinuierlichen Datenstrom zu kleben, um dieses bestimmte Ereignis selbst zu finden, populär geworden. Menschen ziehen es vor, relevante E-Mails/Nachrichten zu erhalten, wenn ihr bevorzugtes Ereignis eingetreten ist, anstatt am Bildschirm hängen zu bleiben, um auf das Eintreten dieses Ereignisses zu warten. Die ereignisbasierte Terminologie ist auch in der Softwarewelt weit verbreitet.
Wie toll wäre es, wenn Sie die Kursaktualisierungen Ihrer Lieblingsaktie auf Ihrem Telefon erhalten könnten?
In diesem Artikel werden wir eine Stocks Price Notifier- Anwendung erstellen, indem wir React, Apollo GraphQL und die Hasura GraphQL-Engine verwenden. Wir werden das Projekt mit einem Boilerplate-Code zum create-react-app
starten und alles von Grund auf aufbauen. Wir werden lernen, wie man die Datenbanktabellen und Ereignisse auf der Hasura-Konsole einrichtet. Wir werden auch lernen, wie man Hasuras Ereignisse verdrahtet, um Aktienkursaktualisierungen mithilfe von Web-Push-Benachrichtigungen zu erhalten.
Hier ist ein kurzer Blick auf das, was wir bauen würden:
Lasst uns anfangen!
Ein Überblick darüber, worum es bei diesem Projekt geht
Die Aktiendaten (einschließlich Metriken wie High , Low , Open , Close , Volume ) würden in einer von Hasura unterstützten Postgres-Datenbank gespeichert. Der Benutzer könnte eine bestimmte Aktie basierend auf einem bestimmten Wert abonnieren oder sich dafür entscheiden, stündlich benachrichtigt zu werden. Der Benutzer erhält eine Web-Push-Benachrichtigung, sobald seine Abonnementkriterien erfüllt sind.
Das sieht nach einer Menge Zeug aus und es gäbe offensichtlich einige offene Fragen darüber, wie wir diese Teile aufbauen werden.
Hier ist ein Plan, wie wir dieses Projekt in vier Schritten durchführen würden:
- Abrufen der Aktiendaten mit einem NodeJs-Skript
Wir beginnen damit, die Aktiendaten mit einem einfachen NodeJs-Skript von einem der Anbieter der Aktien-API abzurufen – Alpha Vantage. Dieses Skript ruft die Daten für eine bestimmte Aktie in Intervallen von 5 Minuten ab. Die Antwort der API umfasst High , Low , Open , Close und Volume . Diese Daten werden dann in die Postgres-Datenbank eingefügt, die in das Hasura-Backend integriert ist. - Einrichten der Hasura GraphQL-Engine
Wir werden dann einige Tabellen in der Postgres-Datenbank einrichten, um Datenpunkte aufzuzeichnen. Hasura generiert automatisch die GraphQL-Schemas, Abfragen und Mutationen für diese Tabellen. - Frontend mit React und Apollo Client
Der nächste Schritt besteht darin, die GraphQL-Schicht mit dem Apollo-Client und Apollo Provider (dem von Hasura bereitgestellten GraphQL-Endpunkt) zu integrieren. Die Datenpunkte werden als Diagramme im Frontend angezeigt. Wir werden auch die Abonnementoptionen erstellen und entsprechende Mutationen auf der GraphQL-Schicht auslösen. - Einrichten von Ereignis-/geplanten Triggern
Hasura bietet ein hervorragendes Werkzeug rund um Trigger. Wir werden der Aktiendatentabelle Ereignis- und geplante Auslöser hinzufügen. Diese Auslöser werden gesetzt, wenn der Benutzer daran interessiert ist, eine Benachrichtigung zu erhalten, wenn die Aktienkurse einen bestimmten Wert erreichen (Ereignisauslöser). Der Benutzer kann sich auch dafür entscheiden, jede Stunde eine Benachrichtigung über eine bestimmte Aktie zu erhalten (geplanter Trigger).
Nun, da der Plan fertig ist, lasst ihn uns in die Tat umsetzen!
Hier ist das GitHub-Repository für dieses Projekt. Wenn Sie sich irgendwo im Code unten verirren, beziehen Sie sich auf dieses Repository und machen Sie sich wieder auf den Weg!
Abrufen der Aktiendaten mit einem NodeJs-Skript
Das ist gar nicht so kompliziert, wie es sich anhört! Wir müssen eine Funktion schreiben, die Daten mithilfe des Alpha Vantage-Endpunkts abruft, und dieser Abrufaufruf sollte in einem Intervall von 5 Minuten ausgelöst werden (Sie haben richtig geraten, wir müssen diesen Funktionsaufruf in setInterval
).
Wenn Sie sich immer noch fragen, was Alpha Vantage ist, und das nur aus Ihrem Kopf bekommen wollen, bevor Sie zum Codierungsteil springen, dann ist es hier:
Alpha Vantage Inc. ist ein führender Anbieter kostenloser APIs für Echtzeit- und historische Daten zu Aktien, Devisen (FX) und digitalen/Kryptowährungen.
Wir würden diesen Endpunkt verwenden, um die erforderlichen Metriken einer bestimmten Aktie zu erhalten. Diese API erwartet einen API-Schlüssel als einen der Parameter. Hier erhalten Sie Ihren kostenlosen API-Schlüssel. Wir sind jetzt gut genug, um zum interessanten Teil zu kommen – fangen wir an, Code zu schreiben!
Abhängigkeiten installieren
Erstellen Sie ein stocks-app
Verzeichnis und erstellen Sie darin ein server
. Initialisieren Sie es als Knotenprojekt mit npm init
und installieren Sie dann diese Abhängigkeiten:
npm i isomorphic-fetch pg nodemon --save
Dies sind die einzigen drei Abhängigkeiten, die wir benötigen, um dieses Skript zum Abrufen der Aktienkurse und zum Speichern in der Postgres-Datenbank zu schreiben.
Hier ist eine kurze Erklärung dieser Abhängigkeiten:
-
isomorphic-fetch
Es macht es einfach,fetch
isomorph (in der gleichen Form) sowohl auf dem Client als auch auf dem Server zu verwenden. -
pg
Es ist ein nicht blockierender PostgreSQL-Client für NodeJs. -
nodemon
Es startet den Server automatisch neu, wenn sich eine Datei im Verzeichnis ändert.
Einrichten der Konfiguration
Fügen Sie eine config.js
-Datei auf Stammebene hinzu. Fügen Sie das folgende Code-Snippet vorerst in diese Datei ein:
const config = { user: '<DATABASE_USER>', password: '<DATABASE_PASSWORD>', host: '<DATABASE_HOST>', port: '<DATABASE_PORT>', database: '<DATABASE_NAME>', ssl: '<IS_SSL>', apiHost: 'https://www.alphavantage.co/', }; module.exports = config;
user
, password
, host
, port
, database
und ssl
beziehen sich auf die Postgres-Konfiguration. Wir werden darauf zurückkommen, um dies zu bearbeiten, während wir den Hasura-Motorteil einrichten!
Initialisieren des Postgres-Verbindungspools zum Abfragen der Datenbank
Ein connection pool
ist ein geläufiger Begriff in der Informatik und Sie werden diesen Begriff oft hören, wenn Sie mit Datenbanken zu tun haben.
Beim Abfragen von Daten in Datenbanken müssen Sie zunächst eine Verbindung zur Datenbank herstellen. Diese Verbindung übernimmt die Datenbankanmeldeinformationen und gibt Ihnen einen Hook, um alle Tabellen in der Datenbank abzufragen.
Hinweis : Das Herstellen von Datenbankverbindungen ist kostspielig und verschwendet auch erhebliche Ressourcen. Ein Verbindungspool speichert die Datenbankverbindungen zwischen und verwendet sie bei nachfolgenden Abfragen wieder. Wenn alle offenen Verbindungen verwendet werden, wird eine neue Verbindung hergestellt und dann dem Pool hinzugefügt.
Nachdem nun klar ist, was der Verbindungspool ist und wofür er verwendet wird, beginnen wir damit, eine Instanz des pg
-Verbindungspools für diese Anwendung zu erstellen:
Fügen Sie die Datei pool.js
auf Stammebene hinzu und erstellen Sie eine Poolinstanz wie folgt:
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;
Die obigen Codezeilen erstellen eine Instanz von Pool
mit den in der Konfigurationsdatei festgelegten Konfigurationsoptionen. Wir müssen die Konfigurationsdatei noch vervollständigen, aber es wird keine Änderungen in Bezug auf die Konfigurationsoptionen geben.
Wir haben jetzt den Grundstein gelegt und sind bereit, mit einigen API-Aufrufen an den Alpha Vantage-Endpunkt zu beginnen.
Kommen wir zum Interessanten!
Abrufen der Aktiendaten
In diesem Abschnitt werden wir die Bestandsdaten vom Alpha Vantage-Endpunkt abrufen. Hier ist die index.js
-Datei:
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); }); }); }) })()
Für die Zwecke dieses Projekts werden wir die Preise nur für diese Aktien abfragen – NFLX (Netflix), MSFT (Microsoft), AMZN (Amazon), W (Wayfair), FB (Facebook).
Verweisen Sie auf diese Datei für die Konfigurationsoptionen. Die Funktion IIFE getStocksData
macht nicht viel! Es durchläuft diese Symbole und fragt den Alpha Vantage-Endpunkt ${host}query/?function=${timeSeriesFunction}&symbol=${symbol}&interval=${interval}&apikey=${key}
, um die Metriken für diese Aktien zu erhalten.
Die Funktion insertStocksData
diese Datenpunkte in die Postgres-Datenbank ein. Hier ist die insertStocksData
-Funktion:
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); }); };
Das ist es! Wir haben Datenpunkte der Aktie von der Alpha Vantage API abgerufen und eine Funktion geschrieben, um diese in der Postgres-Datenbank in der Tabelle stock_data
. Es fehlt nur noch ein Teil, um all dies zum Laufen zu bringen! Wir müssen die richtigen Werte in die Konfigurationsdatei einfügen. Wir erhalten diese Werte nach dem Einrichten der Hasura-Engine. Kommen wir gleich dazu!
Den vollständigen Code zum Abrufen von Datenpunkten vom Alpha Vantage-Endpunkt und zum Einfügen in die Hasura Postgres-Datenbank finden Sie im server
.
Wenn dieser Ansatz zum Einrichten von Verbindungen, Konfigurationsoptionen und Einfügen von Daten mithilfe der Rohabfrage etwas schwierig aussieht, machen Sie sich darüber bitte keine Sorgen! Wir werden lernen, wie man all dies auf einfache Weise mit einer GraphQL-Mutation macht, sobald die Hasura-Engine eingerichtet ist!
Einrichten der Hasura GraphQL-Engine
Es ist wirklich einfach, die Hasura-Engine einzurichten und mit den GraphQL-Schemas, Abfragen, Mutationen, Abonnements, Ereignisauslösern und vielem mehr loszulegen!
Klicken Sie auf Hasura ausprobieren und geben Sie den Projektnamen ein:
Ich verwende die auf Heroku gehostete Postgres-Datenbank. Erstellen Sie eine Datenbank auf Heroku und verknüpfen Sie sie mit diesem Projekt. Sie sollten dann bereit sein, die Leistungsfähigkeit der abfragereichen Hasura-Konsole zu erleben.
Bitte kopieren Sie die Postgres-DB-URL, die Sie nach dem Erstellen des Projekts erhalten. Wir müssen dies in die Konfigurationsdatei einfügen.
Klicken Sie auf Konsole starten und Sie werden zu dieser Ansicht weitergeleitet:
Beginnen wir mit dem Erstellen des Tabellenschemas, das wir für dieses Projekt benötigen.
Erstellen eines Tabellenschemas in der Postgres-Datenbank
Bitte gehen Sie zur Registerkarte Daten und klicken Sie auf Tabelle hinzufügen! Beginnen wir mit der Erstellung einiger Tabellen:
symbol
Diese Tabelle würde zum Speichern der Informationen der Symbole verwendet werden. Im Moment habe ich hier zwei Felder beibehalten – id
und company
. Die Feld- id
ist ein Primärschlüssel und company
ist vom Typ varchar
. Lassen Sie uns einige der Symbole in dieser Tabelle hinzufügen:
stock_data
Tabelle
Die Tabelle stock_data
speichert id
, symbol
, time
und die Metriken wie high
, low
, open
, close
, volume
. Das NodeJs-Skript, das wir zuvor in diesem Abschnitt geschrieben haben, wird verwendet, um diese spezielle Tabelle zu füllen.
So sieht die Tabelle aus:
Sauber! Kommen wir zur anderen Tabelle im Datenbankschema!
user_subscription
Tabelle
Die user_subscription
Tabelle speichert das Abonnementobjekt für die Benutzer-ID. Dieses Abonnementobjekt wird zum Senden von Web-Push-Benachrichtigungen an die Benutzer verwendet. Wir werden später in diesem Artikel erfahren, wie dieses Abonnementobjekt generiert wird.
Es gibt zwei Felder in dieser Tabelle – id
ist der Primärschlüssel vom Typ uuid
und das Abonnementfeld ist vom Typ jsonb
.
events
Dies ist der wichtige und wird zum Speichern der Benachrichtigungsereignisoptionen verwendet. Wenn sich ein Benutzer für die Preisaktualisierungen einer bestimmten Aktie entscheidet, speichern wir diese Ereignisinformationen in dieser Tabelle. Diese Tabelle enthält diese Spalten:
-
id
: ist ein Primärschlüssel mit der Eigenschaft auto-increment. -
symbol
: ist ein Textfeld. -
user_id
: ist vom Typuuid
. -
trigger_type
: wird zum Speichern des Ereignis-Triggertyps verwendet —time/event
. -
trigger_value
: wird zum Speichern des Triggerwerts verwendet. Wenn sich ein Benutzer beispielsweise für einen preisbasierten Ereignisauslöser entschieden hat – er möchte Updates, wenn der Preis der Aktie 1000 erreicht hat, dann wäre dertrigger_value
1000 und dertrigger_type
wäreevent
.
Dies sind alle Tabellen, die wir für dieses Projekt benötigen würden. Wir müssen auch Beziehungen zwischen diesen Tabellen einrichten, um einen reibungslosen Datenfluss und Verbindungen zu haben. Lass uns das tun!
Beziehungen zwischen Tabellen einrichten
Die events
wird zum Senden von Web-Push-Benachrichtigungen basierend auf dem Ereigniswert verwendet. Daher ist es sinnvoll, diese Tabelle mit der Tabelle user_subscription
zu verbinden, um Push-Benachrichtigungen zu den in dieser Tabelle gespeicherten Abonnements senden zu können.
events.user_id → user_subscription.id
Die stock_data
Tabelle ist wie folgt mit der Symboltabelle verknüpft:
stock_data.symbol → symbol.id
Wir müssen auch einige Beziehungen in der symbol
konstruieren:
stock_data.symbol → symbol.id events.symbol → symbol.id
Wir haben jetzt die erforderlichen Tabellen erstellt und auch die Beziehungen zwischen ihnen hergestellt! Wechseln wir zum GRAPHIQL
Tab auf der Konsole, um die Magie zu sehen!
Hasura hat bereits die GraphQL-Abfragen basierend auf diesen Tabellen eingerichtet:
Es ist ganz einfach, diese Tabellen abzufragen, und Sie können auch alle diese Filter/Eigenschaften ( distinct_on
, limit
, offset
, order_by
, where
) anwenden, um die gewünschten Daten zu erhalten.
Das sieht alles gut aus, aber wir haben unseren serverseitigen Code immer noch nicht mit der Hasura-Konsole verbunden. Lassen Sie uns diesen Teil vervollständigen!
Verbinden des NodeJs-Skripts mit der Postgres-Datenbank
Bitte fügen Sie die erforderlichen Optionen in der Datei config.js
im server
wie folgt ein:
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;
Bitte fügen Sie diese Optionen aus der Datenbankzeichenfolge ein, die generiert wurde, als wir die Postgres-Datenbank auf Heroku erstellt haben.
Die apiHostOptions
bestehen aus den API-bezogenen Optionen wie host
, key
, timeSeriesFunction
und interval
.
Sie erhalten das Feld graphqlURL
auf der Registerkarte GRAPHIQL in der Hasura-Konsole.
Die getConfig
Funktion wird verwendet, um den angeforderten Wert aus dem Konfigurationsobjekt zurückzugeben. Wir haben dies bereits in index.js
im server
.
Es ist an der Zeit, den Server auszuführen und einige Daten in die Datenbank zu füllen. Ich habe ein Skript in package.json
als:
"scripts": { "start": "nodemon index.js" }
Führen Sie npm start
auf dem Terminal aus und die Datenpunkte des Symbolarrays in index.js
sollten in die Tabellen gefüllt werden.
Umgestaltung der Rohabfrage im NodeJs-Skript in die GraphQL-Mutation
Nachdem die Hasura-Engine nun eingerichtet ist, sehen wir uns an, wie einfach es sein kann, eine Mutation in der stock_data
Tabelle aufzurufen.
Die Funktion insertStocksData
in queries.js
verwendet eine Rohabfrage:
const query = 'INSERT INTO stock_data (symbol, high, low, open, close, volume, time) VALUES ($1, $2, $3, $4, $5, $6, $7)';
Lassen Sie uns diese Abfrage umgestalten und die von der Hasura-Engine unterstützte Mutation verwenden. Hier ist die umgestaltete queries.js
im Serververzeichnis:
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 }
Bitte beachten Sie: Wir müssen graphqlURL
in der Datei config.js
hinzufügen.
Das apollo-fetch
Modul gibt eine Abruffunktion zurück, mit der das Datum auf dem GraphQL-Endpunkt abgefragt/verändert werden kann. Einfach genug, oder?
Die einzige Änderung, die wir in index.js
, besteht darin, das Aktienobjekt in dem Format zurückzugeben, das von der Funktion insertStocksData
gefordert wird. Bitte sehen Sie sich index2.js
und queries2.js
für den vollständigen Code mit diesem Ansatz an.
Nachdem wir nun die Datenseite des Projekts abgeschlossen haben, wollen wir uns dem Front-End-Bit zuwenden und einige interessante Komponenten erstellen!
Hinweis : Bei diesem Ansatz müssen wir die Datenbankkonfigurationsoptionen nicht beibehalten!
Frontend mit React und Apollo Client
Das Frontend-Projekt befindet sich im selben Repository und wird mit dem Paket create-react-app
erstellt. Der mit diesem Paket generierte Service-Worker unterstützt das Zwischenspeichern von Assets, lässt jedoch nicht zu, dass weitere Anpassungen zur Service-Worker-Datei hinzugefügt werden. Es gibt bereits einige offene Probleme, um Unterstützung für benutzerdefinierte Service-Worker-Optionen hinzuzufügen. Es gibt Möglichkeiten, dieses Problem zu lösen und Unterstützung für einen benutzerdefinierten Servicemitarbeiter hinzuzufügen.
Betrachten wir zunächst die Struktur für das Frontend-Projekt:
Bitte überprüfen Sie das src
Verzeichnis! Machen Sie sich vorerst keine Sorgen um die Service Worker-bezogenen Dateien. Wir werden später in diesem Abschnitt mehr über diese Dateien erfahren. Der Rest der Projektstruktur sieht einfach aus. Der components
die Komponenten (Loader, Chart); der services
enthält einige der Hilfsfunktionen/Dienste, die zum Transformieren von Objekten in die erforderliche Struktur verwendet werden; styles
enthält, wie der Name schon sagt, die Sass-Dateien, die zum Stylen des Projekts verwendet werden; views
ist das Hauptverzeichnis und enthält die View-Layer-Komponenten.
Für dieses Projekt benötigen wir nur zwei Ansichtskomponenten – die Symbolliste und die Symbolzeitreihe. Wir erstellen die Zeitreihe mit der Chart-Komponente aus der Highcharts-Bibliothek. Beginnen wir mit dem Hinzufügen von Code in diesen Dateien, um die Teile am Front-End aufzubauen!
Abhängigkeiten installieren
Hier ist die Liste der Abhängigkeiten, die wir benötigen:
-
apollo-boost
Apollo Boost ist eine konfigurationsfreie Möglichkeit, mit der Verwendung von Apollo Client zu beginnen. Es wird mit den Standardkonfigurationsoptionen geliefert. -
reactstrap
undbootstrap
Die Komponenten werden mit diesen beiden Paketen erstellt. -
graphql
undgraphql-type-json
graphql
ist eine erforderliche Abhängigkeit für die Verwendung vonapollo-boost
undgraphql-type-json
wird zur Unterstützung desjson
-Datentyps verwendet, der im GraphQL-Schema verwendet wird. highcharts
undhighcharts-react-official
Und diese beiden Pakete werden zum Erstellen des Diagramms verwendet:node-sass
Dies wird hinzugefügt, um Sass-Dateien für das Styling zu unterstützen.uuid
Dieses Paket wird zum Generieren starker Zufallswerte verwendet.
All diese Abhängigkeiten werden Sinn machen, sobald wir damit beginnen, sie im Projekt zu verwenden. Kommen wir zum nächsten Bit!
Apollo-Client einrichten
Erstellen Sie apolloClient.js
im src
-Ordner wie folgt:
import ApolloClient from 'apollo-boost'; const apolloClient = new ApolloClient({ uri: '<HASURA_CONSOLE_URL>' }); export default apolloClient;
Der obige Code instanziiert ApolloClient und nimmt uri
in die Konfigurationsoptionen auf. Der uri
ist die URL Ihrer Hasura-Konsole. Sie erhalten dieses GRAPHIQL
-Feld auf der Registerkarte uri
im Abschnitt GraphQL-Endpunkt .
Der obige Code sieht einfach aus, aber er kümmert sich um den Hauptteil des Projekts! Es verbindet das auf Hasura aufgebaute GraphQL-Schema mit dem aktuellen Projekt.
Wir müssen dieses Apollo-Client-Objekt auch an ApolloProvider
und die Root-Komponente innerhalb von ApolloProvider
. Dadurch können alle verschachtelten Komponenten innerhalb der Hauptkomponente client
verwenden und Abfragen für dieses Clientobjekt auslösen.
Ändern wir die Datei index.js
wie folgt:
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') );
Bitte ignorieren Sie den insertSubscription
-bezogenen Code. Wir werden das später im Detail verstehen. Der Rest des Codes sollte einfach zu umgehen sein. Die render
-Funktion übernimmt die Root-Komponente und die elementId als Parameter. Beachten Sie, dass der client
(ApolloClient-Instanz) als Prop an ApolloProvider
. Sie können die vollständige index.js
-Datei hier überprüfen.
Einrichten des Custom Service Worker
Ein Service Worker ist eine JavaScript-Datei, die Netzwerkanfragen abfangen kann. Es wird verwendet, um den Cache abzufragen, um zu prüfen, ob das angeforderte Asset bereits im Cache vorhanden ist, anstatt zum Server zu fahren. Servicemitarbeiter werden auch zum Senden von Web-Push-Benachrichtigungen an die abonnierten Geräte verwendet.
Wir müssen Web-Push-Benachrichtigungen für die Aktienkursaktualisierungen an die abonnierten Benutzer senden. Lassen Sie uns den Grundstein legen und diese Service-Worker-Datei erstellen!
Der in der Datei insertSubscription
enthaltene Ausschnitt von insertSubscription übernimmt die Registrierung des Dienstmitarbeiters und das index.js
des Abonnementobjekts in die Datenbank mithilfe von subscriptionMutation
.
Bitte beziehen Sie sich auf querys.js für alle Abfragen und Mutationen, die im Projekt verwendet werden.
serviceWorker.register(insertSubscription);
ruft die in der Datei serviceWorker.js
geschriebene register
auf. Hier ist es:
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(); }) } }
Die obige Funktion prüft zunächst, ob serviceWorker
vom Browser unterstützt wird, und registriert dann die auf der URL swUrl
gehostete Service-Worker-Datei. Wir werden diese Datei gleich überprüfen!
Die getSubscription
Funktion erledigt die Arbeit des Abrufens des Subskriptionsobjekts mithilfe der subscribe
-Methode für das pushManager
Objekt. Dieses Subskriptionsobjekt wird dann in der user_subscription
Tabelle gegen eine userId gespeichert. Bitte beachten Sie, dass die userId mit der uuid
-Funktion generiert wird. Schauen wir uns die getSubscription
Funktion an:
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 })); }) } }) }
Sie können die Datei serviceWorker.js
auf den vollständigen Code überprüfen!
Notification.requestPermission()
hat dieses Popup aufgerufen, das den Benutzer nach der Erlaubnis zum Senden von Benachrichtigungen fragt. Sobald der Benutzer auf Zulassen klickt, wird vom Push-Dienst ein Abonnementobjekt generiert. Wir speichern dieses Objekt im localStorage als:
Der endpoint
im obigen Objekt wird zur Identifizierung des Geräts verwendet, und der Server verwendet diesen Endpunkt, um Web-Push-Benachrichtigungen an den Benutzer zu senden.
Wir haben die Arbeit der Initialisierung und Registrierung des Servicemitarbeiters erledigt. Wir haben auch das Abonnementobjekt des Benutzers! Dies funktioniert gut, da die Datei serviceWorker.js
im public
Ordner vorhanden ist. Lassen Sie uns jetzt den Servicemitarbeiter einrichten, um die Dinge vorzubereiten!
Dies ist ein etwas schwieriges Thema, aber lass es uns richtig angehen! Wie bereits erwähnt, unterstützt das Dienstprogramm create-react-app
standardmäßig keine Anpassungen für den Service Worker. Wir können die Implementierung von Kundendienstmitarbeitern mit workbox-build
Modul erreichen.
Wir müssen auch sicherstellen, dass das Standardverhalten des Pre-Cachings von Dateien intakt ist. Wir ändern den Teil, in dem der Service Worker im Projekt erstellt wird. Und Workbox-Build hilft dabei, genau das zu erreichen! Ordentliches Zeug! Lassen Sie es uns einfach halten und alles auflisten, was wir tun müssen, damit der Custom Service Worker funktioniert:
- Behandeln Sie das Pre-Caching von Assets mit
workboxBuild
. - Erstellen Sie eine Service-Worker-Vorlage zum Zwischenspeichern von Assets.
- Erstellen Sie die Datei
sw-precache-config.js
, um benutzerdefinierte Konfigurationsoptionen bereitzustellen. - Fügen Sie das Build-Service-Worker-Skript im Build-Schritt in
package.json
.
Machen Sie sich keine Sorgen, wenn das alles verwirrend klingt! Der Artikel konzentriert sich nicht darauf, die Semantik hinter jedem dieser Punkte zu erklären. Wir müssen uns vorerst auf den Implementierungsteil konzentrieren! Ich werde versuchen, die Gründe für die ganze Arbeit, um einen Zolldienstmitarbeiter zu machen, in einem anderen Artikel zu behandeln.
Lassen Sie uns zwei Dateien sw-build.js
und sw-custom.js
im src
-Verzeichnis erstellen. Bitte beachten Sie die Links zu diesen Dateien und fügen Sie den Code zu Ihrem Projekt hinzu.
Lassen Sie uns nun die Datei sw-precache-config.js
auf Root-Ebene erstellen und den folgenden Code in diese Datei einfügen:
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' }] }
Ändern wir auch die Datei „ package.json
“, um Platz für die Erstellung der benutzerdefinierten Service-Worker-Datei zu schaffen:
Fügen Sie diese Anweisungen im scripts
hinzu:
"build-sw": "node ./src/sw-build.js", "clean-cra-sw": "rm -f build/precache-manifest.*.js && rm -f build/service-worker.js",
Und ändern Sie das build
-Skript wie folgt:
"build": "react-scripts build && npm run build-sw && npm run clean-cra-sw",
Die Einrichtung ist endlich fertig! Wir müssen jetzt eine benutzerdefinierte Service-Worker-Datei im public
Ordner hinzufügen:
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)); })
Wir haben gerade einen push
-Listener hinzugefügt, um auf Push-Benachrichtigungen zu hören, die vom Server gesendet werden. Die Funktion showNotification
dient dazu, dem Benutzer Web-Push-Benachrichtigungen anzuzeigen.
Das ist es! Wir haben die harte Arbeit hinter uns, einen benutzerdefinierten Servicemitarbeiter einzurichten, der Web-Push-Benachrichtigungen bearbeitet. Wir werden diese Benachrichtigungen in Aktion sehen, sobald wir die Benutzeroberflächen erstellt haben!
Wir nähern uns dem Bau der Hauptcodeteile. Beginnen wir jetzt mit der ersten Ansicht!
Symbollistenansicht
Die im vorherigen Abschnitt verwendete App
Komponente sieht folgendermaßen aus:
import React from 'react'; import SymbolList from './views/symbolList'; const App = () => { return <SymbolList />; }; export default App;
Es ist eine einfache Komponente, die die SymbolList
-Ansicht zurückgibt, und SymbolList
übernimmt die ganze Arbeit, um Symbole in einer sauber verknüpften Benutzeroberfläche anzuzeigen.
Schauen wir uns symbolList.js
im Ordner views
an:
Bitte beachten Sie die Datei hier!
Die Komponente gibt die Ergebnisse der Funktion renderSymbols
zurück. Und diese Daten werden mit dem useQuery
Hook wie folgt aus der Datenbank abgerufen:
const { loading, error, data } = useQuery(symbolsQuery, {variables: { userId }});
Die symbolsQuery
ist definiert als:
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 } } } } } `;
Es nimmt die Benutzer- userId
auf und ruft die abonnierten Ereignisse dieses bestimmten Benutzers ab, um den korrekten Zustand des Benachrichtigungssymbols (Glockensymbol, das zusammen mit dem Titel angezeigt wird) anzuzeigen. Die Abfrage ruft auch die Höchst- und Mindestwerte der Aktie ab. Beachten Sie die Verwendung von aggregate
in der obigen Abfrage. Die Aggregationsabfragen von Hasura erledigen die Arbeit hinter den Kulissen, um die aggregierten Werte wie count
, sum
, avg
, max
, min
usw. abzurufen.
Basierend auf der Antwort des obigen GraphQL-Aufrufs ist hier die Liste der Karten, die im Frontend angezeigt werden:
Die HTML-Struktur der Karte sieht in etwa so aus:
<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>
Wir verwenden die Card
Komponente von ReactStrap, um diese Karten zu rendern. Die Popover
-Komponente wird zum Anzeigen der abonnementbasierten Optionen verwendet:
Wenn der Benutzer auf das bell
für eine bestimmte Aktie klickt, kann er sich dafür entscheiden, jede Stunde benachrichtigt zu werden oder wenn der Kurs der Aktie den eingegebenen Wert erreicht hat. Wir werden dies im Abschnitt „Ereignisse/Zeitauslöser“ in Aktion sehen.
Hinweis : Zur StockTimeseries
Komponente kommen wir im nächsten Abschnitt!
Den vollständigen Code für die symbolList.js
Sie in symbolList.js.
Aktien-Zeitreihenansicht
Die StockTimeseries
Komponente verwendet die Abfrage 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 } } `;
Die obige Abfrage ruft die letzten 25 Datenpunkte der ausgewählten Aktie ab. Hier ist zum Beispiel das Diagramm für die Facebook- Aktienöffnungsmetrik :
Dies ist eine einfache Komponente, bei der wir einige Diagrammoptionen an die Komponente [ HighchartsReact
] übergeben. Hier sind die Diagrammoptionen:
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') } ] }
Die X-Achse zeigt die Zeit und die Y-Achse zeigt den metrischen Wert zu diesem Zeitpunkt. Die Funktion getDataPoints
wird zum Generieren einer Reihe von Punkten für jede der Reihen verwendet.
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; }
Einfach! So wird die Chart-Komponente generiert! Den vollständigen Code für Aktienzeitreihen finden Sie in den Dateien Chart.js und stockTimeseries.js
.
Sie sollten jetzt mit den Daten und dem Teil der Benutzerschnittstellen des Projekts fertig sein. Kommen wir nun zum interessanten Teil – dem Einrichten von Ereignis-/Zeitauslösern basierend auf den Eingaben des Benutzers.
Ereignis-/geplante Trigger einrichten
In diesem Abschnitt erfahren Sie, wie Sie Trigger auf der Hasura-Konsole einrichten und Web-Push-Benachrichtigungen an die ausgewählten Benutzer senden. Lass uns anfangen!
Ereignisauslöser auf der Hasura-Konsole
Lassen Sie uns einen Event-Trigger stock_value
für die Tabelle stock_data
und als Trigger-Operation insert
. Der Webhook wird jedes Mal ausgeführt, wenn eine Einfügung in die stock_data
Tabelle erfolgt.
Wir werden ein Glitch-Projekt für die Webhook-URL erstellen. Lassen Sie mich ein wenig über Webhooks schreiben, um das Verständnis zu verdeutlichen:
Webhooks werden zum Senden von Daten von einer Anwendung zu einer anderen beim Auftreten eines bestimmten Ereignisses verwendet. Wenn ein Ereignis ausgelöst wird, erfolgt ein HTTP-POST-Aufruf an die Webhook-URL mit den Ereignisdaten als Nutzdaten.
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!
Abrufen von abonnierten Benutzern
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()); }
In der obigen Funktion handleStockValueTrigger
wir zuerst die abonnierten Benutzer mit der Funktion getSubscribedUsers
. Wir senden dann Web-Push-Benachrichtigungen an jeden dieser Benutzer. Zum Versenden der Benachrichtigung wird die Funktion sendWebpush
verwendet. Wir werden uns gleich die Web-Push-Implementierung ansehen.
Die Funktion getSubscribedUsers
verwendet die Abfrage:
query getSubscribedUsers($symbol: String, $triggerValue: numeric) { events(where: {symbol: {_eq: $symbol}, trigger_type: {_eq: "event"}, trigger_value: {_gte: $triggerValue}}) { user_id user_subscription { subscription } } }
Diese Abfrage nimmt das user_subscription
und den Wert auf und ruft die Benutzerdetails ab, einschließlich user-id
und Benutzerabonnement, die diesen Bedingungen entsprechen:
-
symbol
gleich demjenigen, das in der Nutzlast übergeben wird. -
trigger_type
ist gleichevent
. -
trigger_value
ist größer oder gleich dem Wert, der an diese Funktion übergeben wird (in diesem Fallclose
).
Sobald wir die Liste der Benutzer erhalten haben, müssen wir ihnen nur noch Web-Push-Benachrichtigungen senden! Machen wir gleich!
Senden von Web-Push-Benachrichtigungen an die abonnierten Benutzer
Wir müssen zuerst die öffentlichen und privaten VAPID-Schlüssel abrufen, um Web-Push-Benachrichtigungen zu senden. Bitte speichern Sie diese Schlüssel in der .env
-Datei und legen Sie diese Details in index.js
wie folgt fest:
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)) }
Die sendNotification
-Funktion wird zum Senden des Web-Push an den als ersten Parameter bereitgestellten Abonnement-Endpunkt verwendet.
Das ist alles, was erforderlich ist, um erfolgreich Web-Push-Benachrichtigungen an die abonnierten Benutzer zu senden. Hier ist der vollständige Code, der in index.js
definiert ist:
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"); });
Testen wir diesen Ablauf, indem wir eine Aktie mit einem bestimmten Wert abonnieren und diesen Wert manuell in die Tabelle einfügen (zum Testen)!
Ich habe AMZN
mit dem Wert 2000
abonniert und dann einen Datenpunkt mit diesem Wert in die Tabelle eingefügt. So hat mich die Aktienmelde-App direkt nach dem Einfügen benachrichtigt:
Sauber! Sie können das Ereignisaufrufprotokoll auch hier überprüfen:
Der Webhook erledigt die Arbeit wie erwartet! Wir sind jetzt bereit für die Ereignisauslöser!
Geplante/Cron-Trigger
Wir können einen zeitbasierten Trigger für die stündliche Benachrichtigung der Abonnenten-Benutzer erreichen, indem wir den Cron-Ereignis-Trigger wie folgt verwenden:
Wir können dieselbe Webhook-URL verwenden und die abonnierten Benutzer basierend auf dem Auslöserereignistyp als stock_price_time_based_trigger
. Die Implementierung ähnelt dem ereignisbasierten Trigger.
Fazit
In diesem Artikel haben wir eine Anwendung zur Anzeige von Aktienkursen erstellt. Wir haben gelernt, wie man Preise mit den Alpha Vantage APIs abruft und die Datenpunkte in der von Hasura unterstützten Postgres-Datenbank speichert. Wir haben auch gelernt, wie man die Hasura GraphQL-Engine einrichtet und ereignisbasierte und geplante Trigger erstellt. Wir haben ein Glitch-Projekt zum Senden von Web-Push-Benachrichtigungen an die abonnierten Benutzer erstellt.