React, Apollo GraphQL ve Hasura Kullanarak Hisse Senedi Fiyat Bildirimi Uygulaması Oluşturma

Yayınlanan: 2022-03-10
Kısa özet ↬ Bu makalede, olay tabanlı bir uygulamanın nasıl oluşturulacağını ve belirli bir olay tetiklendiğinde web-push bildiriminin nasıl gönderileceğini öğreneceğiz. Hasura GraphQL motorunda veritabanı tabloları, olaylar ve planlanmış tetikleyiciler ayarlayacağız ve kullanıcının hisse senedi fiyatı tercihini kaydetmek için GraphQL uç noktasını ön uç uygulamasına bağlayacağız.

Seçtiğiniz olayı gerçekleştiğinde haberdar olma konsepti, belirli olayları kendiniz bulmak için sürekli veri akışına yapıştırılmaya kıyasla popüler hale gelmiştir. İnsanlar, o olayın olmasını beklemek için ekrana takılıp kalmak yerine, tercih ettikleri olay gerçekleştiğinde ilgili e-postaları/mesajları almayı tercih ederler. Olay tabanlı terminoloji yazılım dünyasında da oldukça yaygındır.

En sevdiğiniz hisse senedinin fiyat güncellemelerini telefonunuzda alabilseydiniz, bu ne kadar harika olurdu?

Bu yazımızda React, Apollo GraphQL ve Hasura GraphQL motorunu kullanarak bir Stocks Price Notifier uygulaması oluşturacağız. Projeye bir create-react-app ortak kodundan başlayacağız ve her şeyi temelden oluşturacağız. Hasura konsolunda veritabanı tablolarını ve olaylarını nasıl kuracağımızı öğreneceğiz. Ayrıca, web-push bildirimlerini kullanarak hisse senedi fiyat güncellemelerini almak için Hasura'nın olaylarını nasıl bağlayacağımızı da öğreneceğiz.

İşte ne inşa edeceğimize hızlı bir bakış:

Hisse Fiyat Bildirimi Uygulamasına Genel Bakış
Stok fiyat bildirici uygulaması

Haydi gidelim!

Atlamadan sonra daha fazlası! Aşağıdan okumaya devam edin ↓

Bu Projenin Ne Hakkında Olduğuna Genel Bir Bakış

Hisse senedi verileri ( yüksek , düşük , ​​açık , kapalı , hacim gibi metrikler dahil) Hasura destekli bir Postgres veritabanında saklanacaktır. Kullanıcı, belirli bir değere dayalı olarak belirli bir hisse senedine abone olabilir veya her saat başı bildirim almayı tercih edebilir. Kullanıcı, abonelik kriterleri karşılandığında bir web-push bildirimi alacaktır.

Bu çok fazla şeye benziyor ve açıkçası bu parçaları nasıl oluşturacağımıza dair bazı açık sorular olacak.

İşte bu projeyi dört adımda nasıl gerçekleştireceğimize dair bir plan:

  1. NodeJs betiği kullanarak hisse senedi verilerini getirme
    Hisse senedi API'si sağlayıcılarından biri olan Alpha Vantage'dan basit bir NodeJs betiği kullanarak hisse senedi verilerini getirerek başlayacağız. Bu komut dosyası, belirli bir hisse senedi için verileri 5 dakikalık aralıklarla getirecektir. API'nin yanıtı yüksek , düşük , ​​açık , kapat ve hacim içerir . Bu veriler daha sonra Hasura arka ucu ile entegre olan Postgres veritabanına eklenecektir.
  2. Hasura Graphql motorunu ayarlama
    Daha sonra veri noktalarını kaydetmek için Postgres veritabanında bazı tablolar ayarlayacağız. Hasura, bu tablolar için GraphQL şemalarını, sorgularını ve mutasyonlarını otomatik olarak oluşturur.
  3. React ve Apollo Client kullanan ön uç
    Sonraki adım, Apollo istemcisini ve Apollo Sağlayıcısını (Hasura tarafından sağlanan GraphQL uç noktası) kullanarak GraphQL katmanını entegre etmektir. Veri noktaları ön uçta çizelgeler olarak gösterilecektir. Ayrıca abonelik seçeneklerini oluşturacağız ve GraphQL katmanında karşılık gelen mutasyonları tetikleyeceğiz.
  4. Olay / Planlanmış Tetikleyiciler Ayarlama
    Hasura, tetikleyiciler etrafında mükemmel bir araç sağlar. Hisse senedi veri tablosuna etkinlik ve planlanmış tetikleyiciler ekleyeceğiz. Bu tetikleyiciler, kullanıcı, hisse senedi fiyatları belirli bir değere ulaştığında (olay tetikleyici) bir bildirim almak istiyorsa ayarlanacaktır. Kullanıcı ayrıca her saat belirli bir hisse senedi bildirimi almayı da seçebilir (planlanmış tetikleyici).

