Tworzenie aplikacji do powiadamiania o cenach akcji za pomocą React, Apollo GraphQL i Hasura

Opublikowany: 2022-03-10
Szybkie podsumowanie ↬ W tym artykule dowiemy się, jak zbudować aplikację opartą na zdarzeniach i wysłać powiadomienie web-push po wyzwoleniu określonego zdarzenia. Skonfigurujemy tabele bazy danych, zdarzenia i zaplanowane wyzwalacze w silniku Hasura GraphQL i połączymy punkt końcowy GraphQL z aplikacją typu front-end, aby zarejestrować preferencje cen akcji użytkownika.

Koncepcja otrzymywania powiadomienia o zaistnieniu wybranego zdarzenia stała się popularna w porównaniu do przyklejania się do ciągłego strumienia danych w celu samodzielnego znalezienia tego konkretnego zdarzenia. Ludzie wolą otrzymywać odpowiednie e-maile/wiadomości, gdy ich preferowane zdarzenie miało miejsce, w przeciwieństwie do wiązania się z ekranem, aby czekać na to zdarzenie. Terminologia oparta na zdarzeniach jest również dość powszechna w świecie oprogramowania.

Jak niesamowite będzie, gdybyś mógł uzyskać aktualizacje ceny ulubionego zapasów w telefonie?

W tym artykule zbudujemy aplikację Notifier Price Notifier przy użyciu silnika React, Apollo GraphQL i Hasura GraphQL. Będziemy rozpocząć projekt z kodu z kotłowików Create create-react-app React-APP i zbudowałby wszystko, co uzasadniają. Dowiemy się, jak skonfigurować tabele bazy danych i zdarzenia na konsoli Hasura. Dowiemy się również, jak wydrukować wydarzenia Hasury, aby uzyskać aktualizacje ceny za pomocą powiadomień Web-Push.

Oto szybkie spojrzenie na to, co będziemy budować:

Omówienie aplikacji powiadamiania o cenie akcji
Aplikacja powiadamiająca o cenie akcji

Kontynuujmy!

Więcej po skoku! Kontynuuj czytanie poniżej ↓

Przegląd tego, o czym jest ten projekt

Dane giełdowe (w tym wskaźniki takie jak wysokie , niskie , otwarte , zamknięte , wolumen ) byłyby przechowywane w bazie danych Postgres wspieranej przez Hasura. Użytkownik będzie mógł subskrybować określone akcje na podstawie określonej wartości lub może zdecydować się na otrzymywanie powiadomień co godzinę. Użytkownik otrzyma powiadomienie internetowe po spełnieniu kryteriów subskrypcji.

Wygląda to na wiele rzeczy i oczywiście byłyby otwarte pytania dotyczące tego, jak będziemy budować te elementy.

Oto plan, jak zrealizować ten projekt w czterech krokach:

  1. Pobieranie danych giełdowych za pomocą skryptu NodeJs
    Zaczniemy od pobrania danych giełdowych za pomocą prostego skryptu NodeJs od jednego z dostawców API giełdowego — Alpha Vantage. Ten skrypt będzie pobierał dane dla konkretnego towaru w odstępach 5 minut. Odpowiedź API obejmuje wysoki , niski , otwarty , zamknięty i głośność . Dane te zostaną następnie włożone do bazy danych Postgre, która jest zintegrowana z tylną częścią Hasury.
  2. Konfigurowanie silnika Hasury Graphql
    Następnie skonfigurujemy kilka tabel w bazie danych Postgres, aby rejestrować punkty danych. Hasura automatycznie generuje schematy Graphql, zapytania i mutacje dla tych tabel.
  3. Front-end wykorzystujący React i Apollo Client
    Następnym krokiem jest integracja warstwy GraphQL za pomocą klienta Apollo i Apollo Provider (punkt końcowy GraphQL dostarczony przez Hasura). Punkty danych zostaną pokazane jako wykresy na interfejsie użytkownika. Będziemy również zbudować opcje subskrypcji i odbieramy odpowiednie mutacje na warstwie Graphql.
  4. Konfigurowanie wyzwalaczy zdarzeń/zaplanowanych
    Hasura zapewnia doskonałe narzędzia wokół wyzwalaczy. Będziemy dodawać zdarzenia i zaplanowane wyzwalacze w tabeli danych giełdowych. Te wyzwalacze zostaną ustawione, jeśli użytkownik jest zainteresowany otrzymaniem powiadomienia, gdy ceny akcji osiągną określoną wartość (wyzwalacz zdarzenia). Użytkownik może również zdecydować się na uzyskanie powiadomienia o określonym stanie co godzinę (zaplanowany spust).

