React、Apollo GraphQL、Hasuraを使用した株価通知アプリの構築

公開: 2022-03-10
クイックサマリー↬この記事では、イベントベースのアプリケーションを構築し、特定のイベントがトリガーされたときにWebプッシュ通知を送信する方法を学習します。 Hasura GraphQLエンジンでデータベーステーブル、イベント、スケジュールされたトリガーを設定し、GraphQLエンドポイントをフロントエンドアプリケーションに接続して、ユーザーの株価設定を記録します。

選択したイベントが発生したときに通知を受け取るという概念は、その特定の発生を自分で見つけるためにデータの連続ストリームに接着されるよりも一般的になっています。 人々は、そのイベントが発生するのを待つために画面に引っ掛かるよりも、好みのイベントが発生したときに関連する電子メール/メッセージを受け取ることを好みます。 イベントベースの用語は、ソフトウェアの世界でも非常に一般的です。

あなたの携帯電話であなたのお気に入りの株の価格の更新を得ることができたら、それはどれほど素晴らしいでしょうか?

この記事では、React、Apollo GraphQL、およびHasuraGraphQLエンジンを使用して株価通知アプリケーションを構築します。 create-react-appボイラープレートコードからプロジェクトを開始し、すべてをゼロから構築します。 Hasuraコンソールでデータベーステーブルとイベントを設定する方法を学習します。 また、Webプッシュ通知を使用して株価の更新を取得するためにHasuraのイベントを接続する方法についても学習します。

これが私たちが構築するものの概要です:

株価通知アプリケーションの概要
株価通知アプリケーション

さあ行こう!

ジャンプした後もっと! 以下を読み続けてください↓

このプロジェクトの概要

株式データ(オープンクローズボリュームなどのメトリックを含む)は、HasuraがサポートするPostgresデータベースに保存されます。 ユーザーは、ある値に基づいて特定の株式を購読するか、1時間ごとに通知を受け取ることを選択できます。 サブスクリプション基準が満たされると、ユーザーはWebプッシュ通知を受け取ります。

これは多くのもののように見え、これらの部分をどのように構築するかについて、明らかにいくつかの未解決の質問があります。

このプロジェクトを4つのステップでどのように達成するかについての計画は次のとおりです。

  1. NodeJsスクリプトを使用して株式データを取得する
    まず、株式APIのプロバイダーの1つであるAlphaVantageから単純なNodeJsスクリプトを使用して株式データを取得します。 このスクリプトは、5分間隔で特定の株式のデータをフェッチします。 APIの応答には、オープンクローズ、およびボリュームが含まれます。 このデータは、Hasuraバックエンドと統合されているPostgresデータベースに挿入されます。
  2. HasuraGraphQLエンジンのセットアップ
    次に、データポイントを記録するためにPostgresデータベースにいくつかのテーブルを設定します。 Hasuraは、これらのテーブルのGraphQLスキーマ、クエリ、およびミューテーションを自動的に生成します。
  3. ReactとApolloクライアントを使用したフロントエンド
    次のステップは、ApolloクライアントとApolloプロバイダー(Hasuraが提供するGraphQLエンドポイント)を使用してGraphQLレイヤーを統合することです。 データポイントは、フロントエンドにグラフとして表示されます。 また、サブスクリプションオプションを構築し、GraphQLレイヤーで対応するミューテーションを起動します。
  4. イベント/スケジュールされたトリガーの設定
    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;

userpasswordhostportdatabasesslは、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スキーマ、クエリ、ミューテーション、サブスクリプション、イベントトリガーなどを使用して、起動して実行するのは非常に簡単です。

[ハスラを試す]をクリックして、プロジェクト名を入力します。

Hasuraプロジェクトの作成
Hasuraプロジェクトの作成。 (大プレビュー)

HerokuでホストされているPostgresデータベースを使用しています。 Herokuにデータベースを作成し、それをこのプロジェクトにリンクします。 これで、クエリが豊富なHasuraコンソールのパワーを体験できるようになります。

プロジェクトの作成後に取得するPostgresDBURLをコピーしてください。 これを設定ファイルに入れる必要があります。

[コンソールの起動]をクリックすると、次のビューにリダイレクトされます。

Hasuraコンソール
Hasuraコンソール。 (大プレビュー)

このプロジェクトに必要なテーブルスキーマの構築を始めましょう。

Postgresデータベースでのテーブルスキーマの作成

[データ]タブに移動し、[テーブルの追加]をクリックしてください。 いくつかのテーブルの作成を始めましょう。

symbolテーブル

このテーブルは、シンボルの情報を格納するために使用されます。 今のところ、ここにはidcompanyの2つのフィールドを保持しています。 フィールドidは主キーであり、 companyのタイプはvarcharです。 この表にいくつかの記号を追加してみましょう。

シンボルテーブル
symbolテーブル。 (大プレビュー)

stock_dataテーブル

stock_dataテーブルには、 idsymboltime 、およびhighlowopenclosevolumeなどのメトリックが格納されます。 このセクションの前半で作成したNodeJsスクリプトは、この特定のテーブルにデータを入力するために使用されます。

テーブルは次のようになります。

stock_dataテーブル
stock_dataテーブル。 (大プレビュー)