Artık plan hazır olduğuna göre harekete geçelim!

İşte bu proje için GitHub deposu. Aşağıdaki kodun herhangi bir yerinde kaybolursanız, bu depoya bakın ve hızınıza geri dönün!

NodeJs Komut Dosyası Kullanarak Hisse Senedi Verilerini Alma

Bu göründüğü kadar karmaşık değil! Alpha Vantage Endpoint kullanarak verileri içeren bir işlev yazmanız gerekecek ve bu getirme çağrısı 5 dakika aralığında ateşlenmeli (doğru tahmin ettiniz, bu fonksiyon çağrısını setInterval ).

Hala Alpha Vantage'ın ne olduğunu merak ediyorsanız ve kodlama kısmına geçmeden önce bunu kafanızdan atmak istiyorsanız, işte burada:

Alpha Vantage Inc. hisse senetleri, forex (FX) ve dijital/kripto para birimleri hakkında gerçek zamanlı ve geçmiş veriler için lider bir ücretsiz API sağlayıcısıdır.

Belirli bir hisse senedinin gerekli ölçümlerini almak için bu uç noktayı kullanıyor olacağız. Bu API, parametrelerden biri olarak bir API anahtarı bekler. Ücretsiz API anahtarınızı buradan alabilirsiniz. Şimdi ilginç kısma geçmekte fayda var - hadi biraz kod yazmaya başlayalım!

Bağımlılıkları Yükleme

Bir stocks-app dizini oluşturun ve bunun içinde bir server dizini oluşturun. Bunu npm init kullanarak bir düğüm projesi olarak başlatın ve ardından şu bağımlılıkları kurun:

 npm i isomorphic-fetch pg nodemon --save

Bunlar, hisse senedi fiyatlarını almak ve bunları Postgres veritabanında saklamak için bu komut dosyasını yazmamız gereken üç bağımlılık.

İşte bu bağımlılıkların kısa bir açıklaması:

  • isomorphic-fetch
    Hem istemcide hem de sunucuda eşbiçimli (aynı biçimde) fetch kullanmayı kolaylaştırır.
  • pg
    NodeJ'ler için engelleyici olmayan bir PostgreSQL istemcisidir.
  • nodemon
    Dizindeki herhangi bir dosya değişikliğinde sunucuyu otomatik olarak yeniden başlatır.

Yapılandırmayı ayarlama

Kök düzeyinde bir config.js dosyası ekleyin. Şimdilik aşağıdaki kod parçasını bu dosyaya ekleyin:

 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 yapılandırmasıyla ilgilidir. Hasura motor bölümünü kurarken bunu düzenlemek için geri geleceğiz!

Veritabanını Sorgulamak İçin Postgres Bağlantı Havuzunu Başlatma

connection pool , bilgisayar bilimlerinde yaygın olarak kullanılan bir terimdir ve bu terimi veritabanlarıyla uğraşırken sıklıkla duyarsınız.

Veritabanlarında veri sorgularken öncelikle veri tabanı ile bağlantı kurmanız gerekir. Bu bağlantı, veritabanı kimlik bilgilerini alır ve size veritabanındaki tablolardan herhangi birini sorgulamak için bir kanca verir.

Not : Veritabanı bağlantıları kurmak maliyetlidir ve ayrıca önemli kaynakları boşa harcar. Bir bağlantı havuzu, veritabanı bağlantılarını önbelleğe alır ve bunları sonraki sorgularda yeniden kullanır. Tüm açık bağlantılar kullanımdaysa, yeni bir bağlantı kurulur ve ardından havuza eklenir.

Artık bağlantı havuzunun ne olduğu ve ne için kullanıldığı açık olduğuna göre, bu uygulama için pg bağlantı havuzunun bir örneğini oluşturarak başlayalım:

pool.js dosyasını kök düzeyinde ekleyin ve şu şekilde bir havuz örneği oluşturun:

 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;

Yukarıdaki kod satırları, yapılandırma dosyasında ayarlandığı gibi yapılandırma seçenekleriyle bir Pool örneği oluşturur. Henüz yapılandırma dosyasını tamamlamadık, ancak yapılandırma seçenekleriyle ilgili herhangi bir değişiklik olmayacak.

Şimdi temelleri attık ve Alpha Vantage uç noktasına bazı API çağrıları yapmaya hazırız.

İlginç bir bite girelim!

Hisse Senedi Verilerini Alma

Bu bölümde, Alpha Vantage uç noktasından hisse senedi verilerini getireceğiz. İşte index.js dosyası:

 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); }); }); }) })()