Teraz, kiedy plan jest gotowy, włóżmy do działania!

Oto repozytorium GitHub dla tego projektu. Jeśli zgubisz się w dowolnym miejscu poniżej, zapoznaj się z tym repozytorium i wróć do prędkości!

Pobieranie danych giełdowych za pomocą skryptu NodeJs

To nie jest takie skomplikowane, jak się wydaje! Będziemy musieli napisać funkcję, która pobiera dane za pomocą punktu końcowego Alpha Vantage, a to wywołanie pobierania powinno być uruchamiane w odstępie 5 minut (zgadliście, będziemy musieli umieścić to wywołanie funkcji w setInterval ).

Jeśli nadal zastanawiasz się, czym jest Alpha Vantage i chcesz po prostu wyrzucić to z głowy przed przejściem do części kodowania, oto ona:

Alpha Vantage Inc. jest wiodącym dostawcą bezpłatnych interfejsów API dla danych w czasie rzeczywistym i danych historycznych dotyczących akcji, rynku walutowego (FX) oraz walut cyfrowych/kryptowalut.

Korzystalibyśmy z tego punktu końcowego, aby uzyskać wymagane metryki konkretnego zapasu. Ten API oczekuje interfejsu API jako jednego z parametrów. Możesz otrzymać bezpłatny klucz API stąd. Możemy teraz przejść do interesującego fragmentu — zacznijmy pisać trochę kodu!

Instalowanie zależności

Utwórz katalog stocks-app i utwórz w nim katalog server . Zainicjuj go jako projekt węzła za pomocą npm init , a następnie zainstaluj te zależności:

 npm i isomorphic-fetch pg nodemon --save

To jedyne trzy zależności, których potrzebowalibyśmy do napisania tego skryptu pobierania cen akcji i przechowywania ich w bazie danych Postgres.

Oto krótkie wyjaśnienie tych zależności:

  • isomorphic-fetch
    Ułatwia korzystanie z fetch izomorficznie (w tej samej formie) zarówno na kliencie, jak i serwerowi.
  • pg
    Jest to nie blokujący klient PostgreSQL dla Nodejs.
  • nodemon
    Automatycznie restartuje serwer po każdej zmianie pliku w katalogu.

Konfiguracja konfiguracji

Dodaj plik config.js na poziomie głównym. Dodaj poniżej poniższy fragment kodu w tym pliku:

 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 , ssl są powiązane z konfiguracją Postgres. Wrócimy, aby to edytować, podczas gdy będziemy konfigurować część silnika Hasura!

Inicjowanie puli połączeń Postgres w celu przeszukiwania bazy danych

connection pool jest powszechnym terminem w informatyce i często będziesz go słyszeć podczas pracy z bazami danych.

Podczas wysyłania danych w bazach danych musisz najpierw nawiązać połączenie z bazą danych. To połączenie pobiera poświadczenia bazy danych i daje możliwość przeszukiwania dowolnych tabel w bazie danych.

Uwaga : Ustanowienie połączeń bazy danych jest kosztowne, a także marnuje znaczące zasoby. Połączenie połączeń buforuje połączenia bazy danych i ponowne wykorzystuje je na kolejnych zapytań. Jeśli wszystkie otwarte połączenia są używane, założono nowe połączenie, a następnie dodano do basenu.

Teraz, gdy jest już jasne, czym jest pula połączeń i do czego jest używana, zacznijmy od utworzenia instancji puli połączeń pg dla tej aplikacji:

Dodaj plik pool.js na poziomie korzenia i utwórz instancję basenu jako:

 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;

Powyższe wiersze kodu tworzą instancję Pool z opcjami konfiguracyjnymi zgodnie z plikiem konfiguracyjnym. Nie możemy wypełnić pliku konfiguracyjnego, ale nie będzie żadnych zmian związanych z opcjami konfiguracyjnymi.

Ustawiliśmy teraz ziemię i jesteśmy gotowi do rozpoczęcia tworzenia plików API do punktu końcowego Alpha Vantage.

Wejdźmy na interesujący bit!

Pobieranie danych giełdowych

W tej sekcji będziemy pobierać dane giełdowe z punktu końcowego Alpha Vantage. Oto plik 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); }); }); }) })()

Na potrzeby tego projektu będziemy sprawdzać ceny tylko dla tych akcji — NFLX (Netflix), MSFT (Microsoft), AMZN (Amazon), W (Wayfair), FB (Facebook).

