React、Apollo GraphQL、Hasuraを使用した株価通知アプリの構築
公開: 2022-03-10選択したイベントが発生したときに通知を受け取るという概念は、その特定の発生を自分で見つけるためにデータの連続ストリームに接着されるよりも一般的になっています。 人々は、そのイベントが発生するのを待つために画面に引っ掛かるよりも、好みのイベントが発生したときに関連する電子メール/メッセージを受け取ることを好みます。 イベントベースの用語は、ソフトウェアの世界でも非常に一般的です。
あなたの携帯電話であなたのお気に入りの株の価格の更新を得ることができたら、それはどれほど素晴らしいでしょうか?
この記事では、React、Apollo GraphQL、およびHasuraGraphQLエンジンを使用して株価通知アプリケーションを構築します。 create-react-app
ボイラープレートコードからプロジェクトを開始し、すべてをゼロから構築します。 Hasuraコンソールでデータベーステーブルとイベントを設定する方法を学習します。 また、Webプッシュ通知を使用して株価の更新を取得するためにHasuraのイベントを接続する方法についても学習します。
これが私たちが構築するものの概要です:
さあ行こう!
このプロジェクトの概要
株式データ(高、低、オープン、クローズ、ボリュームなどのメトリックを含む)は、HasuraがサポートするPostgresデータベースに保存されます。 ユーザーは、ある値に基づいて特定の株式を購読するか、1時間ごとに通知を受け取ることを選択できます。 サブスクリプション基準が満たされると、ユーザーはWebプッシュ通知を受け取ります。
これは多くのもののように見え、これらの部分をどのように構築するかについて、明らかにいくつかの未解決の質問があります。
このプロジェクトを4つのステップでどのように達成するかについての計画は次のとおりです。
- NodeJsスクリプトを使用して株式データを取得する
まず、株式APIのプロバイダーの1つであるAlphaVantageから単純なNodeJsスクリプトを使用して株式データを取得します。 このスクリプトは、5分間隔で特定の株式のデータをフェッチします。 APIの応答には、高、低、オープン、クローズ、およびボリュームが含まれます。 このデータは、Hasuraバックエンドと統合されているPostgresデータベースに挿入されます。 - HasuraGraphQLエンジンのセットアップ
次に、データポイントを記録するためにPostgresデータベースにいくつかのテーブルを設定します。 Hasuraは、これらのテーブルのGraphQLスキーマ、クエリ、およびミューテーションを自動的に生成します。 - ReactとApolloクライアントを使用したフロントエンド
次のステップは、ApolloクライアントとApolloプロバイダー(Hasuraが提供するGraphQLエンドポイント)を使用してGraphQLレイヤーを統合することです。 データポイントは、フロントエンドにグラフとして表示されます。 また、サブスクリプションオプションを構築し、GraphQLレイヤーで対応するミューテーションを起動します。 - イベント/スケジュールされたトリガーの設定
Hasuraは、トリガーの周りに優れたツールを提供します。 株式データテーブルにイベントとスケジュールされたトリガーを追加します。 これらのトリガーは、株価が特定の値に達したときにユーザーが通知を受け取ることに関心がある場合に設定されます(イベントトリガー)。 ユーザーは、特定の在庫の通知を1時間ごとに受け取ることを選択することもできます(スケジュールされたトリガー)。
計画の準備ができたので、それを実行に移しましょう!
これがこのプロジェクトのGitHubリポジトリです。 以下のコードのどこかで迷子になった場合は、このリポジトリを参照して、スピードを取り戻してください。
NodeJsスクリプトを使用した株式データの取得
これは、思ったほど複雑ではありません。 Alpha Vantageエンドポイントを使用してデータをフェッチする関数を作成する必要があり、このフェッチ呼び出しは5分間隔で実行される必要があります(ご想像のとおり、この関数呼び出しをsetInterval
に配置する必要があります)。
Alpha Vantageが何であるかまだ疑問に思っていて、コーディング部分に飛び込む前にこれを頭から取り出したい場合は、次のようになります。
Alpha Vantage Inc.は、株式、外国為替(FX)、およびデジタル/暗号通貨に関するリアルタイムおよび履歴データ用の無料APIの大手プロバイダーです。
このエンドポイントを使用して、特定の株式の必要なメトリックを取得します。 このAPIは、パラメーターの1つとしてAPIキーを想定しています。 ここから無料のAPIキーを取得できます。 これで、興味深い部分に取り掛かることができました。コードを書き始めましょう。
依存関係のインストール
stocks-app
ディレクトリを作成し、その中にserver
ディレクトリを作成します。 npm init
を使用してノードプロジェクトとして初期化し、次の依存関係をインストールします。
npm i isomorphic-fetch pg nodemon --save
これらは、株価を取得してPostgresデータベースに保存するこのスクリプトを作成するために必要な3つの依存関係だけです。
これらの依存関係について簡単に説明します。
-
isomorphic-fetch
これにより、クライアントとサーバーの両方で(同じ形式で)同形のfetch
を簡単に使用できます。 -
pg
これは、NodeJ用のノンブロッキングPostgreSQLクライアントです。 -
nodemon
ディレクトリ内のファイルが変更されると、サーバーが自動的に再起動します。
構成のセットアップ
ルートレベルでconfig.js
ファイルを追加します。 今のところ、そのファイルに以下のコードスニペットを追加します。
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
は、Postgresの構成に関連しています。 Hasuraエンジンパーツのセットアップ中に、これを編集するために戻ってきます!
データベースをクエリするためのPostgres接続プールの初期化
connection pool
はコンピュータサイエンスの一般的な用語であり、データベースを扱うときにこの用語をよく耳にします。
データベース内のデータをクエリするときは、最初にデータベースへの接続を確立する必要があります。 この接続はデータベースのクレデンシャルを受け取り、データベース内の任意のテーブルをクエリするためのフックを提供します。
注:データベース接続の確立にはコストがかかり、大量のリソースも浪費します。 接続プールはデータベース接続をキャッシュし、後続のクエリでそれらを再利用します。 開いている接続がすべて使用されている場合は、新しい接続が確立され、プールに追加されます。
接続プールとは何か、そしてそれが何のために使用されるかが明確になったので、このアプリケーションのpg
接続プールのインスタンスを作成することから始めましょう。
ルートレベルでpool.js
ファイルを追加し、次のようにプールインスタンスを作成します。
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;
上記のコード行は、構成ファイルで設定された構成オプションを使用してPool
のインスタンスを作成します。 構成ファイルはまだ完成していませんが、構成オプションに関連する変更はありません。
これで準備が整い、AlphaVantageエンドポイントへのAPI呼び出しを開始する準備が整いました。
面白いことに取り掛かりましょう!
株式データの取得
このセクションでは、AlphaVantageエンドポイントから株式データを取得します。 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); }); }); }) })()
このプロジェクトの目的のために、これらの株式(NFLX(Netflix)、MSFT(Microsoft)、AMZN(Amazon)、W(Wayfair)、FB(Facebook))の価格のみを照会します。
構成オプションについては、このファイルを参照してください。 IIFE getStocksData
関数はあまり機能していません! これらのシンボルをループし、AlphaVantageエンドポイント${host}query/?function=${timeSeriesFunction}&symbol=${symbol}&interval=${interval}&apikey=${key}
をクエリして、これらの株式のメトリックを取得します。
insertStocksData
関数は、これらのデータポイントをPostgresデータベースに配置します。 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); }); };
これだよ! Alpha Vantage APIから株式のデータポイントをフェッチし、これらをstock_data
テーブルのPostgresデータベースに配置する関数を作成しました。 このすべてを機能させるための欠落している部分は1つだけです! 設定ファイルに正しい値を入力する必要があります。 これらの値は、Hasuraエンジンを設定した後に取得します。 すぐに行きましょう!
Alpha Vantageエンドポイントからデータポイントをフェッチし、それをHasura Postgresデータベースに取り込むための完全なコードについては、 server
ディレクトリを参照してください。
接続、構成オプションを設定し、生のクエリを使用してデータを挿入するこのアプローチが少し難しいように思われる場合でも、心配する必要はありません。 Hasuraエンジンがセットアップされたら、GraphQLミューテーションを使用してこれらすべてを簡単に行う方法を学習します。
HasuraGraphQLエンジンのセットアップ
Hasuraエンジンをセットアップし、GraphQLスキーマ、クエリ、ミューテーション、サブスクリプション、イベントトリガーなどを使用して、起動して実行するのは非常に簡単です。
[ハスラを試す]をクリックして、プロジェクト名を入力します。
HerokuでホストされているPostgresデータベースを使用しています。 Herokuにデータベースを作成し、それをこのプロジェクトにリンクします。 これで、クエリが豊富なHasuraコンソールのパワーを体験できるようになります。
プロジェクトの作成後に取得するPostgresDBURLをコピーしてください。 これを設定ファイルに入れる必要があります。
[コンソールの起動]をクリックすると、次のビューにリダイレクトされます。
このプロジェクトに必要なテーブルスキーマの構築を始めましょう。
Postgresデータベースでのテーブルスキーマの作成
[データ]タブに移動し、[テーブルの追加]をクリックしてください。 いくつかのテーブルの作成を始めましょう。
symbol
テーブル
このテーブルは、シンボルの情報を格納するために使用されます。 今のところ、ここにはid
とcompany
の2つのフィールドを保持しています。 フィールドid
は主キーであり、 company
のタイプはvarchar
です。 この表にいくつかの記号を追加してみましょう。
stock_data
テーブル
stock_data
テーブルには、 id
、 symbol
、 time
、およびhigh
、 low
、 open
、 close
、 volume
などのメトリックが格納されます。 このセクションの前半で作成したNodeJsスクリプトは、この特定のテーブルにデータを入力するために使用されます。
テーブルは次のようになります。
きちんとした! データベーススキーマの他のテーブルに移動しましょう!
user_subscription
テーブル
user_subscription
テーブルは、ユーザーIDに対するサブスクリプションオブジェクトを格納します。 このサブスクリプションオブジェクトは、ユーザーにWebプッシュ通知を送信するために使用されます。 このサブスクリプションオブジェクトを生成する方法については、この記事の後半で学習します。
このテーブルには2つのフィールドがありますid
はタイプuuid
の主キーであり、サブスクリプションフィールドはタイプjsonb
です。
events
テーブル
これは重要なものであり、通知イベントオプションを保存するために使用されます。 ユーザーが特定の株式の価格更新をオプトインすると、そのイベント情報がこのテーブルに保存されます。 このテーブルには、次の列が含まれています。
-
id
:はauto-incrementプロパティを持つ主キーです。 -
symbol
:はテキストフィールドです。 -
user_id
:タイプはuuid
です。 -
trigger_type
:イベントトリガータイプ—time/event
を保存するために使用されます。 -
trigger_value
:トリガー値を格納するために使用されます。 たとえば、ユーザーが価格ベースのイベントトリガーをオプトインしている場合、株価が1000に達した場合に更新が必要な場合、trigger_value
は1000になり、trigger_type
はevent
になります。
これらはすべて、このプロジェクトに必要なテーブルです。 また、スムーズなデータフローと接続を実現するには、これらのテーブル間の関係を設定する必要があります。 それをしましょう!
テーブル間の関係の設定
events
テーブルは、イベント値に基づいてWebプッシュ通知を送信するために使用されます。 したがって、このテーブルをuser_subscription
テーブルに接続して、このテーブルに格納されているサブスクリプションに関するプッシュ通知を送信できるようにすることは理にかなっています。
events.user_id → user_subscription.id
stock_data
テーブルは、シンボルテーブルに次のように関連付けられています。
stock_data.symbol → symbol.id
また、 symbol
テーブル上に次のようにいくつかの関係を構築する必要があります。
stock_data.symbol → symbol.id events.symbol → symbol.id
これで、必要なテーブルが作成され、それらの間の関係も確立されました。 コンソールのGRAPHIQL
タブに切り替えて、魔法を見てみましょう。
Hasuraは、これらのテーブルに基づいてGraphQLクエリをすでに設定しています。
これらのテーブルでクエリを実行するのは非常に簡単です。また、これらのフィルター/プロパティ( distinct_on
、 limit
、 offset
、 order_by
、 where
)のいずれかを適用して、目的のデータを取得することもできます。
これはすべて良さそうですが、サーバー側のコードをHasuraコンソールに接続していません。 そのビットを完成させましょう!
NodeJsスクリプトをPostgresデータベースに接続する
server
ディレクトリのconfig.js
ファイルに必要なオプションを次のように入力してください。
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;
これらのオプションは、HerokuでPostgresデータベースを作成したときに生成されたデータベース文字列から入力してください。
apiHostOptions
は、 host
、 key
、 timeSeriesFunction
、 interval
などのAPI関連のオプションで構成されています。
Hasuraコンソールの[ GRAPHIQL ]タブにgraphqlURL
フィールドが表示されます。
getConfig
関数は、構成オブジェクトから要求された値を返すために使用されます。 これは、 server
ディレクトリのindex.js
ですでに使用されています。
サーバーを実行し、データベースにいくつかのデータを入力するときが来ました。 package.json
に次のように1つのスクリプトを追加しました:
"scripts": { "start": "nodemon index.js" }
ターミナルでnpm start
を実行すると、 index.js
のシンボル配列のデータポイントがテーブルに入力されます。
NodeJsスクリプトの生のクエリをGraphQLミューテーションにリファクタリングする
Hasuraエンジンがセットアップされたので、 stock_data
テーブルでミューテーションを呼び出すのがいかに簡単であるかを見てみましょう。
queries.js
の関数insertStocksData
は、生のクエリを使用します。
const query = 'INSERT INTO stock_data (symbol, high, low, open, close, volume, time) VALUES ($1, $2, $3, $4, $5, $6, $7)';
このクエリをリファクタリングして、Hasuraエンジンを利用したミューテーションを使用してみましょう。 サーバーディレクトリにあるリファクタリングされたqueries.js
は次のとおりです。
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 }
注意: config.js
ファイルにgraphqlURL
を追加する必要があります。
apollo-fetch
モジュールは、GraphQLエンドポイントの日付を照会/変更するために使用できるフェッチ関数を返します。 簡単ですよね?
index.js
で行う必要がある唯一の変更は、 insertStocksData
関数で必要とされる形式でstocksオブジェクトを返すことです。 このアプローチの完全なコードについては、 index2.js
とqueries2.js
を確認してください。
プロジェクトのデータ側が完了したので、フロントエンドビットに移動して、いくつかの興味深いコンポーネントを構築しましょう。
注:このアプローチでは、データベース構成オプションを保持する必要はありません。
ReactとApolloクライアントを使用したフロントエンド
フロントエンドプロジェクトは同じリポジトリにあり、 create-react-app
パッケージを使用して作成されます。 このパッケージを使用して生成されたServiceWorkerは、アセットのキャッシュをサポートしますが、ServiceWorkerファイルにさらにカスタマイズを追加することはできません。 カスタムサービスワーカーオプションのサポートを追加するには、すでにいくつかの未解決の問題があります。 この問題を回避し、カスタムサービスワーカーのサポートを追加する方法があります。
まず、フロントエンドプロジェクトの構造を見てみましょう。
src
ディレクトリを確認してください! 今のところ、ServiceWorker関連のファイルについて心配する必要はありません。 これらのファイルについては、このセクションの後半で詳しく説明します。 プロジェクト構造の残りの部分は単純に見えます。 components
フォルダには、コンポーネント(ローダー、チャート)が含まれます。 services
フォルダーには、必要な構造でオブジェクトを変換するために使用されるヘルパー関数/サービスの一部が含まれています。 名前が示すように、 styles
には、プロジェクトのスタイリングに使用されるsassファイルが含まれています。 views
はメインディレクトリであり、ビューレイヤーコンポーネントが含まれています。
このプロジェクトに必要なビューコンポーネントは、シンボルリストとシンボル時系列の2つだけです。 highchartsライブラリのChartコンポーネントを使用して時系列を作成します。 これらのファイルにコードを追加して、フロントエンドでピースを構築してみましょう。
依存関係のインストール
必要な依存関係のリストは次のとおりです。
-
apollo-boost
Apollo Boostは、ApolloClientの使用を開始するためのゼロ構成の方法です。 デフォルトの構成オプションがバンドルされています。 -
reactstrap
とbootstrap
コンポーネントは、これら2つのパッケージを使用して構築されています。 -
graphql
およびgraphql-type-json
graphql
はapollo-boost
を使用するために必要な依存関係であり、graphql-type-json
はGraphQLスキーマで使用されているjson
データ型をサポートするために使用されます。 highcharts
およびhighcharts-react-official
そして、これら2つのパッケージは、チャートの作成に使用されます。node-sass
これは、スタイリング用のsassファイルをサポートするために追加されました。uuid
このパッケージは、強力なランダム値を生成するために使用されます。
これらの依存関係はすべて、プロジェクトで使用を開始すると意味があります。 次のビットに取り掛かりましょう!
Apolloクライアントのセットアップ
src
フォルダー内にapolloClient.js
を次のように作成します。
import ApolloClient from 'apollo-boost'; const apolloClient = new ApolloClient({ uri: '<HASURA_CONSOLE_URL>' }); export default apolloClient;
上記のコードはApolloClientをインスタンス化し、構成オプションにuri
を取り込みます。 uri
は、HasuraコンソールのURLです。 このuri
フィールドは、 GraphQLEndpointセクションのGRAPHIQL
タブに表示されます。
上記のコードは単純に見えますが、プロジェクトの主要部分を処理します。 Hasura上に構築されたGraphQLスキーマを現在のプロジェクトに接続します。
また、このapolloクライアントオブジェクトをApolloProvider
に渡し、ルートコンポーネントをApolloProvider
内にラップする必要があります。 これにより、メインコンポーネント内のネストされたすべてのコンポーネントが、 client
プロップを使用してこのクライアントオブジェクトに対してクエリを実行できるようになります。
index.js
ファイルを次のように変更してみましょう。
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') );
insertSubscription
関連のコードは無視してください。 これについては後で詳しく説明します。 残りのコードは簡単に回避できるはずです。 render
関数は、ルートコンポーネントとelementIdをパラメーターとして受け取ります。 client
(ApolloClientインスタンス)が小道具としてApolloProvider
に渡されていることに注意してください。 完全なindex.js
ファイルはここで確認できます。
カスタムサービスワーカーの設定
Service Workerは、ネットワークリクエストをインターセプトする機能を持つJavaScriptファイルです。 これは、サーバーにアクセスする代わりに、要求されたアセットがすでにキャッシュに存在するかどうかを確認するためにキャッシュにクエリを実行するために使用されます。 サービスワーカーは、サブスクライブされたデバイスにWebプッシュ通知を送信するためにも使用されます。
購読しているユーザーに株価の更新に関するWebプッシュ通知を送信する必要があります。 土台を築き、このServiceWorkerファイルを作成しましょう。
index.js
ファイルに挿入されたinsertSubscription
関連は、subscriptionMutationを使用して、Service Workerを登録し、 subscriptionMutation
ションオブジェクトをデータベースに配置する作業を行っています。
プロジェクトで使用されているすべてのクエリとミューテーションについては、querys.jsを参照してください。
serviceWorker.register(insertSubscription);
serviceWorker.js
ファイルに書き込まれたregister
関数を呼び出します。 ここにあります:
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(); }) } }
上記の関数は、最初にserviceWorker
がブラウザーでサポートされているかどうかを確認してから、URLswUrlでホストされているswUrl
ファイルを登録します。 このファイルをすぐにチェックします!
getSubscription
関数は、 pushManager
オブジェクトのsubscribe
メソッドを使用してサブスクリプションオブジェクトを取得する作業を行います。 このサブスクリプションオブジェクトは、userIdに対してuser_subscription
テーブルに保存されます。 userIdはuuid
関数を使用して生成されていることに注意してください。 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 })); }) } }) }
完全なコードについては、 serviceWorker.js
ファイルを確認できます。
Notification.requestPermission()
は、通知を送信するための許可をユーザーに求めるこのポップアップを呼び出しました。 ユーザーが[許可]をクリックすると、プッシュサービスによってサブスクリプションオブジェクトが生成されます。 そのオブジェクトを次のようにlocalStorageに保存しています。
上記のオブジェクトのフィールドendpoint
はデバイスを識別するために使用され、サーバーはこのエンドポイントを使用してユーザーにWebプッシュ通知を送信します。
サービスワーカーの初期化と登録の作業を行いました。 ユーザーのサブスクリプションオブジェクトもあります! public
フォルダーにserviceWorker.js
ファイルが存在するため、これはすべて正常に機能しています。 それでは、準備を整えるためにサービスワーカーを設定しましょう。
これは少し難しいトピックですが、正しく理解しましょう。 前述のように、 create-react-app
ユーティリティは、ServiceWorkerのデフォルトのカスタマイズをサポートしていません。 workbox-build
モジュールを使用してカスタマーサービスワーカーの実装を実現できます。
また、ファイルの事前キャッシュのデフォルトの動作が損なわれていないことを確認する必要があります。 プロジェクトでServiceWorkerがビルドされる部分を変更します。 そして、workbox-buildはまさにそれを達成するのに役立ちます! きちんとしたもの! シンプルに保ち、カスタムサービスワーカーを機能させるために必要なことをすべてリストアップしましょう。
-
workboxBuild
を使用してアセットのプリキャッシュを処理します。 - アセットをキャッシュするためのServiceWorkerテンプレートを作成します。
-
sw-precache-config.js
ファイルを作成して、カスタム構成オプションを提供します。 -
package.json
のビルドステップにビルドサービスワーカースクリプトを追加します。
これらすべてが混乱しているように聞こえても心配しないでください! この記事では、これらの各ポイントの背後にあるセマンティクスの説明に焦点を当てていません。 今のところ、実装部分に焦点を当てる必要があります。 別の記事で、カスタムサービスワーカーを作成するためのすべての作業を行う理由について説明します。
src
ディレクトリにsw sw-build.js
とsw-custom.js
custom.jsの2つのファイルを作成しましょう。 これらのファイルへのリンクを参照して、プロジェクトにコードを追加してください。
次に、ルートレベルでsw-precache-config.js
ファイルを作成し、そのファイルに次のコードを追加しましょう。
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' }] }
また、 package.json
ファイルを変更して、カスタムServiceWorkerファイルを作成するためのスペースを確保しましょう。
scripts
セクションに次のステートメントを追加します。
"build-sw": "node ./src/sw-build.js", "clean-cra-sw": "rm -f build/precache-manifest.*.js && rm -f build/service-worker.js",
そして、 build
スクリプトを次のように変更します。
"build": "react-scripts build && npm run build-sw && npm run clean-cra-sw",
いよいよセットアップが完了しました! 次に、 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)); })
サーバーから送信されるプッシュ通知をリッスンするpush
リスナーを1つ追加しました。 関数showNotification
は、ユーザーにWebプッシュ通知を表示するために使用されます。
これだよ! Webプッシュ通知を処理するカスタムサービスワーカーを設定するという大変な作業はすべて完了しました。 ユーザーインターフェースを構築すると、これらの通知が実際に動作するのがわかります。
メインのコード部分の構築に近づいています。 最初のビューから始めましょう!
シンボル一覧表示
前のセクションで使用されているApp
コンポーネントは次のようになります。
import React from 'react'; import SymbolList from './views/symbolList'; const App = () => { return <SymbolList />; }; export default App;
これはSymbolList
ビューを返す単純なコンポーネントであり、 SymbolList
は、きちんと結び付けられたユーザーインターフェイスでシンボルを表示するという面倒な作業をすべて行います。
views
フォルダー内のsymbolList.js
を見てみましょう。
こちらのファイルを参照してください!
コンポーネントは、 renderSymbols
関数の結果を返します。 そして、このデータは、 useQuery
フックを使用してデータベースからフェッチされています。
const { loading, error, data } = useQuery(symbolsQuery, {variables: { userId }});
symbolsQuery
は次のように定義されます。
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
を取り込み、その特定のユーザーのサブスクライブされたイベントをフェッチして、通知アイコン(タイトルと一緒に表示されているベルアイコン)の正しい状態を表示します。 クエリは、株式の最大値と最小値もフェッチします。 上記のクエリでaggregate
が使用されていることに注意してください。 HasuraのAggregationクエリは、 count
、 sum
、 avg
、 max
、 min
などの集計値を取得するためにバックグラウンドで作業を行います。
上記のGraphQL呼び出しからの応答に基づいて、フロントエンドに表示されるカードのリストは次のとおりです。
カードのHTML構造は次のようになります。
<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>
ReactStrapのCard
コンポーネントを使用して、これらのカードをレンダリングしています。 Popover
コンポーネントは、サブスクリプションベースのオプションを表示するために使用されます。
ユーザーが特定の株のbell
アイコンをクリックすると、1時間ごとに、または株の価格が入力された値に達したときに通知を受け取るようにオプトインできます。 これは、[イベント/タイムトリガー]セクションで実際に動作することを確認できます。
注:次のセクションでStockTimeseries
コンポーネントについて説明します。
株式リストコンポーネントに関連する完全なコードについては、 symbolList.js
を参照してください。
株式時系列ビュー
StockTimeseries
コンポーネントは、クエリ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 } } `;
上記のクエリは、選択した株式の最近の25データポイントをフェッチします。 たとえば、Facebookの株式オープンメトリックのチャートは次のとおりです。
これは、[ HighchartsReact
]コンポーネントにいくつかのチャートオプションを渡す簡単なコンポーネントです。 チャートオプションは次のとおりです。
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') } ] }
X軸は時間を示し、Y軸はその時点のメトリック値を示します。 関数getDataPoints
は、シリーズごとに一連のポイントを生成するために使用されます。
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; }
単純! これがチャートコンポーネントの生成方法です。 ストック時系列の完全なコードについては、Chart.jsファイルとstockTimeseries.js
ファイルを参照してください。
これで、プロジェクトのデータとユーザーインターフェイスの部分の準備が整いました。 次に、興味深い部分に移りましょう。ユーザーの入力に基づいてイベント/時間トリガーを設定します。
イベント/スケジュールされたトリガーの設定
このセクションでは、Hasuraコンソールでトリガーを設定する方法と、選択したユーザーにWebプッシュ通知を送信する方法を学習します。 始めましょう!
Hasuraコンソールでイベントがトリガーされる
テーブルstock_data
にイベントトリガーstock_value
を作成し、トリガー操作としてinsert
してみましょう。 stock_data
テーブルに挿入があるたびに、Webhookが実行されます。
WebhookURLのグリッチプロジェクトを作成します。 わかりやすくするために、Webhookについて少し説明します。
Webhookは、特定のイベントの発生時に1つのアプリケーションから別のアプリケーションにデータを送信するために使用されます。 イベントがトリガーされると、ペイロードとしてイベントデータを使用してWebhookURLに対してHTTPPOST呼び出しが行われます。
この場合、 stock_data
テーブルに挿入操作があると、設定されたWebhook URLに対してHTTP post呼び出しが行われます(グリッチプロジェクトのpost呼び出し)。
Webプッシュ通知を送信するためのグリッチプロジェクト
上記のイベントトリガーインターフェイスに配置するWebhookURLを取得する必要があります。 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!
依存関係のインストール
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!
購読ユーザーの取得
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()); }
上記のhandleStockValueTrigger
関数では、最初にgetSubscribedUsers
関数を使用してサブスクライブされたユーザーをフェッチしています。 次に、これらの各ユーザーにWebプッシュ通知を送信します。 関数sendWebpush
は、通知の送信に使用されます。 Webプッシュの実装については後で説明します。
関数getSubscribedUsers
は、次のクエリを使用します。
query getSubscribedUsers($symbol: String, $triggerValue: numeric) { events(where: {symbol: {_eq: $symbol}, trigger_type: {_eq: "event"}, trigger_value: {_gte: $triggerValue}}) { user_id user_subscription { subscription } } }
このクエリは、銘柄記号と値を受け取り、次の条件に一致するuser-id
やuser_subscription
などのユーザーの詳細を取得します。
- ペイロードで渡される
symbol
と等しいシンボル。 -
trigger_type
はevent
と同じです。 -
trigger_value
は、この関数に渡される値以上です(この場合はclose
)。
ユーザーのリストを取得したら、残っているのはWebプッシュ通知をユーザーに送信することだけです。 すぐにやろう!
購読しているユーザーへのWebプッシュ通知の送信
まず、Webプッシュ通知を送信するために公開および秘密のVAPIDキーを取得する必要があります。 これらのキーを.env
ファイルに保存し、これらの詳細をindex.js
で次のように設定してください。
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)) }
sendNotification
関数は、最初のパラメーターとして提供されたサブスクリプションエンドポイントでWebプッシュを送信するために使用されます。
サブスクライブされたユーザーにWebプッシュ通知を正常に送信するには、これですべてです。 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"); });
ある値で株式を購読し、その値をテーブルに手動で挿入することによって、このフローをテストしてみましょう(テスト用)!
値が2000
のAMZN
をサブスクライブしてから、この値のデータポイントをテーブルに挿入しました。 挿入直後に株式通知アプリが通知した方法は次のとおりです。
きちんとした! ここでイベント呼び出しログを確認することもできます。
Webhookは期待どおりに作業を行っています! これで、イベントトリガーの準備が整いました。
スケジュールされた/ cronトリガー
次のようにCronイベントトリガーを使用して、サブスクライバーユーザーに1時間ごとに通知する時間ベースのトリガーを実現できます。
同じWebhookURLを使用し、トリガーイベントタイプに基づいてサブスクライブされたユーザーをstock_price_time_based_trigger
として処理できます。 実装は、イベントベースのトリガーに似ています。
結論
この記事では、株価通知アプリケーションを作成しました。 Alpha Vantage APIを使用して価格を取得し、HasuraがサポートするPostgresデータベースにデータポイントを保存する方法を学びました。 また、Hasura GraphQLエンジンをセットアップし、イベントベースのスケジュールされたトリガーを作成する方法も学びました。 サブスクライブしたユーザーにWebプッシュ通知を送信するためのグリッチプロジェクトを構築しました。