Bu projenin amacı doğrultusunda, yalnızca bu hisse senetleri için fiyatları sorgulayacağız - NFLX (Netflix), MSFT (Microsoft), AMZN (Amazon), W (Wayfair), FB (Facebook).

Yapılandırma seçenekleri için bu dosyaya bakın. IIFE getStocksData işlevi pek bir şey yapmıyor! Bu sembollerle döngüler ve alfa vantage son noktayı sorgular ${host}query/?function=${timeSeriesFunction}&symbol=${symbol}&interval=${interval}&apikey=${key} Bu stoklar için metrikleri almak için.

insertStocksData işlevi, bu veri noktalarını Postgres veritabanına yerleştirir. İşte insertStocksData işlevi:

 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); }); };

Budur! Alpha Vantage API'sinden hissenin veri noktalarını aldık ve bunları stock_data tablosundaki Postgres veritabanına koyacak bir fonksiyon yazdık. Tüm bu işleri yapmak için sadece bir eksik parça var! Yapılandırma dosyasında doğru değerleri doldurmamız gerekiyor. Hasura motorunu kurduktan sonra bu değerleri alacağız. Hadi doğru gidelim!

Alpha Vantage uç noktasından veri noktaları alma ve bunu Hasura Postgres veri tabanına yerleştirme ile ilgili kodun tamamı için lütfen server dizinine bakın.

Bu bağlantı kurma, yapılandırma seçenekleri ve ham sorguyu kullanarak veri ekleme yaklaşımı biraz zor görünüyorsa, lütfen bunun için endişelenmeyin! Hasura motoru kurulduktan sonra tüm bunları GraphQL mutasyonuyla kolay yoldan nasıl yapacağımızı öğreneceğiz!

Hasura GraphQL Motorunu Ayarlama

Hasura motorunu kurmak ve GraphQL şemaları, sorguları, mutasyonları, abonelikleri, olay tetikleyicileri ve çok daha fazlasıyla çalışmaya başlamak gerçekten çok basit!

Hasura'yı Dene'ye tıklayın ve proje adını girin:

Hasura Projesi Oluşturma
Hasura Projesi Oluşturma. (Büyük önizleme)

Heroku'da barındırılan Postgres veritabanını kullanıyorum. Heroku'da bir veritabanı oluşturun ve onu bu projeye bağlayın. Ardından, sorgu açısından zengin Hasura konsolunun gücünü deneyimlemeye hazır olmalısınız.

Lütfen projeyi oluşturduktan sonra alacağınız postgres db URL'sini kopyalayın. Bunu yapılandırma dosyasına koymamız gerekecek.

Konsolu Başlat'a tıklayın ve bu görünüme yönlendirileceksiniz:

Hasura Konsolu
Hasura Konsolu. (Büyük önizleme)

Bu proje için ihtiyacımız olan tablo şemasını oluşturmaya başlayalım.

Postgres Veritabanında Tablo Şeması Oluşturma

Lütfen veri sekmesine gidin ve tablo ekle'ye tıklayın! Tablolardan bazılarını oluşturmaya başlayalım:

symbol tablosu

Bu tablo, sembollerin bilgilerini saklamak için kullanılacaktır. Şimdilik, burada iki alan tuttum - id ve company . Alan id birincil anahtardır ve company türünde varchar . Bu tablodaki bazı sembolleri ekleyelim:

sembol tablosu
symbol tablosu. (Büyük önizleme)

stock_data masa

stock_data tablosu id , symbol , time ve high , low , open , close , volume gibi metrikleri saklar . Bu bölümde daha önce yazdığımız NodeJs betiği, bu belirli tabloyu doldurmak için kullanılacaktır.

Tablonun nasıl göründüğü aşağıda açıklanmıştır:

stock_data tablosu
stock_data tablosu. (Büyük önizleme)

Düzenli! Gelelim veritabanı şemasındaki diğer tabloya!

user_subscription tablosu

user_subscription tablosu, abonelik nesnesini kullanıcı kimliğine karşı saklar. Bu abonelik nesnesi, kullanıcılara web-push bildirimleri göndermek için kullanılır. Bu abonelik nesnesinin nasıl üretileceğini makalede daha sonra öğreneceğiz.

Bu tabloda iki alan vardır - id uuid türünde birincil anahtardır ve abonelik alanı jsonb .

events tablosu

Bu önemlidir ve bildirim olay seçeneklerini saklamak için kullanılır. Bir kullanıcı belirli bir hisse senedinin fiyat güncellemelerini tercih ettiğinde, bu olay bilgilerini bu tabloda saklarız. Bu tablo şu sütunları içerir:

  • id : Otomatik Artırma özelliğine sahip bir birincil anahtardır.
  • symbol : bir metin alanıdır.
  • user_id : uuid .
  • trigger_type : olay tetikleme tipini depolamak için kullanılır - time/event .
  • trigger_value : tetikleyici değerini depolamak için kullanılır. Örneğin, bir kullanıcı fiyata dayalı olay tetikleyiciyi seçtiyse — hisse senedinin fiyatı 1000'e ulaştığında güncellemeler istiyorsa, trigger_value 1000 olur ve trigger_type event olur.