Odwołaj się do tego pliku dla opcji konfiguracyjnych. Funkcja getStocksData getStocksData niewiele robi! Przechodzi przez te symbole i wysyła zapytanie do punktu końcowego Alpha Vantage ${host}query/?function=${timeSeriesFunction}&symbol=${symbol}&interval=${interval}&apikey=${key} aby uzyskać metryki dla tych akcji.

Funkcja insertStocksData umieszcza te punkty danych w bazie danych Postgres. Oto funkcja 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); }); };

To jest to! Pobraliśmy punkty danych zapasów z API Alpha Vantage i napisaliśmy funkcję, która umieszcza je w bazie danych Postgres w tabeli stock_data . Brakuje tylko jednego elementu, aby to wszystko działało! Musimy wypełnić poprawne wartości w pliku konfiguracyjnym. Te wartości uzyskamy po skonfigurowaniu silnika Hasura. Przejdźmy do tego od razu!

Proszę zapoznać się z katalogiem server , aby uzyskać pełny kod dotyczący pobierania punktów danych z punktu końcowego Alpha Vantage i wypełniania go w bazie danych Hasura Postgres.

Jeśli takie podejście do ustanawiania połączeń, opcji konfiguracyjnych i wstawiania danych za pomocą surowego zapytania wydaje się nieco trudne, nie martw się tym! Dowiemy się, jak to zrobić łatwy sposób z mutacją Graphql po ustawieniach silnika hasury!

Konfigurowanie silnika Hasura GraphQL

Skonfigurowanie silnika Hasura i uruchomienie ze schematami GraphQL, zapytaniami, mutacjami, subskrypcjami, wyzwalaczami zdarzeń i wieloma innymi funkcjami jest naprawdę proste!

Kliknij Wypróbuj Hasura i wprowadź nazwę projektu:

Tworzenie projektu Hasura
Tworzenie projektu Hasura. (duży podgląd)

Korzystam z bazy danych Postgres hostowanej na Heroku. Utwórz bazę danych na Heroku i połącz ją z tym projektem. Powinieneś być gotowy, aby doświadczyć mocy bogatej w zapytania konsoli Hasura.

Skopiuj adres URL Postgres DB, który otrzymasz po utworzeniu projektu. Będziemy musieli umieścić to w pliku konfiguracyjnym.

Kliknij Konsolę Uruchom i zostaniesz przekierowany do tego widoku:

Konsola Hasura
Konsola Hasura. (duży podgląd)

Zacznijmy budować schemat tabeli, którego potrzebowalibyśmy w tym projekcie.

Tworzenie schematu tabel w bazie danych Postgres

Przejdź do zakładki Dane i kliknij Dodaj tabelę! Zacznijmy tworzyć niektóre tabele:

tabela symbol

Ta tabela byłaby używana do przechowywania informacji o symbolach. Na razie zachowałem tutaj dwa pola — id i company . id pola to klucz podstawowy, a company jest typu varchar . Dodajmy kilka symboli z tej tabeli:

tabela symboli
tabela symbol . (duży podgląd)

tabela stock_data

Tabela tabeli stock_data id , symbol , time i metryki, takie jak high , low , open , close , volume . Skrypt NodeJS, który napisaliśmy wcześniej w tej sekcji, zostanie wykorzystany do wypełnienia tej konkretnej tabeli.

Oto jak wygląda tabela:

tabela stock_data
tabela stock_data . (duży podgląd)

Schludny! Przejdźmy do drugiej tabeli w schemacie bazy danych!

user_subscription

Tabela user_subscription przechowuje obiekt subskrypcji z identyfikatorem użytkownika. Ten obiekt subskrypcji jest używany do wysyłania powiadomień web-push do użytkowników. W dalszej części artykułu dowiemy się, jak wygenerować ten obiekt subskrypcji.

W tej tabeli znajdują się dwa pola — id jest kluczem podstawowym typu uuid , a pole subskrypcji jest typu jsonb .

tabela events

Jest to ważne i służy do przechowywania opcji zdarzeń powiadomienia. Gdy użytkownik zostanie optymowy dla aktualizacji cen określonych zapasów, przechowujemy tę informację o zdarzeniu w tej tabeli. Ta tabela zawiera te kolumny:

  • id : jest kluczem podstawowym z właściwością auto-increment.
  • symbol : to pole tekstowe.
  • user_id : jest uuid typu.
  • trigger_type : służy do przechowywania typu wyzwalacza zdarzenia — time/event .
  • trigger_value : służy do przechowywania wartości wyzwalacza. Na przykład, jeśli użytkownik zdecydował się na wyzwalacz zdarzenia oparty na cenie — chce aktualizacji, jeśli cena akcji osiągnie 1000, wówczas wartość trigger_value będzie wynosić 1000, a trigger_type będzie to event .