きちんとした! データベーススキーマの他のテーブルに移動しましょう!

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_typeeventになります。

これらはすべて、このプロジェクトに必要なテーブルです。 また、スムーズなデータフローと接続を実現するには、これらのテーブル間の関係を設定する必要があります。 それをしましょう!

テーブル間の関係の設定

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クエリをすでに設定しています。

HasuraコンソールでのGraphQLクエリ/ミューテーション
HasuraコンソールでのGraphQLクエリ/ミューテーション。 (大プレビュー)

これらのテーブルでクエリを実行するのは非常に簡単です。また、これらのフィルター/プロパティ( distinct_onlimitoffsetorder_bywhere )のいずれかを適用して、目的のデータを取得することもできます。

これはすべて良さそうですが、サーバー側のコードを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は、 hostkeytimeSeriesFunctionintervalなどの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.jsqueries2.jsを確認してください。

プロジェクトのデータ側が完了したので、フロントエンドビットに移動して、いくつかの興味深いコンポーネントを構築しましょう。

このアプローチでは、データベース構成オプションを保持する必要はありません。

ReactとApolloクライアントを使用したフロントエンド

フロントエンドプロジェクトは同じリポジトリにあり、 create-react-appパッケージを使用して作成されます。 このパッケージを使用して生成されたServiceWorkerは、アセットのキャッシュをサポートしますが、ServiceWorkerファイルにさらにカスタマイズを追加することはできません。 カスタムサービスワーカーオプションのサポートを追加するには、すでにいくつかの未解決の問題があります。 この問題を回避し、カスタムサービスワーカーのサポートを追加する方法があります。

まず、フロントエンドプロジェクトの構造を見てみましょう。

プロジェクトディレクトリ
プロジェクトディレクトリ。 (大プレビュー)

srcディレクトリを確認してください! 今のところ、ServiceWorker関連のファイルについて心配する必要はありません。 これらのファイルについては、このセクションの後半で詳しく説明します。 プロジェクト構造の残りの部分は単純に見えます。 componentsフォルダには、コンポーネント(ローダー、チャート)が含まれます。 servicesフォルダーには、必要な構造でオブジェクトを変換するために使用されるヘルパー関数/サービスの一部が含まれています。 名前が示すように、 stylesには、プロジェクトのスタイリングに使用されるsassファイルが含まれています。 viewsはメインディレクトリであり、ビューレイヤーコンポーネントが含まれています。

このプロジェクトに必要なビューコンポーネントは、シンボルリストとシンボル時系列の2つだけです。 highchartsライブラリのChartコンポーネントを使用して時系列を作成します。 これらのファイルにコードを追加して、フロントエンドでピースを構築してみましょう。

依存関係のインストール

必要な依存関係のリストは次のとおりです。

  • apollo-boost
    Apollo Boostは、ApolloClientの使用を開始するためのゼロ構成の方法です。 デフォルトの構成オプションがバンドルされています。
  • reactstrapbootstrap
    コンポーネントは、これら2つのパッケージを使用して構築されています。
  • graphqlおよびgraphql-type-json
    graphqlapollo-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に保存しています。

Webpushサブスクリプションオブジェクト
Webpushサブスクリプションオブジェクト。 (大プレビュー)

上記のオブジェクトのフィールド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.jssw-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クエリは、 countsumavgmaxminなどの集計値を取得するためにバックグラウンドで作業を行います。

上記の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-iduser_subscriptionなどのユーザーの詳細を取得します。

  • ペイロードで渡されるsymbolと等しいシンボル。
  • trigger_typeeventと同じです。
  • 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"); });

ある値で株式を購読し、その値をテーブルに手動で挿入することによって、このフローをテストしてみましょう(テスト用)!

値が2000AMZNをサブスクライブしてから、この値のデータポイントをテーブルに挿入しました。 挿入直後に株式通知アプリが通知した方法は次のとおりです。

テスト用にstock_dataテーブルに行を挿入する
テストのためにstock_dataテーブルに行を挿入します。 (大プレビュー)

きちんとした! ここでイベント呼び出しログを確認することもできます。

イベントログ
イベントログ。 (大プレビュー)

Webhookは期待どおりに作業を行っています! これで、イベントトリガーの準備が整いました。

スケジュールされた/ cronトリガー

次のようにCronイベントトリガーを使用して、サブスクライバーユーザーに1時間ごとに通知する時間ベースのトリガーを実現できます。

cron /スケジュールされたトリガーのセットアップ
cron /スケジュールされたトリガーのセットアップ。 (大プレビュー)

同じWebhookURLを使用し、トリガーイベントタイプに基づいてサブスクライブされたユーザーをstock_price_time_based_triggerとして処理できます。 実装は、イベントベースのトリガーに似ています。

結論

この記事では、株価通知アプリケーションを作成しました。 Alpha Vantage APIを使用して価格を取得し、HasuraがサポートするPostgresデータベースにデータポイントを保存する方法を学びました。 また、Hasura GraphQLエンジンをセットアップし、イベントベースのスケジュールされたトリガーを作成する方法も学びました。 サブスクライブしたユーザーにWebプッシュ通知を送信するためのグリッチプロジェクトを構築しました。