Bunlar, bu proje için ihtiyacımız olan tüm tablolar. Ayrıca sorunsuz bir veri akışı ve bağlantılara sahip olmak için bu tablolar arasında ilişkiler kurmamız gerekiyor. Hadi bunu yapalım!

Tablolar arasında ilişki kurma

events tablosu, olay değerine göre web-push bildirimleri göndermek için kullanılır. Böylece, bu tabloyu, bu tabloda depolanan aboneliklere push bildirimleri gönderebilmek için bu tabloyu user_subscription bağlamak mantıklıdır.

 events.user_id → user_subscription.id

stock_data tablosu, semboller tablosu ile ilgilidir:

 stock_data.symbol → symbol.id

Ayrıca symbol tablosunda aşağıdaki gibi bazı ilişkiler kurmamız gerekiyor:

 stock_data.symbol → symbol.id events.symbol → symbol.id

Şimdi gerekli masaları oluşturduk ve ayrıca onların arasındaki ilişkileri kurduk! Sihri görmek için konsoldaki GRAPHIQL sekmesine geçelim!

Hasura, şu tablolara dayalı olarak GraphQL sorgularını zaten kurmuştur:

Hasura konsolunda GraphQL Sorguları/Mutasyonları
HASURE konsolunda GRAPHQL sorguları / mutasyonları. (Büyük önizleme)

Bu tablolarda sorgulamak açıkça basittir ve istenen verileri elde etmek için bu filtrelerin / özelliklerden ( distinct_on , limit , offset , order_by , where ) uygulamasını da uygulayabilirsiniz.

Bunların hepsi iyi görünüyor ama hala sunucu tarafı kodumuzu Hasura konsoluna bağlamadık. Hadi o kısmı tamamlayalım!

NodeJs Komut Dosyasını Postgres Veritabanına Bağlama

Lütfen gerekli seçenekleri server dizinindeki config.js dosyasına şu şekilde yerleştirin:

 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;

Lütfen bu seçenekleri Heroku'da Postgres veritabanını oluşturduğumuzda oluşturulan veritabanı dizesinden koyun.

apiHostOptions , host , key , timeSeriesFunction ve interval gibi API ile ilgili seçeneklerden oluşur.

graphqlURL alanını Hasura konsolundaki GRAPHIQL sekmesinde alırsınız.

getConfig işlevi, istenen değeri CONFIG nesnesinden iade etmek için kullanılır. Bunu zaten server dizinindeki index.js kullandık.

Sunucuyu çalıştırmanın ve veritabanındaki bazı verileri doldurmanın zamanı geldi. package.json şu şekilde bir komut dosyası ekledim:

 "scripts": { "start": "nodemon index.js" }

Terminalde npm start ve index.js içindeki semboller dizisinin veri noktaları tablolarda doldurulmalıdır.

NodeJs Komut Dosyasındaki Ham Sorguyu GraphQL Mutasyonuna Yeniden Düzenleme

Şimdi Hasura motoru kurulduğuna göre, stock_data tablosunda bir mutasyon çağırmanın ne kadar kolay olabileceğini görelim.

queries.js içindeki insertStocksData işlevi ham bir sorgu kullanır:

 const query = 'INSERT INTO stock_data (symbol, high, low, open, close, volume, time) VALUES ($1, $2, $3, $4, $5, $6, $7)';

Hadi bu sorguyu değiştirelim ve Hasura Motoru tarafından desteklenen mutasyonu kullanalım. İşte sunucu dizininde refactored 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 }

Lütfen Dikkat: graphqlURL dosyasına config.js eklemeliyiz.

apollo-fetch modülü, GraphQL uç noktasındaki tarihi sorgulamak/değiştirmek için kullanılabilecek bir getirme işlevi döndürür. Yeterince kolay, değil mi?

insertStocksData index.js gerektirdiği şekilde biçiminde iade etmektir. Lütfen bu yaklaşımla birlikte kodun tamamı için index2.js ve queries2.js kontrol edin.

Şimdi projenin veri tarafını gerçekleştirdik, hadi ön uç biti üzerine hareket edelim ve bazı ilginç bileşenler oluşturalım!

Not : Bu yaklaşımla veritabanı yapılandırma seçeneklerini saklamak zorunda değiliz!

React ve Apollo İstemcisini Kullanan Ön Uç