To są wszystkie tabele, których potrzebowalibyśmy do tego projektu. Musimy również utworzyć relacje między tymi tabelami, aby mieć płynny przepływ danych i połączenia. Zróbmy to!

Utworzenie relacji wśród tabel

Tabela events służy do wysyłania powiadomień web-push na podstawie wartości zdarzenia. Dlatego warto połączyć tę tabelę z tabelą user_subscription , aby móc wysyłać powiadomienia push dotyczące subskrypcji przechowywanych w tej tabeli.

 events.user_id → user_subscription.id

Tabela stock_data jest powiązana z tabelą symboli jako:

 stock_data.symbol → symbol.id

Musimy również skonstruować pewne relacje na tablicy symbol jako:

 stock_data.symbol → symbol.id events.symbol → symbol.id

Stworzyliśmy teraz wymagane tabele, a także ustaliliśmy relacje między nimi! Przełączmy się na karcie GRAPHIQL na konsoli, aby zobaczyć magię!

Hasura skonfigurował już zapytania GraphQL w oparciu o te tabele:

Zapytania/mutacje GraphQL na konsoli Hasura
Zapytania/mutacje GraphQL na konsoli Hasura. (duży podgląd)

Wykonywanie zapytań dotyczących tych tabel jest po prostu proste i można również zastosować dowolny z tych filtrów/właściwości ( distinct_on , limit , offset , order_by , where ), aby uzyskać żądane dane.

To wszystko wygląda dobrze, ale nadal nie podłączyliśmy naszego kodu serwera do konsoli Hasury. Dokończmy ten kawałek!

Podłączanie skryptu NodeJS do bazy danych Postgres

Proszę umieścić wymagane opcje w pliku config.js w katalogu server jako:

 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;

Proszę umieścić te opcje z ciągu bazy danych, który został wygenerowany podczas tworzenia bazy danych Postgres w Heroku.

apiHostOptions składa się z opcji związanych z interfejsami API, takich jak host , key , timeSeriesFunction i interval .

Otrzymasz pole graphqlURL w zakładce GRAPHIQL w konsoli Hasura.

Funkcja getConfig jest używana do zwracania żądanej wartości z obiektu konfiguracyjnego. Użyliśmy tego już w index.js w katalogu server .

Czas uruchomić serwer i zapełnić bazę danych. Dodałem jeden skrypt w package.json jako:

 "scripts": { "start": "nodemon index.js" }

Uruchom npm start na terminalu, a punkty danych tablicy symboli w index.js powinny zostać wypełnione w tabelach.

Refaktoryzacja surowego zapytania w skrypcie NodeJs do mutacji GraphQL

Teraz, gdy silnik Hasura jest skonfigurowany, zobaczmy, jak łatwo może zadzwonić do mutacji na tabeli stock_data .

Funkcja insertStocksData w queries.js używa surowego zapytania:

 const query = 'INSERT INTO stock_data (symbol, high, low, open, close, volume, time) VALUES ($1, $2, $3, $4, $5, $6, $7)';

Zrefaktoryzujmy to zapytanie i użyjmy mutacji obsługiwanej przez silnik Hasura. Oto zrefaktoryzowany queries.js w katalogu serwera:

 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 }

Uwaga: musimy dodać graphqlURL w pliku config.js .

Moduł apollo-fetch zwraca funkcję pobierania, której można użyć do zapytania/zmutowania daty w punkcie końcowym GraphQL. Wystarczająco łatwe, prawda?

Jedyną zmianą, jaką musimy zrobić w index.js , jest zwrócenie obiektu stocks w formacie wymaganym przez funkcję insertStocksData . Zapoznaj się z index2.js i queries2.js , aby uzyskać pełny kod z tym podejściem.

Teraz, gdy zakończyliśmy projekt po stronie danych, przejdźmy do front-endu i zbudujmy kilka interesujących komponentów!

Uwaga : Przy takim podejściu nie musimy zachowywać opcji konfiguracji bazy danych!

Front-end przy użyciu klienta React i Apollo

Projekt front-end znajduje się w tym samym repozytorium i jest tworzony przy użyciu pakietu create-react-app . Service Worker wygenerowany przy użyciu tego pakietu obsługuje buforowanie zasobów, ale nie pozwala na dodawanie kolejnych dostosowań do pliku Service Worker. Istnieje już kilka otwartych problemów, które wymagają dodania obsługi niestandardowych opcji Service Worker. Istnieją sposoby, aby uciec z tym problemem i dodać wsparcie dla niestandardowego pracownika serwisowego.

