使用 React、Apollo GraphQL 和 Hasura 构建股票价格通知应用程序
已发表: 2022-03-10与粘在连续数据流上以自己查找特定事件相比,在您选择的事件发生时得到通知的概念变得流行起来。 人们更喜欢在他们喜欢的事件发生时获得相关的电子邮件/消息,而不是被挂在屏幕上等待该事件发生。 基于事件的术语在软件领域也很常见。
如果您可以在手机上获取您最喜欢的股票价格的最新信息,那该有多棒?
在本文中,我们将使用 React、Apollo GraphQL 和 Hasura GraphQL 引擎构建一个Stocks Price Notifier应用程序。 我们将从create-react-app
样板代码开始该项目,并将构建所有内容。 我们将学习如何在 Hasura 控制台上设置数据库表和事件。 我们还将学习如何使用网络推送通知连接 Hasura 的事件以获取股票价格更新。
快速浏览一下我们将要构建的内容:

我们走吧!
这个项目是关于什么的概述
股票数据(包括high 、 low 、 open 、 close 、 volume等指标)将存储在 Hasura 支持的 Postgres 数据库中。 用户将能够根据某些价值订阅特定股票,或者他可以选择每小时收到通知。 满足订阅条件后,用户将收到网络推送通知。
这看起来有很多东西,对于我们将如何构建这些部分,显然会有一些悬而未决的问题。
以下是我们如何分四个步骤完成这个项目的计划:
- 使用 NodeJs 脚本获取股票数据
我们将首先使用一个简单的 NodeJs 脚本从股票 API 的提供者之一——Alpha Vantage 获取股票数据。 此脚本将每隔 5 分钟获取特定股票的数据。 API 的响应包括高、低、开、平和成交量。 然后将这些数据插入与 Hasura 后端集成的 Postgres 数据库中。 - 设置 Hasura GraphQL 引擎
然后我们将在 Postgres 数据库上设置一些表来记录数据点。 Hasura 自动为这些表生成 GraphQL 模式、查询和突变。 - 使用 React 和 Apollo 客户端的前端
下一步是使用 Apollo 客户端和 Apollo Provider(Hasura 提供的 GraphQL 端点)集成 GraphQL 层。 数据点将在前端显示为图表。 我们还将构建订阅选项,并将在 GraphQL 层上触发相应的突变。 - 设置事件/预定触发器
Hasura 为触发器提供了出色的工具。 我们将在股票数据表中添加事件和预定触发器。 如果用户有兴趣在股票价格达到特定值时收到通知(事件触发器),则将设置这些触发器。 用户还可以选择每小时获取特定股票的通知(预定触发)。
现在计划已经准备好了,让我们付诸行动吧!
这是该项目的 GitHub 存储库。 如果您在下面的代码中迷失了方向,请参阅此存储库并恢复速度!
使用 NodeJs 脚本获取股票数据
这并不像听起来那么复杂! 我们必须编写一个使用 Alpha Vantage 端点获取数据的函数,并且这个 fetch 调用应该在5 分钟的间隔内触发(你猜对了,我们必须把这个函数调用放在setInterval
中)。
如果您仍然想知道 Alpha Vantage 是什么,并且只是想在跳到编码部分之前摆脱它,那么这里是:
Alpha Vantage Inc. 是免费 API 的领先提供商,可提供股票、外汇 (FX) 和数字/加密货币的实时和历史数据。
我们将使用此端点来获取特定股票的所需指标。 此 API 需要一个 API 密钥作为参数之一。 您可以从这里获得免费的 API 密钥。 现在我们可以进入有趣的部分了——让我们开始编写一些代码吧!
安装依赖
创建一个stocks-app
目录并在其中创建一个server
目录。 使用npm init
将其初始化为节点项目,然后安装这些依赖项:
npm i isomorphic-fetch pg nodemon --save
这是我们编写获取股票价格并将它们存储在 Postgres 数据库中的脚本所需的仅有的三个依赖项。
以下是对这些依赖项的简要说明:
-
isomorphic-fetch
它使得在客户端和服务器上都可以轻松地以同构方式(以相同的形式)使用fetch
。 -
pg
它是 NodeJs 的非阻塞 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
实例。 我们尚未完成配置文件,但不会有与配置选项相关的任何更改。
我们现在已经做好准备,准备开始对 Alpha Vantage 端点进行一些 API 调用。
让我们进入有趣的部分!
获取股票数据
在本节中,我们将从 Alpha Vantage 端点获取股票数据。 这是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
函数没有做太多! 它遍历这些符号并查询 Alpha Vantage 端点${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 中获取了股票的数据点,并编写了一个函数将这些数据点放入 Postgres 数据库的stock_data
表中。 只需缺少一件即可完成所有这些工作! 我们必须在配置文件中填充正确的值。 我们将在设置 Hasura 引擎后获得这些值。 让我们马上开始吧!
有关从 Alpha Vantage 端点获取数据点并将其填充到 Hasura Postgres 数据库的完整代码,请参阅server
目录。
如果这种使用原始查询设置连接、配置选项和插入数据的方法看起来有点困难,请不要担心! 一旦设置了 Hasura 引擎,我们将学习如何使用 GraphQL 突变轻松完成所有这些工作!
设置 Hasura GraphQL 引擎
设置 Hasura 引擎并使用 GraphQL 模式、查询、突变、订阅、事件触发器等启动和运行非常简单!
点击 Try Hasura 并输入项目名称:

我正在使用 Heroku 上托管的 Postgres 数据库。 在 Heroku 上创建一个数据库并将其链接到该项目。 然后,您应该准备好体验查询丰富的 Hasura 控制台的强大功能。
请复制创建项目后获得的 Postgres DB URL。 我们必须把它放在配置文件中。
单击启动控制台,您将被重定向到此视图:

让我们开始构建这个项目所需的表模式。
在 Postgres 数据库上创建表模式
请转到“数据”选项卡并单击“添加表”! 让我们开始创建一些表:
symbol
表
该表将用于存储符号的信息。 目前,我在这里保留了两个字段—— id
和company
。 字段id
是主键, company
是varchar
类型。 让我们在此表中添加一些符号:

symbol
表。 (大预览) stock_data
表
stock_data
表存储id
、 symbol
、 time
以及high
、 low
、 open
、 close
、 volume
等指标。 我们在本节前面编写的 NodeJs 脚本将用于填充此特定表。
表格如下所示:

stock_data
表。 (大预览)整洁的! 让我们进入数据库模式中的另一个表!
user_subscription
表
user_subscription
表根据用户 ID 存储订阅对象。 此订阅对象用于向用户发送网络推送通知。 我们将在本文后面了解如何生成此订阅对象。
该表中有两个字段 - id
是uuid
类型的主键,订阅字段是jsonb
类型。
events
表
这是重要的一个,用于存储通知事件选项。 当用户选择特定股票的价格更新时,我们将该事件信息存储在此表中。 此表包含以下列:
-
id
:是具有自动增量属性的主键。 -
symbol
:是一个文本字段。 -
user_id
:属于uuid
类型。 -
trigger_type
:用于存储事件触发类型——time/event
。 -
trigger_value
:用于存储触发值。 例如,如果用户选择了基于价格的事件触发器——如果股票价格达到 1000,他想要更新,那么trigger_value
将是 1000 并且trigger_type
将是event
。
这些是我们在这个项目中需要的所有表格。 我们还必须在这些表之间建立关系,以实现顺畅的数据流和连接。 让我们这样做吧!
建立表之间的关系
events
表用于根据事件值发送网络推送通知。 因此,将这个表与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
由与 API 相关的选项组成,例如host
、 key
、 timeSeriesFunction
和interval
。
您将在 Hasura 控制台的GRAPHIQL选项卡中获得graphqlURL
字段。
getConfig
函数用于从配置对象返回请求的值。 我们已经在server
目录的index.js
中使用了它。
是时候运行服务器并在数据库中填充一些数据了。 我在package.json
中添加了一个脚本:
"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
模块返回一个 fetch 函数,该函数可用于查询/改变 GraphQL 端点上的日期。 很容易,对吧?
我们在index.js
中要做的唯一更改是以insertStocksData
函数所需的格式返回股票对象。 请查看index2.js
和queries2.js
以获取使用此方法的完整代码。
现在我们已经完成了项目的数据端,让我们进入前端部分并构建一些有趣的组件!
注意:我们不必使用这种方法保留数据库配置选项!
使用 React 和 Apollo 客户端的前端
前端项目位于同一存储库中,并使用create-react-app
包创建。 使用此包生成的服务工作者支持资产缓存,但它不允许将更多自定义项添加到服务工作者文件中。 已经有一些未解决的问题可以添加对自定义服务工作者选项的支持。 有一些方法可以解决这个问题并添加对自定义服务工作者的支持。
让我们从前端项目的结构开始:

请检查src
目录! 暂时不用担心 service worker 相关的文件。 我们将在本节稍后部分了解有关这些文件的更多信息。 项目结构的其余部分看起来很简单。 components
文件夹将包含组件(Loader、Chart); services
文件夹包含一些用于在所需结构中转换对象的辅助函数/服务; 顾名思义, styles
包含用于设置项目样式的 sass 文件; views
是主目录,它包含视图层组件。
对于这个项目,我们只需要两个视图组件——符号列表和符号时间序列。 我们将使用 highcharts 库中的 Chart 组件构建时间序列。 让我们开始在这些文件中添加代码来构建前端的各个部分!
安装依赖
这是我们需要的依赖项列表:
-
apollo-boost
Apollo boost 是开始使用 Apollo 客户端的零配置方式。 它与默认配置选项捆绑在一起。 -
reactstrap
和bootstrap
这些组件是使用这两个包构建的。 -
graphql
和graphql-type-json
graphql
是使用apollo-boost
的必需依赖项,而graphql-type-json
用于支持 GraphQL 模式中使用的json
数据类型。 highcharts
和highcharts-react-official
这两个包将用于构建图表: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。 您将在GraphQL Endpoint部分的GRAPHIQL
选项卡上获得此uri
字段。
上面的代码看起来很简单,但它处理了项目的主要部分! 它将基于 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 文件。 它用于查询缓存以检查请求的资产是否已经存在于缓存中,而不是乘车到服务器。 Service Worker 还用于向订阅的设备发送网络推送通知。
我们必须向订阅用户发送股票价格更新的网络推送通知。 让我们奠定基础并构建这个 service worker 文件!
index.js
文件中的insertSubscription
相关片段负责注册 service worker 并使用subscriptionMutation
将订阅对象放入数据库中。
请参阅 queries.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
是否被浏览器支持,然后注册托管在 URL swUrl
上的 service worker 文件。 我们稍后会检查这个文件!
getSubscription
函数使用pushManager
对象上的subscribe
方法完成获取订阅对象的工作。 这个订阅对象然后存储在user_subscription
表中,对应一个 userId。 请注意,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
文件,因此一切正常。 现在让我们设置 service worker 来做好准备!
这是一个有点困难的话题,但让我们做对吧! 如前所述, create-react-app
实用程序默认不支持 service worker 的自定义。 我们可以使用workbox-build
模块来实现客户服务工作者的实现。
我们还必须确保预缓存文件的默认行为完好无损。 我们将修改服务工作者在项目中构建的部分。 而且,workbox-build 有助于实现这一目标! 整洁的东西! 让我们保持简单并列出我们必须做的所有事情来使自定义服务工作者工作:
- 使用
workboxBuild
处理资产的预缓存。 - 创建用于缓存资产的服务工作者模板。
- 创建
sw-precache-config.js
文件以提供自定义配置选项。 - 在
package.json
的构建步骤中添加构建服务工作者脚本。
如果这一切听起来令人困惑,请不要担心! 这篇文章并不专注于解释这些点背后的语义。 我们现在必须专注于实施部分! 我将尝试在另一篇文章中介绍完成所有工作以创建自定义服务工作者的原因。
让我们在src
目录下创建两个文件sw-build.js
和sw-custom.js
。 请参考这些文件的链接并将代码添加到您的项目中。
现在让我们在根级别创建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
文件,为构建自定义 service worker 文件腾出空间:
在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
文件夹中添加一个自定义的 service worker 文件:
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
监听器来监听服务器发送的推送通知。 函数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 的聚合查询在后台完成工作以获取聚合值,例如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
图标时,他可以选择每小时或股票价格达到输入值时收到通知。 我们将在事件/时间触发器部分看到这一点。
注意:我们将在下一节中介绍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 都会运行。

我们将为 webhook URL 创建一个故障项目。 让我简单介绍一下 webhook 以便于理解:
Webhook 用于在发生特定事件时将数据从一个应用程序发送到另一个应用程序。 触发事件时,将对 Webhook URL 进行 HTTP POST 调用,并将事件数据作为有效负载。
在这种情况下,当对stock_data
表进行插入操作时,将对配置的 webhook URL 进行 HTTP post 调用(glitch 项目中的 post 调用)。
用于发送 Web 推送通知的故障项目
我们必须获取 webhook URL 以放入上述事件触发接口。 转到 glitch.com 并创建一个新项目。 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
函数获取订阅用户。 然后,我们向这些用户中的每一个发送网络推送通知。 函数sendWebpush
用于发送通知。 稍后我们将看看 web-push 的实现。
函数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 推送通知
我们必须首先获取公共和私有 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-push。
这就是成功向订阅用户发送网络推送通知所需的全部内容。 这是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"); });
让我们通过使用某个值订阅 stock 并手动将该值插入表中来测试这个流程(用于测试)!
我订阅了价值为2000
的AMZN
,然后用这个值在表中插入了一个数据点。 以下是股票通知应用程序在插入后立即通知我的方式:

整洁的! 您还可以在此处查看事件调用日志:

webhook 正在按预期工作! 我们现在都准备好触发事件了!
计划/Cron 触发器
我们可以使用 Cron 事件触发器实现基于时间的触发器,以每小时通知订阅用户用户:

我们可以使用相同的 webhook URL 并根据触发事件类型为stock_price_time_based_trigger
处理订阅用户。 实现类似于基于事件的触发器。
结论
在本文中,我们构建了一个股票价格通知器应用程序。 我们学习了如何使用 Alpha Vantage API 获取价格并将数据点存储在 Hasura 支持的 Postgres 数据库中。 我们还学习了如何设置 Hasura GraphQL 引擎并创建基于事件和计划的触发器。 我们构建了一个故障项目,用于向订阅用户发送网络推送通知。