Создание приложения для уведомления о ценах на акции с использованием React, Apollo GraphQL и Hasura

Опубликовано: 2022-03-10
Краткий обзор ↬ В этой статье мы узнаем, как создать приложение, основанное на событиях, и отправлять push-уведомления при возникновении определенного события. Мы настроим таблицы базы данных, события и запланированные триггеры в механизме Hasura GraphQL и подключим конечную точку GraphQL к внешнему приложению для записи ценовых предпочтений пользователя.

Концепция получения уведомлений о наступлении события по вашему выбору стала популярной по сравнению с приклеиванием к непрерывному потоку данных, чтобы найти это конкретное событие самостоятельно. Люди предпочитают получать релевантные электронные письма/сообщения, когда происходит предпочитаемое ими событие, а не зацикливаться на экране, ожидая, пока это событие произойдет. Терминология, основанная на событиях, также довольно распространена в мире программного обеспечения.

Как здорово было бы, если бы вы могли получать обновления цен на ваши любимые акции на свой телефон?

В этой статье мы собираемся создать приложение для уведомления о ценах на акции , используя React, Apollo GraphQL и движок Hasura GraphQL. Мы собираемся начать проект с шаблонного кода create-react-app и будем строить все с нуля. Мы узнаем, как настроить таблицы базы данных и события на консоли Hasura. Мы также узнаем, как подключить события Hasura, чтобы получать обновления цен на акции с помощью веб-push-уведомлений.

Вот краткий обзор того, что мы будем строить:

Обзор приложения уведомления о ценах на акции
Приложение уведомления о цене акций

Давайте идти!

Еще после прыжка! Продолжить чтение ниже ↓

Обзор того, о чем этот проект

Данные об акциях (включая такие показатели, как максимум , минимум , открытие , закрытие , объем ) будут храниться в базе данных Postgres, поддерживаемой Hasura. Пользователь сможет подписаться на определенную акцию на основе определенной стоимости или может получать уведомления каждый час. Пользователь получит веб-push-уведомление, как только его критерии подписки будут выполнены.

Это выглядит как много вещей, и, очевидно, будут некоторые открытые вопросы о том, как мы будем создавать эти части.

Вот план реализации этого проекта в четыре этапа:

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

Создание проекта Hasura
Создание проекта 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
таблица 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_typeevent .

Это все таблицы, которые нам понадобятся для этого проекта. Мы также должны установить отношения между этими таблицами, чтобы обеспечить плавный поток данных и соединения. Давайте сделаем это!

Настройка отношений между таблицами

Таблица 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 на основе этих таблиц:

Запросы/мутации GraphQL на консоли Hasura
Запросы/мутации GraphQL на консоли Hasura. (Большой превью)

Выполнять запросы к этим таблицам очень просто, и вы также можете применить любой из этих фильтров/свойств ( 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 как:

Объект подписки Webpush
Объект подписки Webpush. (Большой превью)

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 , а затем вставил точку данных в таблицу с этим значением. Вот как приложение уведомления об акциях уведомило меня сразу после вставки:

Вставка строки в таблицу stock_data для тестирования
Вставка строки в таблицу stock_data для тестирования. (Большой превью)

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

Журнал событий
Журнал событий. (Большой превью)

Вебхук работает как положено! Теперь все готово для триггеров событий!

Запланированные/Cron-триггеры

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

Настройка Cron/запланированного триггера
Настройка Cron/Scheduled Trigger. (Большой превью)

Мы можем использовать тот же URL-адрес веб-перехватчика и обрабатывать подписанных пользователей на основе типа триггерного события как stock_price_time_based_trigger . Реализация аналогична триггеру на основе событий.

Заключение

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