Zacznijmy od przyjrzenia się strukturze projektu front-endowego:

Katalog projektów
Katalog projektów. (duży podgląd)

Sprawdź katalog src ! Na razie nie martw się o pliki związane z Service Worker. W dalszej części tej sekcji dowiemy się więcej o tych plikach. Reszta struktury projektu wygląda na prostą. Folder components będzie miał komponenty (ładowacz, wykres); folder services zawiera niektóre funkcje/usługi pomocnicze używane do przekształcania obiektów w wymaganą strukturę; styles jak sama nazwa wskazuje zawiera pliki sass używane do stylizacji projektu; views jest głównym katalogiem i zawiera komponenty warstwy widoku.

Potrzebujemy tylko dwóch elementów widoku dla tego projektu - lista symboli i czasami symbolu. Szeregi czasowe zbudujemy za pomocą komponentu Chart z biblioteki highcharts. Zacznijmy dodawać kod w tych plikach, aby zbudować elementy na interfejsie użytkownika!

Instalowanie zależności

Oto lista zależności, których będziemy potrzebować:

  • apollo-boost
    Apollo Boost to sposób zero-konfiguracji, aby rozpocząć korzystanie z klienta Apollo. Jest dostarczany w pakiecie z domyślnymi opcjami konfiguracji.
  • reactstrap i bootstrap
    Komponenty są budowane przy użyciu tych dwóch pakietów.
  • graphql i graphql-type-json
    graphql jest wymaganą zależnością do używania apollo-boost a graphql-type-json służy do obsługi typu danych json używanego w schemacie GraphQL.
  • highcharts i highcharts-react-official
    Do budowy wykresu zostaną wykorzystane te dwa pakiety:

  • node-sass
    Dodaje się to do wspierania plików Sass do stylizacji.

  • uuid
    Ten pakiet służy do generowania silnych wartości losowych.

Wszystkie te zależności nabiorą sensu, gdy zaczniemy je wykorzystywać w projekcie. Przejdźmy do następnego kawałka!

Konfigurowanie klienta Apollo

Utwórz plik apolloClient.js w folderze src jako:

 import ApolloClient from 'apollo-boost'; const apolloClient = new ApolloClient({ uri: '<HASURA_CONSOLE_URL>' }); export default apolloClient;

Powyższy kod tworzy instancję ApolloClient i przyjmuje uri w opcjach konfiguracyjnych. URI to adres URL Twojej konsoli uri . Otrzymasz to pole uri na karcie GRAPHIQL w sekcji Punkt końcowy GraphQL .

Powyższy kod wygląda na prosty, ale zajmuje się główną częścią projektu! Łączy schemat GraphQL zbudowany na Hasurze z aktualnym projektem.

Musimy również przekazać ten obiekt klienta Apollo do ApolloProvider i owinąć element korzeniowy wewnątrz ApolloProvider . Umożliwi to wszystkim zagnieżdżonym komponentom w głównym komponencie używanie właściwości client i zapytań uruchamiających na tym obiekcie klienta.

Zmodyfikujmy plik index.js jako:

 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') );

Proszę zignorować insertSubscription kod związany z subskrypcją. Rozumiemy to szczegółowo później. Reszta kodu powinna być prosta do obejścia. Funkcja render przyjmuje składnik główny i elementId jako parametry. Zwróć uwagę, że client (instancja ApolloClient) jest przekazywany jako prop do ApolloProvider . Tutaj możesz sprawdzić pełny plik index.js .

Konfigurowanie niestandardowego pracownika usług

Pracownik serwisowy jest plikiem JavaScript, który ma możliwość przechwycenia żądań sieciowych. Jest używany do wysyłania zapytań do pamięci podręcznej, aby sprawdzić, czy żądany zasób jest już obecny w pamięci podręcznej, zamiast jeździć na serwer. Pracownicy usług są również wykorzystywane do wysyłania powiadomień Web-push do subskrybowanych urządzeń.

Aby wysłać powiadomienia Web-Push dla aktualizacji ceny akcji dla subskrybowanych użytkowników. Przygotujmy grunt i zbudujmy ten plik Service Worker!

Wycięty w index.js element związany z insertSubscription wykonuje pracę polegającą na zarejestrowaniu service workera i umieszczeniu obiektu subskrypcji w bazie danych za pomocą subscriptionMutation .