Ön uç projesi aynı depodadır ve create-react-app paketi kullanılarak oluşturulur. Bu paket kullanılarak üretilen servis işçisi, önbellekleme varlıklarını destekler ancak daha fazla özelleştirmenin servis çalışanı dosyasına eklenmesine izin vermez. Özel servis çalışanı seçeneklerine destek eklemek için zaten açık konular var. Bu problemden kurtulmanın yolları var ve özel bir servis çalışanı için destek ekleyin.

Ön uç projesinin yapısına bakarak başlayalım:

Proje Dizini
Proje Dizini. (Büyük önizleme)

Lütfen src dizinini kontrol edin! Servis çalışanı ile ilgili dosyalar için şimdilik endişelenmeyin. Bu bölümde daha sonra bu dosyalar hakkında daha fazla bilgi edineceğiz. Proje yapısının geri kalanı basit görünüyor. components klasörü bileşenleri içerecektir (Yükleyici, Grafik); services klasörü, gerekli yapıdaki nesneleri dönüştürmek için kullanılan bazı yardımcı işlevleri/hizmetleri içerir; adından da anlaşılacağı gibi styles , projeyi şekillendirmek için kullanılan sass dosyalarını içerir; views ana dizindir ve görünüm katmanı bileşenlerini içerir.

Bu proje için sadece iki görünüm bileşenine ihtiyacımız var - Sembol Listesi ve Sembol Zaman Serisi. Highcharts kitaplığındaki Chart bileşenini kullanarak zaman serisini oluşturacağız. Ön uçtaki parçaları oluşturmak için bu dosyalara kod eklemeye başlayalım!

Bağımlılıkları Yükleme

İşte ihtiyacımız olan bağımlılıkların listesi:

  • apollo-boost
    Apollo boost, Apollo İstemcisini kullanmaya başlamanın sıfır yapılandırmalı bir yoludur. Varsayılan yapılandırma seçenekleri ile birlikte gelir.
  • reactstrap ve bootstrap
    Bileşenler bu iki paket kullanılarak oluşturulur.
  • graphql ve graphql-type-json
    graphql , WRAPHQL şemasında kullanılan json veri türünü desteklemek için apollo-boost ve graphql-type-json kullanmak için gerekli bir bağımlılıktır.
  • highcharts ve highcharts-react-official
    Ve bu iki paket, grafiği oluşturmak için kullanılacaktır:

  • node-sass
    Bu, stil için sass dosyalarını desteklemek için eklenmiştir.

  • uuid
    Bu paket, güçlü rastgele değerler üretmek için kullanılır.

Tüm bu bağımlılıklar, onları projede kullanmaya başladığımızda anlam kazanacaktır. Bir sonraki kısma geçelim!

Apollo İstemcisini Ayarlama

src klasörünün içindeki bir apolloClient.js oluşturun:

 import ApolloClient from 'apollo-boost'; const apolloClient = new ApolloClient({ uri: '<HASURA_CONSOLE_URL>' }); export default apolloClient;

Yukarıdaki kod, ApolloClient'i başlatır ve yapılandırma seçeneklerinde uri alır. uri , Hasura konsolunuzun URL'sidir. Bu URI alanını uri Endpoint bölümündeki GRAPHIQL sekmesinde alacaksınız.

Yukarıdaki kod basit görünüyor ama projenin ana kısmına bakıyor! HASURE üzerine inşa edilen GRAPHQL şemasını mevcut proje ile bağlar.

Ayrıca bu apollo istemci nesnesini ApolloProvider ve kök bileşeni ApolloProvider içine sarmalıyız. Bu, ana bileşenin içindeki tüm iç içe bileşenlerin bu istemci nesnesinde client desteği ve yangın sorgularını kullanmasını sağlayacaktır.

index.js dosyasını şu şekilde değiştirelim:

 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') );

Lütfen insertSubscription ile ilgili kodu yok sayınız. Bunu daha sonra ayrıntılı olarak anlayacağız. Kodun geri kalanı dolaşmak için basit olmalıdır. render işlevi, kök bileşenini ve ElementID'i parametre olarak alır. Bildirim client (ApolloClient örneği), bir destek olarak ApolloProvider . index.js dosyasının tamamını buradan kontrol edebilirsiniz.

Özel Hizmet Çalışanını Ayarlama

Bir servis işçisi, ağ isteklerini engelleme yeteneğine sahip bir javascript dosyasıdır. Önbelleğin sorunu sorgulamak için, istenen varlığın sunucuya bir yolculuk yapmak yerine önbelleğe daha önce mevcut olup olmadığını kontrol etmek için kullanılır. Hizmet çalışanları, abone olunan cihazlara web-push bildirimleri göndermek için de kullanılır.

