Создание приложения для уведомления о ценах на акции с использованием React, Apollo GraphQL и Hasura
Опубликовано: 2022-03-10Концепция получения уведомлений о наступлении события по вашему выбору стала популярной по сравнению с приклеиванием к непрерывному потоку данных, чтобы найти это конкретное событие самостоятельно. Люди предпочитают получать релевантные электронные письма/сообщения, когда происходит предпочитаемое ими событие, а не зацикливаться на экране, ожидая, пока это событие произойдет. Терминология, основанная на событиях, также довольно распространена в мире программного обеспечения.
Как здорово было бы, если бы вы могли получать обновления цен на ваши любимые акции на свой телефон?
В этой статье мы собираемся создать приложение для уведомления о ценах на акции , используя React, Apollo GraphQL и движок Hasura GraphQL. Мы собираемся начать проект с шаблонного кода create-react-app
и будем строить все с нуля. Мы узнаем, как настроить таблицы базы данных и события на консоли Hasura. Мы также узнаем, как подключить события Hasura, чтобы получать обновления цен на акции с помощью веб-push-уведомлений.
Вот краткий обзор того, что мы будем строить:

Давайте идти!
Обзор того, о чем этот проект
Данные об акциях (включая такие показатели, как максимум , минимум , открытие , закрытие , объем ) будут храниться в базе данных Postgres, поддерживаемой Hasura. Пользователь сможет подписаться на определенную акцию на основе определенной стоимости или может получать уведомления каждый час. Пользователь получит веб-push-уведомление, как только его критерии подписки будут выполнены.
Это выглядит как много вещей, и, очевидно, будут некоторые открытые вопросы о том, как мы будем создавать эти части.
Вот план реализации этого проекта в четыре этапа:
- Получение данных об акциях с помощью скрипта NodeJs
Мы начнем с получения данных об акциях с помощью простого скрипта NodeJs от одного из поставщиков API акций — Alpha Vantage. Этот скрипт будет получать данные для конкретной акции с интервалом в 5 минут. Ответ API включает максимум , минимум , открытие , закрытие и объем . Затем эти данные будут вставлены в базу данных Postgres, интегрированную с серверной частью Hasura. - Настройка движка Hasura GraphQL
Затем мы настроим несколько таблиц в базе данных Postgres для записи точек данных. Hasura автоматически генерирует схемы, запросы и мутации GraphQL для этих таблиц. - Внешний интерфейс с использованием React и Apollo Client
Следующим шагом является интеграция слоя GraphQL с использованием клиента Apollo и поставщика Apollo (конечная точка GraphQL, предоставленная Hasura). Точки данных будут отображаться в виде диаграмм во внешнем интерфейсе. Мы также создадим параметры подписки и запустим соответствующие мутации на уровне GraphQL. - Настройка событий/запланированных триггеров
Hasura предоставляет отличные инструменты для работы с триггерами. Мы добавим события и запланированные триггеры в таблицу данных об акциях. Эти триггеры будут установлены, если пользователь заинтересован в получении уведомления, когда цены на акции достигают определенного значения (триггер события). Пользователь также может выбрать получение уведомления о конкретной акции каждый час (запланированный триггер).
Теперь, когда план готов, давайте претворим его в жизнь!
Вот репозиторий GitHub для этого проекта. Если вы где-то заблудились в приведенном ниже коде, обратитесь к этому репозиторию и вернитесь к скорости!
Получение данных об акциях с помощью скрипта NodeJs
Это не так сложно, как кажется! Нам нужно написать функцию, которая извлекает данные с использованием конечной точки Alpha Vantage, и этот вызов выборки должен запускаться с интервалом в 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
Это неблокирующий клиент PostgreSQL для NodeJs. -
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
с параметрами конфигурации, заданными в файле конфигурации. Нам еще предстоит завершить файл конфигурации, но никаких изменений, связанных с параметрами конфигурации, не будет.
Теперь мы подготовили почву и готовы начать делать некоторые вызовы API к конечной точке Alpha Vantage.
Давайте перейдем к интересному!
Получение данных об акциях
В этом разделе мы будем получать данные о запасах из конечной точки 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); }); };
Это оно! Мы получили данные об акциях из API Alpha Vantage и написали функцию для помещения их в базу данных Postgres в таблице stock_data
. Не хватает только одной детали, чтобы все это заработало! Мы должны заполнить правильные значения в файле конфигурации. Эти значения мы получим после настройки движка Hasura. Давайте перейдем к этому прямо сейчас!
Пожалуйста, обратитесь к каталогу server
для получения полного кода для получения точек данных из конечной точки Alpha Vantage и заполнения их в базе данных Hasura Postgres.
Если такой подход к настройке соединений, параметрам конфигурации и вставке данных с помощью необработанного запроса кажется немного сложным, не беспокойтесь об этом! Мы собираемся научиться делать все это простым способом с помощью мутации GraphQL после настройки движка Hasura!
Настройка движка Hasura GraphQL
Настроить движок Hasura и начать работу со схемами GraphQL, запросами, мутациями, подписками, триггерами событий и многим другим очень просто!
Нажмите «Попробовать Hasura» и введите название проекта:

Я использую базу данных Postgres, размещенную на Heroku. Создайте базу данных на Heroku и свяжите ее с этим проектом. После этого у вас должно быть все готово, чтобы испытать всю мощь консоли Hasura, богатой запросами.
Скопируйте URL-адрес базы данных Postgres, который вы получите после создания проекта. Мы должны поместить это в файл конфигурации.
Нажмите «Запустить консоль», и вы будете перенаправлены в это представление:

Давайте начнем создавать схему таблицы, которая нам понадобится для этого проекта.
Создание схемы таблиц в базе данных 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
хранится объект подписки по идентификатору пользователя. Этот объект подписки используется для отправки пользователям веб-push-уведомлений. Позже в этой статье мы узнаем, как создать этот объект подписки.
В этой таблице есть два поля: id
— это первичный ключ типа uuid
, а поле подписки — типа jsonb
.
таблица events
Это важный элемент, который используется для хранения параметров события уведомления. Когда пользователь соглашается на обновление цен на определенную акцию, мы сохраняем информацию об этом событии в этой таблице. Эта таблица содержит следующие столбцы:
-
id
: это первичный ключ со свойством автоинкремента. -
symbol
: это текстовое поле. -
user_id
: имеет типuuid
. -
trigger_type
: используется для хранения типа триггера события —time/event
. -
trigger_value
: используется для хранения значения триггера. Например, если пользователь выбрал триггер события на основе цены — он хочет получать обновления, если цена акции достигла 1000, тогда значениеtrigger_value
будет равно 1000, аtrigger_type
—event
.
Это все таблицы, которые нам понадобятся для этого проекта. Мы также должны установить отношения между этими таблицами, чтобы обеспечить плавный поток данных и соединения. Давайте сделаем это!
Настройка отношений между таблицами
Таблица events
используется для отправки веб-push-уведомлений на основе значения события. Итак, имеет смысл связать эту таблицу с таблицей user_subscription
, чтобы иметь возможность отправлять push-уведомления о подписках, хранящихся в этой таблице.
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
Поместите необходимые параметры в файл config.js
в каталоге server
следующим образом:
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;
Пожалуйста, введите эти параметры из строки базы данных, которая была сгенерирована, когда мы создали базу данных Postgres на Heroku.
apiHostOptions
состоит из параметров, связанных с API, таких как host
, key
, timeSeriesFunction
и interval
.
Вы получите поле graphqlURL
на вкладке GRAPHIQL в консоли Hasura.
Функция getConfig
используется для возврата запрошенного значения из объекта конфигурации. Мы уже использовали это в index.js
в каталоге server
.
Пришло время запустить сервер и внести некоторые данные в базу данных. Я добавил один скрипт в package.json
как:
"scripts": { "start": "nodemon index.js" }
Запустите npm start
на терминале, и точки данных массива символов в index.js
должны быть заполнены в таблицах.
Рефакторинг необработанного запроса в скрипте NodeJs для мутации GraphQL
Теперь, когда движок Hasura настроен, давайте посмотрим, насколько просто вызвать мутацию в таблице stock_data
.
Функция insertStocksData
в queries.js
использует необработанный запрос:
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 }
Обратите внимание: мы должны добавить graphqlURL
в файл config.js
.
Модуль apollo-fetch
возвращает функцию выборки, которую можно использовать для запроса/изменения даты в конечной точке GraphQL. Достаточно легко, верно?
Единственное изменение, которое мы должны сделать в index.js
, — вернуть объект акций в формате, требуемом функцией insertStocksData
. Пожалуйста, проверьте index2.js
и queries2.js
для полного кода с этим подходом.
Теперь, когда мы завершили работу с данными проекта, давайте перейдем к интерфейсу и создадим несколько интересных компонентов!
Примечание . При таком подходе нам не нужно сохранять параметры конфигурации базы данных!
Внешний интерфейс с использованием React и клиента Apollo
Интерфейсный проект находится в том же репозитории и создается с помощью пакета create-react-app
. Service Worker, созданный с помощью этого пакета, поддерживает кэширование активов, но не позволяет добавлять дополнительные настройки в файл service worker. Уже есть некоторые открытые проблемы, чтобы добавить поддержку настраиваемых параметров сервисного работника. Есть способы обойти эту проблему и добавить поддержку пользовательского работника службы.
Начнем с рассмотрения структуры фронтенд-проекта:

Пожалуйста, проверьте каталог src
! Пока не беспокойтесь о файлах, связанных с сервис-воркером. Мы узнаем больше об этих файлах позже в этом разделе. В остальном структура проекта выглядит просто. В папке components
будут компоненты (Loader, Chart); папка services
содержит некоторые вспомогательные функции/сервисы, используемые для преобразования объектов в требуемую структуру; styles
, как следует из названия, содержат файлы sass, используемые для стилизации проекта; views
является основным каталогом и содержит компоненты слоя представления.
Для этого проекта нам понадобятся всего два компонента представления — список символов и временные ряды символов. Мы построим временной ряд, используя компонент Chart из библиотеки highcharts. Давайте начнем добавлять код в эти файлы, чтобы создать интерфейсную часть!
Установка зависимостей
Вот список зависимостей, которые нам понадобятся:
-
apollo-boost
Apollo boost — это способ начать использовать Apollo Client без настройки. Он поставляется в комплекте с параметрами конфигурации по умолчанию. -
reactstrap
иbootstrap
Компоненты создаются с использованием этих двух пакетов. -
graphql
иgraphql-type-json
graphql
является необходимой зависимостью для использованияapollo-boost
аgraphql-type-json
используется для поддержки типа данныхjson
, используемого в схеме GraphQL. highcharts
иhighcharts-react-official
И эти два пакета будут использоваться для построения диаграммы:node-sass
Это добавлено для поддержки файлов sass для стилей.uuid
Этот пакет используется для генерации сильных случайных значений.
Все эти зависимости обретут смысл, как только мы начнем использовать их в проекте. Давайте перейдем к следующему биту!
Настройка клиента Apollo
Создайте apolloClient.js
внутри папки src
как:
import ApolloClient from 'apollo-boost'; const apolloClient = new ApolloClient({ uri: '<HASURA_CONSOLE_URL>' }); export default apolloClient;
Приведенный выше код создает экземпляр ApolloClient и принимает uri
в параметрах конфигурации. uri
— это URL-адрес вашей консоли Hasura. Вы получите это поле uri
на вкладке GRAPHIQL
в разделе GraphQL Endpoint .
Приведенный выше код выглядит простым, но он выполняет основную часть проекта! Он связывает схему GraphQL, построенную на Hasura, с текущим проектом.
Мы также должны передать этот клиентский объект 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, который может перехватывать сетевые запросы. Он используется для запроса кеша, чтобы проверить, присутствует ли запрошенный ресурс в кеше, вместо того, чтобы отправляться на сервер. Сервисные работники также используются для отправки веб-push-уведомлений на подписанные устройства.
Мы должны отправлять веб-push-уведомления об обновлении цен на акции подписавшимся пользователям. Давайте заложим основу и создадим этот сервисный рабочий файл!
Связанная с insertSubscription
вырезка в файле index.js
выполняет работу по регистрации сервисного работника и размещению объекта subscriptionMutation
в базе данных с помощью subscribeMutation .
Пожалуйста, обратитесь к query.js для всех запросов и мутаций, используемых в проекте.
serviceWorker.register(insertSubscription);
вызывает функцию register
, записанную в файле serviceWorker.js
. Вот:
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
. Мы проверим этот файл через минуту!
Функция getSubscription
выполняет работу по получению объекта подписки, используя метод subscribe
объекта pushManager
. Затем этот объект подписки сохраняется в таблице user_subscription
для идентификатора пользователя. Обратите внимание, что идентификатор пользователя создается с помощью функции 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()
вызвал это всплывающее окно, которое запрашивает у пользователя разрешение на отправку уведомлений. Как только пользователь нажимает «Разрешить», служба push генерирует объект подписки. Мы сохраняем этот объект в localStorage как:

endpoint
поля в указанном выше объекте используется для идентификации устройства, и сервер использует эту конечную точку для отправки пользователю веб-push-уведомлений.
Мы выполнили работу по инициализации и регистрации сервис-воркера. У нас также есть объект подписки пользователя! Все работает хорошо, потому что файл serviceWorker.js
находится в public
папке. Теперь давайте настроим сервис-воркера, чтобы все было готово!
Это немного сложная тема, но давайте разберемся! Как упоминалось ранее, утилита create-react-app
не поддерживает настройки по умолчанию для сервис-воркера. Мы можем добиться реализации работника службы поддержки клиентов, используя модуль workbox-build
.
Мы также должны убедиться, что поведение предварительного кэширования файлов по умолчанию не повреждено. Мы изменим часть, в которой сервис-воркер собирается в проекте. И workbox-build помогает именно в этом! Аккуратные вещи! Давайте не будем усложнять и перечислим все, что нам нужно сделать, чтобы пользовательский сервис-воркер заработал:
- Управляйте предварительным кэшированием ресурсов с помощью
workboxBuild
. - Создайте шаблон сервисного работника для кэширования ресурсов.
- Создайте файл
sw-precache-config.js
, чтобы предоставить настраиваемые параметры конфигурации. - Добавьте рабочий скрипт службы сборки на этапе сборки в
package.json
.
Не беспокойтесь, если все это звучит запутанно! Статья не фокусируется на объяснении семантики каждого из этих пунктов. Сейчас мы должны сосредоточиться на части реализации! Я постараюсь рассказать о причинах выполнения всей работы по созданию пользовательского сервис-воркера в другой статье.
Создадим два файла sw-build.js
и sw-custom.js
в каталоге src
. Пожалуйста, обратитесь к ссылкам на эти файлы и добавьте код в свой проект.
Давайте теперь создадим файл 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
, чтобы освободить место для создания пользовательского рабочего файла службы:
Добавьте эти операторы в раздел 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
-уведомлений для прослушивания push-уведомлений, отправляемых сервером. Функция showNotification
используется для отображения пользователю веб-push-уведомлений.
Это оно! Мы закончили всю тяжелую работу по настройке специального работника службы для обработки push-уведомлений. Мы увидим эти уведомления в действии, как только создадим пользовательские интерфейсы!
Мы приближаемся к созданию основных частей кода. Давайте теперь начнем с первого вида!
Просмотр списка символов
Компонент App
, использованный в предыдущем разделе, выглядит следующим образом:
import React from 'react'; import SymbolList from './views/symbolList'; const App = () => { return <SymbolList />; }; export default App;
Это простой компонент, который возвращает представление SymbolList
, а SymbolList
выполняет всю тяжелую работу по отображению символов в аккуратно связанном пользовательском интерфейсе.
Давайте посмотрим на symbolList.js
внутри папки views
:
Пожалуйста, обратитесь к файлу здесь!
Компонент возвращает результаты функции 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>
Мы используем компонент Card
ReactStrap для рендеринга этих карт. Компонент Popover
используется для отображения опций на основе подписки:

Когда пользователь нажимает на значок bell
для определенной акции, он может подписаться на получение уведомлений каждый час или когда цена акции достигает введенного значения. Мы увидим это в действии в разделе Events/Time Triggers.
Примечание . Мы перейдем к компоненту 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! Полный код временных рядов акций см. в файлах Chart.js и stockTimeseries.js
.
Теперь вы должны быть готовы к работе с данными и пользовательскими интерфейсами. Давайте теперь перейдем к интересной части — настройке триггеров событий/времени на основе ввода пользователя.
Настройка событий/запланированных триггеров
В этом разделе мы узнаем, как настроить триггеры на консоли Hasura и как отправлять push-уведомления выбранным пользователям. Давайте начнем!
Триггеры событий на консоли Hasura
Давайте создадим триггер события stock_value
для таблицы stock_data
и insert
в качестве операции триггера. Веб-перехватчик будет запускаться каждый раз при вставке в таблицу stock_data
.

Мы собираемся создать проект с ошибками для URL-адреса веб-перехватчика. Позвольте мне немного рассказать о веб-хуках, чтобы их было легко понять:
Веб-перехватчики используются для отправки данных из одного приложения в другое при наступлении определенного события. Когда событие инициируется, выполняется вызов HTTP POST на URL-адрес веб-перехватчика с данными события в качестве полезной нагрузки.
В этом случае, когда есть операция вставки в таблицу stock_data
, почтовый вызов HTTP будет сделан на настроенный URL-адрес веб-перехватчика (почтовый вызов в проекте с ошибками).
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!
Получение подписавшихся пользователей
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
. Затем мы отправляем push-уведомления каждому из этих пользователей. Для отправки уведомления используется функция 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-push-уведомления! Давайте сделаем это прямо сейчас!
Отправка Web-Push-уведомлений подписавшимся пользователям
Сначала нам нужно получить открытый и закрытый ключи VAPID для отправки веб-push-уведомлений. Пожалуйста, сохраните эти ключи в файле .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 на конечную точку подписки, указанную в качестве первого параметра.
Это все, что требуется для успешной отправки 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"); });
Давайте проверим этот поток, подписавшись на акции с некоторой стоимостью и вручную вставив это значение в таблицу (для тестирования)!
Я подписался на AMZN
со значением 2000
, а затем вставил точку данных в таблицу с этим значением. Вот как приложение уведомления об акциях уведомило меня сразу после вставки:

Аккуратный! Вы также можете проверить журнал вызовов событий здесь:

Вебхук работает как положено! Теперь все готово для триггеров событий!
Запланированные/Cron-триггеры
Мы можем получить основанный на времени триггер для уведомления пользователей-подписчиков каждый час, используя триггер события Cron, как:

Мы можем использовать тот же URL-адрес веб-перехватчика и обрабатывать подписанных пользователей на основе типа триггерного события как stock_price_time_based_trigger
. Реализация аналогична триггеру на основе событий.
Заключение
В этой статье мы создали приложение для уведомления о ценах на акции. Мы узнали, как получать цены с помощью API-интерфейсов Alpha Vantage и сохранять точки данных в базе данных Postgres, поддерживаемой Hasura. Мы также узнали, как настроить движок Hasura GraphQL и создать триггеры на основе событий и по расписанию. Мы создали глитч-проект для отправки web-push-уведомлений подписавшимся пользователям.