Wszystkie zapytania i mutacje używane w projekcie można znaleźć w pliku query.js.

serviceWorker.register(insertSubscription); wywołuje funkcję register zapisaną w pliku serviceWorker.js . Oto on:

 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(); }) } }

Powyższa funkcja najpierw sprawdza, czy serviceWorker jest obsługiwany przez przeglądarkę, a następnie rejestruje plik service worker hostowany pod adresem URL swUrl . Za chwilę sprawdzimy ten plik!

Funkcja getSubscription wykonuje pracę polegającą na pobraniu obiektu subskrypcji przy użyciu metody subscribe w obiekcie pushManager . Ten obiekt subskrypcji jest następnie przechowywany w user_subscription przed identyfikatorem użytkownika. Należy pamiętać, że identyfikator użytkownika jest generowany przy użyciu funkcji uuid . Sprawdźmy funkcję 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 })); }) } }) }

Możesz sprawdzić plik serviceWorker.js , aby uzyskać pełny kod!

Powiadomienie PopUp.
Wyskakujące powiadomienie. (duży podgląd)

Notification.requestPermission() wywołał to wyskakujące okienko, które prosi użytkownika o pozwolenie na wysyłanie powiadomień. Gdy użytkownik kliknie Zezwól, usługa wypychania generuje obiekt subskrypcji. Przechowujemy ten obiekt w localStorage jako:

Obiekt subskrypcji WebPush
Obiekt subskrypcji WebPush. (duży podgląd)

Pole endpoint w powyższym obiekcie służy do identyfikacji urządzenia i serwer wykorzystuje ten endpoint do wysyłania powiadomień web push do użytkownika.

Wykonaliśmy pracę inicjalizacji i rejestracji pracownika serwisu. Mamy również przedmiot subskrypcji użytkownika! Pracuje to dobrze z powodu public serviceWorker.js . Skonfigurujmy teraz pracownika serwisu, aby wszystko było gotowe!

Jest to trochę trudny temat, ale pozwólmy to zrobić dobrze! Jak wspomniano wcześniej, narzędzie create-react-app domyślnie nie obsługuje dostosowań dla pracownika serwisu. Wdrożenie pracownika obsługi klienta możemy zrealizować za pomocą modułu workbox-build .

Musimy również upewnić się, że domyślne zachowanie plików wstępnego buforowania jest nienaruszone. Zmodyfikujemy część, w której Service Worker jest kompilowany w projekcie. A, kompilacja robocza pomaga w osiągnięciu dokładnie! Schludne rzeczy! Zachowajmy to proste i wymieniajmy wszystko, co musimy zrobić, aby pracownikom pracownikom niestandardowym:

  • Obsługuj wstępnie buforowanie aktywów przy użyciu workboxBuild .
  • Utwórz szablon pracownik służby do aktywach buforowania.
  • Utwórz plik sw-precache-config.js , aby zapewnić niestandardowe opcje konfiguracji.
  • Dodaj skrypt procesu roboczego kompilacji w kroku kompilacji w package.json .

Nie martw się, jeśli to wszystko zabrzmi dezorientująco! Artykuł nie koncentruje się na wyjaśnianiu semantyki stojącej za każdym z tych punktów. Na razie musimy skupić się na części wdrożeniowej! Spróbuję pokryć rozumowanie za wykonanie całej pracy, aby uzyskać niestandardowy pracownik służby w innym artykule.

Stwórzmy dwa pliki sw-build.js i sw-custom.js w katalogu src . Zapoznaj się z linkami do tych plików i dodaj kod do swojego projektu.

Utwórzmy teraz plik sw-precache-config.js na poziomie korzenia i dodaj następujący kod w tym pliku:

 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' }] }

Modyfikujmy także plik package.json , aby zrobić miejsce do budowy niestandardowego pliku pracownika usługi:

Dodaj te instrukcje w sekcji scripts :

 "build-sw": "node ./src/sw-build.js", "clean-cra-sw": "rm -f build/precache-manifest.*.js && rm -f build/service-worker.js",

I zmodyfikuj skrypt build jako:

 "build": "react-scripts build && npm run build-sw && npm run clean-cra-sw",

Konfiguracja jest już zakończona! Teraz musimy dodać niestandardowy plik Service Worker w folderze 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)); })

Właśnie dodaliśmy jeden słuchacz push , aby słuchać push-powitych wysyłanych przez serwer. Funkcja showNotification służy do wyświetlania użytkownikowi powiadomień web push.

To jest to! Skończyliśmy z całą ciężką pracą w konfigurowaniu niestandardowego pracownika serwisowego do obsługi powiadomień Web Push. Zobaczymy te powiadomienia w akcji, gdy zbudujemy interfejsy użytkownika!