Hisse senedi fiyat güncellemeleri için abone olan kullanıcılara web-push bildirimleri göndermeliyiz. Zemini ayarlayalım ve bu servis çalışanı dosyasını oluşturalım!

insertSubscription dosyasında tutturulmuş ekli iskelet, servis işçisini kaydetme ve abonelik nesnesini subscriptionMutation index.js kullanarak veritabanına koyma çalışmasını yapıyor.

Lütfen projede kullanılan tüm sorgular ve mutasyonlar için querlies.js bakın.

serviceWorker.register(insertSubscription); serviceWorker.js dosyasında yazılan register işlevini çağırır. İşte burada:

 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(); }) } }

Yukarıdaki işlev, önce servisçinin tarayıcı tarafından desteklenip desteklenmediğini kontrol eder ve ardından URL serviceWorker barındırılan servis çalışanı dosyasını swUrl . Bu dosyayı birazdan kontrol edeceğiz!

getSubscription işlevi, Abonelik Nesnesini subscribe pushManager nesnesindeki Abone Oluşturma'yı kullanma çalışmasını yapar. Bu abonelik nesnesi daha sonra user_subscription tablosunda bir kullanıcı kimliğine karşı saklanır. Lütfen uuid işlevi kullanılarak oluşturulduğunu unutmayın. getSubscription fonksiyonunu kontrol edelim:

 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 })); }) } }) }

Kodun tamamı için serviceWorker.js dosyasını kontrol edebilirsiniz!

Bildirim açılır
Bildirim Açılır Penceresi. (Büyük önizleme)

Notification.requestPermission() , kullanıcıdan bildirim göndermek için izin isteyen bu açılır pencereyi çağırdı. Kullanıcı izin verdikten sonra, bir abonelik nesnesi itme hizmeti tarafından oluşturulur. Bu nesneyi yerel gün içinde saklıyoruz:

Webpush Abonelikleri nesnesi
Webpush Abonelikleri nesnesi. (Büyük önizleme)

Yukarıdaki nesnedeki alan endpoint , cihazı tanımlamak için kullanılır ve sunucu, kullanıcı için Web Push bildirimleri göndermek için bu son noktayı kullanır.

Servis çalışanını başlatma ve kaydetme işini yaptık. Ayrıca kullanıcının abonelik nesnesine sahibiz! Bu, public klasörde bulunan serviceWorker.js dosyası nedeniyle bu kadar iyi çalışıyor. Şimdi işyerinde işleri hazırlamak için hazırlayalım!

Bu biraz zor bir konu ama doğru anlayalım! Daha önce belirtildiği gibi, create-react-app yardımcı programı hizmet çalışanı için varsayılan olarak özelleştirmeleri desteklemez. workbox-build modülünü kullanarak müşteri hizmetleri çalışanı uygulamasını gerçekleştirebiliriz.

Ayrıca, önbelleğe alma dosyalarının varsayılan davranışının sağlam olduğundan emin olmalıyız. Servis çalışanının projede inşa edildiği kısmı değiştireceğiz. Ve workbox-build tam olarak bunu başarmaya yardımcı olur! Düzgün şeyler! Bunu basit tutalım ve özel hizmet çalışanının çalışması için yapmamız gereken her şeyi listeleyelim:

  • workboxBuild kullanarak varlıkların ön önbelleğe alınmasını gerçekleştirin.
  • Varlıkları önbelleğe almak için bir hizmet çalışanı şablonu oluşturun.
  • Özel yapılandırma seçenekleri sağlamak için sw-precache-config.js dosyası oluşturun.
  • derleme hizmeti çalışanı komut dosyasını package.json içindeki derleme adımına ekleyin.

Bütün bunlar kafa karıştırıcı geliyorsa endişelenmeyin! Makale, bu noktaların her birinin arkasındaki anlambilimi açıklamaya odaklanmıyor. Şimdilik uygulama bölümüne odaklanmalıyız! Özel bir hizmet çalışanı yapmak için tüm çalışmaları yapmanın ardındaki mantığı başka bir makalede ele almaya çalışacağım.

src dizininde sw-build.js ve sw-custom.js olmak üzere iki dosya oluşturalım. Lütfen bu dosyalara bağlantılara bakın ve kodu projenize ekleyin.

Şimdi sw-precache-config.js dosyasını root seviyesinde oluşturalım ve o dosyaya aşağıdaki kodu ekleyelim:

 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' }] }

Özel hizmet çalışanı dosyasını oluşturmaya yer açmak için package.json dosyasını da değiştirelim:

Bu ifadeleri scripts bölümüne ekleyin:

 "build-sw": "node ./src/sw-build.js", "clean-cra-sw": "rm -f build/precache-manifest.*.js && rm -f build/service-worker.js",

Ve build komut dosyasını şu şekilde değiştirin:

 "build": "react-scripts build && npm run build-sw && npm run clean-cra-sw",

Kurulum sonunda tamamlandı! Şimdi public klasörün içine özel bir hizmet çalışanı dosyası eklememiz gerekiyor:

 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)); })

Sunucu tarafından gönderilen itme bildirimlerini dinlemek için bir push dinleyicisi ekledik. showNotification işlevi, Web Push bildirimlerini kullanıcıya görüntülemek için kullanılır.

Budur! Web push bildirimlerini işlemek için özel bir hizmet çalışanı ayarlamanın tüm zor işini bitirdik. Kullanıcı arayüzlerini oluşturduğumuzda bu bildirimleri eylemde göreceğiz!

Ana kod parçalarını inşa etmek için yaklaşıyoruz. Şimdi ilk görünümle başlayalım!

Sembol Listesi Görünümü

Önceki bölümde kullanılan App bileşeni şuna benzer:

 import React from 'react'; import SymbolList from './views/symbolList'; const App = () => { return <SymbolList />; }; export default App;

SymbolList görünümünü döndüren basit bir bileşendir ve SymbolList , sergilenen tüm ağır kaldırma sembollerini düzgünce bağlı bir kullanıcı arayüzüne yapar.

views klasörünün içindeki symbolList.js bakalım:

Lütfen buradaki dosyaya bakın!

Bileşen, renderSymbols işlevinin sonuçlarını döndürür. Ve, bu veriler, Kullanım Kancası kullanarak veritabanından useQuery :

 const { loading, error, data } = useQuery(symbolsQuery, {variables: { userId }});

symbolsQuery şu şekilde tanımlanır:

 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 } } } } } `;

Kullanıcı userId alır ve bu belirli kullanıcının abone olan olaylarını, bildirim simgesinin doğru durumunu (başlıkla birlikte görüntülenen Bell simgesi) görüntülemek için alınır. Sorgu ayrıca stoğun maksimum ve minimum değerlerini de getirir. Yukarıdaki sorguda aggregate kullanımına dikkat edin. Hasura'nın Toplama sorguları, count , sum , avg , max , min vb. toplu değerleri getirmek için arka planda çalışır.

Yukarıdaki GraphQL çağrısından alınan yanıta göre, ön uçta görüntülenen kartların listesi:

Stok Kartları
Stok Kartları. (Büyük önizleme)

Kart HTML yapısı böyle bir şey görünüyor:

 <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>

Bu kartları işlemek için ReactStrap'in Card bileşenini kullanıyoruz. Popover bileşeni, abonelik tabanlı seçenekleri görüntülemek için kullanılır:

Bildirim Seçenekleri
Bildirim Seçenekleri. (Büyük önizleme)

Kullanıcı belirli bir stok için bell simgesini tıkladığında, her saate haber verilecek şekilde veya stokun fiyatının girilen değere ulaştığında öğütülebilir. Bunu Olaylar/Zaman Tetikleyicileri bölümünde çalışırken göreceğiz.

Not : Bir sonraki bölümde StockTimeseries bileşenine geleceğiz!

Lütfen stoklar listesi bileşeniyle ilgili tüm kod için symbolList.js bakın.

Hisse Senedi Zaman Serisi Görünümü

StockTimeseries bileşeni stocksDataQuery sorgusunu kullanır:

 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 } } `;

Yukarıdaki sorgu, seçilen hisse senedinin son 25 veri noktasını getirir. Örneğin, Facebook hisse senedi açık metriği için grafik:

Hisse Senedi Fiyatları zaman çizelgesi
Hisse Senedi Fiyatları zaman çizelgesi. (Büyük önizleme)

Bu, bazı grafik seçeneklerini [ HighchartsReact ] bileşenine geçirdiğimiz basit bir bileşendir. İşte grafik seçenekleri:

 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 ekseni zamanı gösterir ve Y ekseni o zamandaki metrik değeri gösterir. getDataPoints işlevi, seri her biri için bir dizi nokta oluşturmak için kullanılır.

 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; }

Basit! Grafik bileşeni bu şekilde oluşturulur! Hisse senedi zaman serilerindeki tam kod için lütfen Chart.js ve stockTimeseries.js dosyalarına bakın.

Artık projenin veri ve kullanıcı arayüzleri kısmıyla hazır olmalısınız. Şimdi ilginç kısma geçelim - kullanıcının girdisine göre olay/zaman tetikleyicilerini ayarlama.

Olay/Zamanlanmış Tetikleyicileri Ayarlama

Bu bölümde, Hasura Konsolu'ndaki tetikleyicileri nasıl oluşturacağını ve seçilen kullanıcılara web push bildirimlerinin nasıl gönderileceğini öğreneceğiz. Başlayalım!

Hasura Konsolunda Olayları Tetikliyor