Zbliżamy się do zbudowania głównych fragmentów kodu. Zacznijmy od pierwszego widoku!

Widok listy symboli

Komponent App używany w poprzedniej sekcji wygląda tak:

 import React from 'react'; import SymbolList from './views/symbolList'; const App = () => { return <SymbolList />; }; export default App;

Jest to prosty komponent, który zwraca widok SymbolList , a SymbolList wykonuje wszystkie ciężkie zadania związane z wyświetlaniem symboli w zgrabnie powiązanym interfejsie użytkownika.

Spójrzmy na symbolList.js w folderze views :

Proszę odnieść się do pliku tutaj!

Komponent zwraca wyniki funkcji renderSymbols . A te dane są pobierane z bazy danych za pomocą haka useQuery jako:

 const { loading, error, data } = useQuery(symbolsQuery, {variables: { userId }});

symbolsQuery definiuje się jako:

 export const symbolsQuery = gql` query getSymbols($userId: uuid) { symbol { id company symbol_events(where: {user_id: {_eq: $userId}}) { id symbol trigger_type trigger_value user_id } stock_symbol_aggregate { aggregate { max { high volume } min { low volume } } } } } `;

userId identyfikator użytkownika i pobiera subskrybowane zdarzenia tego konkretnego użytkownika, aby wyświetlić prawidłowy stan ikony powiadomienia (ikona dzwonka, która jest wyświetlana wraz z tytułem). Zapytanie pobiera również maksymalne i minimalne wartości zapasów. Zwróć uwagę na użycie aggregate w powyższym zapytaniu. Zapytania agregacyjne Hasury wykonują pracę za kulisami, aby pobrać wartości zagregowane, takie jak count , sum , avg , max , min , itp.

W oparciu o odpowiedź z powyższego wywołania Graphql, tutaj lista kart, które są wyświetlane na front-end:

Karty akcji
Karty magazynowe. (duży podgląd)

Struktura HTML karty wygląda mniej więcej tak:

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

Do renderowania tych kart używamy komponentu Card ReactStrap. Komponent Popover służy do wyświetlania opcji opartych na subskrypcji:

Opcje powiadomień
Opcje powiadomień. (duży podgląd)

Gdy użytkownik kliknie ikonę bell dla danego towaru, może wyrazić zgodę na otrzymywanie powiadomień co godzinę lub gdy cena akcji osiągnie wprowadzoną wartość. Zobaczymy to w sekcji Wydarzenia / czas wyzwalacza.

Uwaga : przejdziemy do komponentu StockTimeseries w następnej sekcji!

Proszę odnieść się do symbolList.js w celu uzyskania pełnego kodu związanego z komponentem listy zapasów.

Widok serii czasowych akcji