insert tablosunda bir event trigger stock_value oluşturalım ve tetikleme işlemi olarak stock_data . Web kancası, stock_data tablosunda her ekleme olduğunda çalışır.

Olay tetikleyicileri kurulumu
Olay kurulumu tetikler. (Büyük önizleme)

Web kancası URL'si için bir aksaklık projesi oluşturacağız. Kolay anlaşılır hale getirmek için web kancaları hakkında biraz bilgi vereyim:

Web kancaları, belirli bir olayın meydana gelmesiyle ilgili olarak bir uygulamadan diğerine veri göndermek için kullanılır. Bir olay tetiklendiğinde, yük olarak olay verileriyle web kancası URL'sine bir HTTP POST çağrısı yapılır.

In this case, when there is an insert operation on the stock_data table, an HTTP post call will be made to the configured webhook URL (post call in the glitch project).

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!

Abone Olunan Kullanıcılar Alınıyor

 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()); }

Yukarıdaki handleStockValueTrigger işlevinde, önce abone olan kullanıcıları getSubscribedUsers işlevini kullanarak getiriyoruz. Ardından, bu kullanıcıların her birine web push bildirimleri gönderiyoruz. sendWebpush işlevi bildirim göndermek için kullanılır. Birazdan web-push uygulamasına bakacağız.

getSubscribedUsers işlevi şu sorguyu kullanır:

 query getSubscribedUsers($symbol: String, $triggerValue: numeric) { events(where: {symbol: {_eq: $symbol}, trigger_type: {_eq: "event"}, trigger_value: {_gte: $triggerValue}}) { user_id user_subscription { subscription } } }

Bu sorgu, hisse senedi sembolünü ve değeri alır ve bu koşullarla eşleşen user-id ve user_subscription dahil olmak üzere kullanıcı ayrıntılarını getirir:

  • yükte geçirilene eşit symbol .
  • trigger_type , event eşittir.
  • trigger_value , bu işleve iletilen değerden büyük veya ona eşittir (bu durumda close ).

Kullanıcıların listesini aldığımızda, geriye kalan tek şey onlara web-push bildirimleri göndermek! Bunu hemen yapalım!

Abone Olunan Kullanıcılara Web-Push Bildirimleri Gönderme

Web-push bildirimleri göndermek için önce genel ve özel VAPID anahtarlarını almalıyız. Lütfen bu anahtarları .env dosyasında saklayın ve bu ayrıntıları index.js şu şekilde ayarlayın:

 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 işlevi, ilk parametre olarak sağlanan abonelik uç noktasında web push'u göndermek için kullanılır.

Abone olanlara web-push bildirimlerini başarılı bir şekilde göndermek için hepsi bu kadar. İşte index.js tanımlanan kodun tamamı:

 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"); });

Bir değere sahip hisse senedine abone olarak ve bu değeri tabloya manuel olarak ekleyerek (test için) bu akışı test edelim!

2000 değeriyle AMZN üye oldum ve ardından tabloya bu değerle bir veri noktası ekledim. Hisse senedi bildirim uygulaması, eklemeden hemen sonra beni şu şekilde bilgilendirdi:

Test için stock_data tablosuna bir satır ekleme
Test için stock_data tablosuna bir satır ekleme. (Büyük önizleme)

Düzenli! Ayrıca olay çağırma günlüğünü buradan kontrol edebilirsiniz:

Olay günlüğü
Olay günlüğü. (Büyük önizleme)

Web kancası işi beklendiği gibi yapıyor! Artık etkinlik tetikleyicileri için hazırız!

Zamanlanmış/Cron Tetikleyicileri

Cron olay tetikleyicisini kullanarak abone kullanıcılarına her saat başı bilgilendirmek için zamana dayalı bir tetikleyici elde edebiliriz:

Cron/Zamanlanmış Tetikleyici kurulumu
Cron/Zamanlanmış Tetikleyici kurulumu. (Büyük önizleme)

Aynı web kancası URL'sini kullanabilir ve abone olan kullanıcıları, stock_price_time_based_trigger olarak tetikleyici olay türüne göre işleyebiliriz. Uygulama, olay tabanlı tetikleyiciye benzer.

Çözüm

Bu yazımızda bir hisse senedi fiyat bildirici uygulaması oluşturduk. Alpha Vantage API'lerini kullanarak fiyatların nasıl alınacağını ve veri noktalarını Hasura destekli Postgres veritabanında nasıl depolayacağımızı öğrendik. Ayrıca Hasura GraphQL motorunun nasıl kurulacağını ve olay tabanlı ve zamanlanmış tetikleyicilerin nasıl oluşturulacağını da öğrendik. Abone olan kullanıcılara web-push bildirimleri göndermek için bir aksaklık projesi oluşturduk.