Komponent StockTimeseries używa zapytań 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 } } `;

Powyższe zapytanie pobiera ostatnie 25 punktów danych wybranego magazynu. Na przykład, oto wykres dla wskaźnika otwarcia akcji Facebooka:

Ceny akcji Temeline.
Timeline cenach akcji. (duży podgląd)

Jest to prosty komponent, w którym przekazujemy w niektórych opcjach wykresów do komponentu [ HighchartsReact ]. Oto opcje wykresu:

 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') } ] }

Oś X pokazuje czas, a oś Y pokazuje wartość metryki w tym czasie. Funkcja getDataPoints służy do generowania serii punktów dla każdej z serii.

 const getDataPoints = (type) => { const values = []; data.stock_data.map((dataPoint) => { let value = dataPoint[type]; if (type === 'time') { value = new Date(dataPoint['time']).toLocaleString('en-US'); } values.push(value); }); return values; }

Prosty! W ten sposób generowany jest komponent Wykres! Proszę zapoznać się z plikami stockTimeseries.js i

Teraz powinieneś być gotowy z danymi i interfejsami użytkownika w części projektu. Przejdźmy teraz do ciekawej części - konfigurowanie wyzwalaczy zdarzeń / czasu w oparciu o wejście użytkownika.

Konfigurowanie wyzwalaczy zdarzeń/zaplanowanych

W tym rozdziale dowiemy się, jak skonfigurować wyzwalacze w konsoli Hasura oraz jak wysyłać powiadomienia web push do wybranych użytkowników. Zacznijmy!

Wyzwalacze zdarzeń na konsoli Hasura

Utwórzmy wydarzenie stock_value na tabeli stock_data i insert jako operacja wyzwalacza. Webhook będzie uruchamiany za każdym razem, gdy w tabeli stock_data się wstawka.

Konfiguracja wyzwalaczy zdarzeń
Konfiguracja wyzwalaczy zdarzeń. (duży podgląd)

Zamierzamy utworzyć projekt usterki dla adresu URL webhooka. Pozwólcie, że opiszę trochę webhooki, aby było łatwo zrozumieć:

WebHook są używane do wysyłania danych z jednej aplikacji do drugiego w przypadku wystąpienia konkretnego zdarzenia. Gdy zdarzenie zostanie wyzwolone, wykonany jest połączenie HTTP do adresu URL WebHook z danymi zdarzeń jako ładunek.

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!

Pobieranie subskrybowanych użytkowników

 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()); }

W powyższej funkcji handleStockValueTrigger najpierw pobieramy subskrybowanych użytkowników za pomocą funkcji getSubscribedUsers . Następnie do każdego z tych użytkowników wysyłamy powiadomienia web-push. Do wysłania powiadomienia służy funkcja sendWebpush . Za chwilę przyjrzymy się implementacji web-push.

Funkcja getSubscribedUsers wykorzystuje zapytanie:

 query getSubscribedUsers($symbol: String, $triggerValue: numeric) { events(where: {symbol: {_eq: $symbol}, trigger_type: {_eq: "event"}, trigger_value: {_gte: $triggerValue}}) { user_id user_subscription { subscription } } }

To zapytanie pobiera symbol giełdowy i wartość oraz pobiera szczegóły użytkownika, w tym user-id i user_subscription użytkownika, które spełniają te warunki:

  • symbol równy temu, który jest przekazywany w ładunku.
  • trigger_type jest równy event .
  • trigger_value jest większa lub równa wartości przekazanej do tej funkcji (w tym przypadku close ).

Gdy otrzymamy listę użytkowników, jedyne, co pozostaje, to wysyłanie do nich powiadomień web-push! Zróbmy to od razu!

Wysyłanie powiadomień Web-Push do subskrybowanych użytkowników

Aby wysyłać powiadomienia web-push, musimy najpierw uzyskać publiczny i prywatny klucz VAPID. Proszę przechowywać te klucze w pliku .env i ustawić te szczegóły w index.js jako:

 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)) }

Funkcja sendNotification służy do wysyłania web-push w punkcie końcowym subskrypcji podanym jako pierwszy parametr.

To wszystko jest wymagane do pomyślnego wysyłania powiadomień web-push do subskrybowanych użytkowników. Oto pełny kod zdefiniowany w 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"); });

Przetestujmy ten przepływ, subskrybując akcje z pewną wartością i ręcznie wstawiając tę ​​wartość do tabeli (do testowania)!

AMZN o wartości 2000 a następnie wstawiłem do tabeli punkt danych o tej wartości. Oto jak aplikacja powiadamiania o akcjach powiadomiła mnie zaraz po wstawieniu:

Wstawianie wiersza w tabeli stock_data do testowania
Wstawianie wiersza w tabeli stock_data do testowania. (duży podgląd)

Schludny! Możesz również sprawdzić dziennik wywołań zdarzeń tutaj:

Dziennik zdarzeń
Dziennik zdarzeń. (duży podgląd)

Webhook działa zgodnie z oczekiwaniami! Wszyscy jesteśmy przygotowani na wyzwalacze wydarzeń już teraz!

Zaplanowane/Cron Wyzwalacze

Możemy osiągnąć wyzwalacz czasowy do powiadamiania użytkowników subskrybenta co godzinę za pomocą wyzwalacza zdarzenia Cron jako:

Konfiguracja Cron/Scheduled Trigger
Konfiguracja Cron/Scheduled Trigger. (duży podgląd)

Możemy użyć tego samego adresu URL webhooka i obsłużyć subskrybowanych użytkowników na podstawie typu zdarzenia wyzwalającego jako stock_price_time_based_trigger . Implementacja jest podobna do wyzwalacza opartego na zdarzeniach.

Wniosek

W tym artykule stworzyliśmy aplikację do powiadamiania o cenach akcji. Dowiedzieliśmy się, jak pobierać ceny za pomocą interfejsów API Alpha Vantage i przechowywać punkty danych w bazie danych Postgres wspieranej przez Hasura. Dowiedzieliśmy się również, jak skonfigurować silnik Hasura GraphQL i tworzyć wyzwalacze oparte na zdarzeniach i zaplanowane. Zbudowaliśmy projekt usterki do wysyłania powiadomień web-push do subskrybowanych użytkowników.