إنشاء تطبيق لتنبيه أسعار الأسهم باستخدام React و Apollo GraphQL و Hasura
نشرت: 2022-03-10أصبح مفهوم تلقي الإشعارات عند وقوع الحدث الذي تختاره شائعًا مقارنة بالالتصاق بالتيار المستمر للبيانات للعثور على هذا الحدث المعين بنفسك. يفضل الأشخاص الحصول على رسائل بريد إلكتروني / رسائل ذات صلة عند وقوع الحدث المفضل لديهم بدلاً من تعليقهم على الشاشة لانتظار حدوث هذا الحدث. المصطلحات المستندة إلى الأحداث شائعة أيضًا في عالم البرامج.
ما مدى روعة ذلك إذا تمكنت من الحصول على تحديثات سعر الأسهم المفضلة لديك على هاتفك؟
في هذه المقالة ، سننشئ تطبيق تنبيه أسعار الأسهم باستخدام محرك React و Apollo GraphQL و Hasura GraphQL. سنبدأ المشروع من كود create-react-app
معياري وسنقوم ببناء كل شيء على الأرض. سنتعلم كيفية إعداد جداول قاعدة البيانات والأحداث على وحدة تحكم Hasura. سنتعلم أيضًا كيفية توصيل أحداث Hasura للحصول على تحديثات أسعار الأسهم باستخدام إشعارات الدفع عبر الويب.
إليك نظرة سريعة على ما سنبنيه:
هيا بنا نذهب!
نظرة عامة حول ماهية هذا المشروع
سيتم تخزين بيانات الأسهم (بما في ذلك المقاييس مثل عالية ، ومنخفضة ، ومفتوحة ، وإغلاق ، والحجم ) في قاعدة بيانات Postgres المدعومة من Hasura. سيكون المستخدم قادرًا على الاشتراك في سهم معين بناءً على بعض القيمة أو يمكنه اختيار تلقي إشعار كل ساعة. سيتلقى المستخدم إشعارًا عبر الويب بمجرد استيفاء معايير الاشتراك الخاصة به.
يبدو هذا وكأنه الكثير من الأشياء ومن الواضح أنه سيكون هناك بعض الأسئلة المفتوحة حول كيفية بناء هذه القطع.
إليك خطة حول كيفية إنجاز هذا المشروع في أربع خطوات:
- إحضار بيانات المخزونات باستخدام برنامج نصي NodeJs
سنبدأ بجلب بيانات المخزون باستخدام برنامج نصي بسيط NodeJs من أحد مزودي واجهة برمجة تطبيقات الأسهم - 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. هي شركة رائدة في توفير واجهات برمجة التطبيقات المجانية للبيانات الواقعية والتاريخية عن الأسهم والفوركس (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
مع خيارات التكوين كما تم تعيينها في ملف التكوين. لم ننتهي بعد من ملف التكوين ولكن لن تكون هناك أية تغييرات تتعلق بخيارات التكوين.
لقد وضعنا الأرضية الآن ومستعدون لبدء إجراء بعض استدعاءات واجهة برمجة التطبيقات لنقطة نهاية 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).
راجع هذا الملف للتعرف على خيارات التكوين. وظيفة getStocksData
لا تفعل الكثير! وهي تدور عبر هذه الرموز وتستعلم عن نقطة نهاية Alpha Vantage ${host}query/?function=${timeSeriesFunction}&symbol=${symbol}&interval=${interval}&apikey=${key}
للحصول على مقاييس هذه الأسهم.
تضع وظيفة insertStocksData
نقاط البيانات هذه في قاعدة بيانات Postgres. ها هي وظيفة insertStocksData
:
const insertStocksData = async (payload) => { const query = 'INSERT INTO stock_data (symbol, high, low, open, close, volume, time) VALUES ($1, $2, $3, $4, $5, $6, $7)'; pool.query(query, payload, (err, result) => { console.log('result here', err); }); };
هذه هي! لقد جلبنا نقاط بيانات المخزون من واجهة برمجة تطبيقات Alpha Vantage وقمنا بكتابة وظيفة لوضعها في قاعدة بيانات Postgres في جدول stock_data
. هناك قطعة واحدة مفقودة لجعل كل هذا يعمل! يجب علينا ملء القيم الصحيحة في ملف التكوين. سنحصل على هذه القيم بعد إعداد محرك Hasura. دعنا نصل إلى ذلك على الفور!
يرجى الرجوع إلى دليل server
للحصول على الكود الكامل لجلب نقاط البيانات من نقطة نهاية Alpha Vantage وتعبئة ذلك في قاعدة بيانات Hasura Postgres.
إذا كانت طريقة إعداد الاتصالات وخيارات التكوين وإدخال البيانات باستخدام الاستعلام الأولي تبدو صعبة بعض الشيء ، فالرجاء عدم القلق بشأن ذلك! سنتعلم كيفية القيام بكل هذا بالطريقة السهلة باستخدام طفرة GraphQL بمجرد إعداد محرك Hasura!
إعداد محرك Hasura GraphQL
من السهل حقًا إعداد محرك Hasura والتشغيل مع مخططات GraphQL والاستعلامات والطفرات والاشتراكات ومحفزات الأحداث وغير ذلك الكثير!
انقر فوق Try Hasura وأدخل اسم المشروع:
أنا أستخدم قاعدة بيانات Postgres المستضافة على Heroku. قم بإنشاء قاعدة بيانات على Heroku واربطها بهذا المشروع. يجب أن تكون جاهزًا لتجربة قوة وحدة تحكم Hasura الغنية بالاستعلامات.
يرجى نسخ عنوان URL الخاص بـ Postgres DB الذي ستحصل عليه بعد إنشاء المشروع. سيتعين علينا وضع هذا في ملف التكوين.
انقر فوق Launch Console وستتم إعادة توجيهك إلى طريقة العرض هذه:
لنبدأ في بناء مخطط الجدول الذي سنحتاجه لهذا المشروع.
إنشاء مخطط جداول في قاعدة بيانات Postgres
الرجاء الانتقال إلى علامة التبويب "البيانات" والنقر فوق "إضافة جدول"! لنبدأ في إنشاء بعض الجداول:
جدول symbol
يستخدم هذا الجدول لتخزين معلومات الرموز. في الوقت الحالي ، احتفظت بحقلين هنا - id
company
. id
الحقل هو مفتاح أساسي company
من النوع varchar
. دعنا نضيف بعض الرموز في هذا الجدول:
جدول stock_data
يخزن جدول stock_data
id
symbol
time
والمقاييس مثل volume
high
low
open
close
. سيتم استخدام البرنامج النصي NodeJs الذي كتبناه سابقًا في هذا القسم لملء هذا الجدول المحدد.
إليك كيف يبدو الجدول:
مرتب! دعنا ننتقل إلى الجدول الآخر في مخطط قاعدة البيانات!
جدول user_subscription
يخزن جدول user_subscription
كائن الاشتراك مقابل معرف المستخدم. يتم استخدام كائن الاشتراك هذا لإرسال إشعارات الدفع عبر الويب إلى المستخدمين. سنتعلم لاحقًا في المقالة كيفية إنشاء كائن الاشتراك هذا.
يوجد حقلين في هذا الجدول - id
هو المفتاح الأساسي لنوع uuid
وحقل الاشتراك من النوع jsonb
.
جدول events
هذا هو المهم ويستخدم لتخزين خيارات حدث الإخطار. عندما يختار المستخدم تحديثات الأسعار لسهم معين ، نقوم بتخزين معلومات هذا الحدث في هذا الجدول. يحتوي هذا الجدول على هذه الأعمدة:
-
id
: هو مفتاح أساسي بخاصية الزيادة التلقائية. -
symbol
: حقل نصي. -
user_id
: من النوعuuid
. - نوع
trigger_type
: يستخدم لتخزين نوع مشغل الحدث -time/event
. -
trigger_value
: يستخدم لتخزين قيمة المشغل. على سبيل المثال ، إذا اختار المستخدم مشغل الحدث المستند إلى السعر - فهو يريد تحديثات إذا وصل سعر السهم إلى 1000 ، فإن قيمةtrigger_value
ستكون 1000 ويكون نوعtrigger_type
هوevent
.
هذه هي كل الجداول التي نحتاجها لهذا المشروع. يتعين علينا أيضًا إعداد العلاقات بين هذه الجداول للحصول على تدفق سلس للبيانات والاتصالات. لنفعل ذلك!
إقامة العلاقات بين الجداول
يتم استخدام جدول events
لإرسال إشعارات الدفع عبر الويب بناءً على قيمة الحدث. لذلك ، من المنطقي ربط هذا الجدول بجدول user_subscription
لتتمكن من إرسال إشعارات فورية بشأن الاشتراكات المخزنة في هذا الجدول.
events.user_id → user_subscription.id
يرتبط جدول stock_data
بجدول الرموز على النحو التالي:
stock_data.symbol → symbol.id
يتعين علينا أيضًا إنشاء بعض العلاقات على جدول symbol
على النحو التالي:
stock_data.symbol → symbol.id events.symbol → symbol.id
لقد أنشأنا الآن الجداول المطلوبة وأقمنا العلاقات فيما بينها! دعنا ننتقل إلى علامة التبويب GRAPHIQL
في وحدة التحكم لرؤية السحر!
قام Hasura بالفعل بإعداد استعلامات GraphQL بناءً على هذه الجداول:
من السهل ببساطة الاستعلام عن هذه الجداول ويمكنك أيضًا تطبيق أي من هذه المرشحات / الخصائص ( distinct_on
، limit
، offset
، order_by
، where
) للحصول على البيانات المطلوبة.
يبدو كل هذا جيدًا ولكننا ما زلنا لم نقم بتوصيل رمز جانب الخادم الخاص بنا بوحدة التحكم Hasura. دعونا نكمل هذا الجزء!
ربط البرنامج النصي NodeJs بقاعدة بيانات Postgres
يرجى وضع الخيارات المطلوبة في ملف 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
من الخيارات ذات الصلة بواجهة برمجة التطبيقات مثل 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
. يدعم عامل الخدمة الذي تم إنشاؤه باستخدام هذه الحزمة التخزين المؤقت للأصول ولكنه لا يسمح بإضافة المزيد من التخصيصات إلى ملف عامل الخدمة. توجد بالفعل بعض المشكلات المفتوحة لإضافة دعم لخيارات عامل الخدمة المخصص. هناك طرق للتخلص من هذه المشكلة وإضافة دعم لعامل خدمة مخصص.
لنبدأ بإلقاء نظرة على هيكل مشروع الواجهة الأمامية:
يرجى التحقق من دليل src
! لا تقلق بشأن الملفات المتعلقة بعامل الخدمة في الوقت الحالي. سنتعرف على المزيد حول هذه الملفات لاحقًا في هذا القسم. يبدو باقي هيكل المشروع بسيطًا. سيحتوي مجلد components
على المكونات (Loader ، Chart) ؛ يحتوي مجلد services
على بعض الوظائف / الخدمات المساعدة المستخدمة لتحويل الكائنات في الهيكل المطلوب ؛ styles
كما يوحي الاسم تحتوي على ملفات sass المستخدمة لتصميم المشروع ؛ views
هو الدليل الرئيسي ويحتوي على مكونات طبقة العرض.
سنحتاج فقط إلى عنصرين لعرض هذا المشروع - قائمة الرموز وسلسلة الرموز. سنقوم ببناء السلاسل الزمنية باستخدام مكون الرسم البياني من مكتبة Highcharts. لنبدأ في إضافة التعليمات البرمجية في هذه الملفات لبناء الأجزاء على الواجهة الأمامية!
تثبيت التبعيات
فيما يلي قائمة التبعيات التي سنحتاجها:
-
apollo-boost
يعد Apollo Boost طريقة تكوين صفري لبدء استخدام عميل Apollo. يأتي مرفقًا مع خيارات التكوين الافتراضية. -
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 .
يبدو الكود أعلاه بسيطًا ولكنه يعتني بالجزء الرئيسي من المشروع! يربط مخطط 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
مكون الجذر ومعرّف العنصر كمعلمات. يتم تمرير client
الإشعار (مثيل ApolloClient) كخاصية إلى ApolloProvider
. يمكنك التحقق من ملف index.js
الكامل هنا.
إعداد عامل الخدمة المخصصة
عامل الخدمة هو ملف جافا سكريبت لديه القدرة على اعتراض طلبات الشبكة. يتم استخدامه للاستعلام عن ذاكرة التخزين المؤقت للتحقق مما إذا كان الأصل المطلوب موجودًا بالفعل في ذاكرة التخزين المؤقت بدلاً من الانتقال إلى الخادم. يتم استخدام عمال الخدمة أيضًا لإرسال إشعارات الدفع عبر الويب إلى الأجهزة المشتركة.
يتعين علينا إرسال إشعارات الدفع عبر الويب لتحديثات أسعار الأسهم للمستخدمين المشتركين. دعونا نضع الأساس ونبني ملف عامل الخدمة هذا!
يقوم insertSubscription
تم قصه في ملف index.js
بعمل تسجيل عامل الخدمة ووضع كائن الاشتراك في قاعدة البيانات باستخدام ميزة subscriptionMutation
.
يرجى الرجوع إلى queries.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()
باستدعاء هذه النافذة المنبثقة التي تطلب من المستخدم الإذن بإرسال الإخطارات. بمجرد أن ينقر المستخدم على السماح ، يتم إنشاء كائن اشتراك بواسطة خدمة الدفع. نقوم بتخزين هذا الكائن في LocalStorage على النحو التالي:
تُستخدم endpoint
الحقل في الكائن أعلاه لتحديد الجهاز ويستخدم الخادم نقطة النهاية هذه لإرسال إشعارات الدفع عبر الويب إلى المستخدم.
لقد قمنا بأعمال تهيئة وتسجيل عامل الخدمة. لدينا أيضا موضوع الاشتراك للمستخدم! يعمل هذا بشكل جيد بسبب ملف serviceWorker.js
الموجود في المجلد public
. لنقم الآن بإعداد عامل الخدمة لتجهيز الأشياء!
هذا موضوع صعب بعض الشيء لكن دعونا نفهمه بشكل صحيح! كما ذكرنا سابقًا ، لا تدعم أداة create-react-app
المساعدة التخصيصات افتراضيًا لعامل الخدمة. يمكننا تحقيق تنفيذ عامل خدمة العملاء باستخدام وحدة workbox-build
.
يتعين علينا أيضًا التأكد من أن السلوك الافتراضي لملفات التخزين المؤقت المسبق سليم. سنقوم بتعديل الجزء حيث يتم بناء عامل الخدمة في المشروع. و workbox-build يساعد في تحقيق ذلك بالضبط! الأشياء الرائعة! دعنا نجعل الأمر بسيطًا ونقوم بإدراج كل ما يتعين علينا القيام به لجعل عامل الخدمة المخصصة يعمل:
- تعامل مع التخزين المؤقت المسبق للأصول باستخدام
workboxBuild
. - قم بإنشاء قالب عامل خدمة لتخزين الأصول مؤقتًا.
- قم بإنشاء ملف
sw-precache-config.js
لتوفير خيارات التكوين المخصصة. - أضف النص البرمجي لعامل خدمة البناء في خطوة الإنشاء في
package.json
.
لا تقلق إذا كان كل هذا يبدو محيرا! لا تركز المقالة على شرح الدلالات الكامنة وراء كل نقطة من هذه النقاط. علينا التركيز على جزء التنفيذ في الوقت الحالي! سأحاول تغطية السبب وراء القيام بكل العمل لتكوين عامل خدمة مخصص في مقال آخر.
لنقم بإنشاء ملفين sw-build.js
و sw-custom.js
-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
الدفع التي يرسلها الخادم. تُستخدم وظيفة showNotification
لعرض إشعارات الدفع على الويب للمستخدم.
هذه هي! لقد انتهينا من كل العمل الشاق المتمثل في إعداد عامل خدمة مخصص للتعامل مع إشعارات الدفع عبر الويب. سنرى هذه الإخطارات قيد التنفيذ بمجرد أن نبني واجهات المستخدم!
نحن نقترب من بناء أجزاء الكود الرئيسية. لنبدأ الآن بالمنظر الأول!
عرض قائمة الرموز
يبدو مكون 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
في الاستعلام أعلاه. تقوم استعلامات تجميع avg
بالعمل خلف الكواليس لجلب القيم sum
مثل count
، والجمع ، والمتوسط ، 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
لسهم معين ، يمكنه الاشتراك للحصول على إشعار كل ساعة أو عندما يصل سعر السهم إلى القيمة التي تم إدخالها. سنرى هذا في العمل في قسم الأحداث / مشغلات الوقت.
ملاحظة : سنصل إلى مكون 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') } ] }
يعرض المحور السيني الوقت ويعرض المحور الصادي قيمة المقياس في ذلك الوقت. تُستخدم وظيفة 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; }
بسيط! هذه هي الطريقة التي يتم بها إنشاء مكون المخطط! يرجى الرجوع إلى ملفات stockTimeseries.js
للحصول على الكود الكامل في السلاسل الزمنية للمخزون.
يجب أن تكون الآن جاهزًا بالبيانات وواجهات المستخدم كجزء من المشروع. دعنا ننتقل الآن إلى الجزء المثير للاهتمام - إعداد مشغلات الحدث / الوقت بناءً على مدخلات المستخدم.
إعداد الحدث / المشغلات المجدولة
في هذا القسم ، سنتعلم كيفية إعداد المشغلات على وحدة تحكم Hasura وكيفية إرسال إشعارات الويب إلى المستخدمين المحددين. هيا بنا نبدأ!
مشغلات الأحداث على وحدة التحكم Hasura
لنقم بإنشاء حدث مشغل stock_value
في الجدول stock_data
insert
تشغيل. سيتم تشغيل الويب هوك في كل مرة يوجد فيها إدراج في جدول stock_data
.
سنقوم بإنشاء مشروع خلل لعنوان URL الخاص بخطاف الويب. اسمحوا لي أن أتحدث قليلاً عن webhooks لتسهيل فهمها:
يتم استخدام Webhooks لإرسال البيانات من تطبيق إلى آخر عند حدوث حدث معين. عندما يتم تشغيل حدث ما ، يتم إجراء استدعاء HTTP POST إلى عنوان URL الخاص بـ webhook مع بيانات الحدث كحمولة.
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!
جلب المستخدمين المشتركين
const fetch = createApolloFetch({ uri: process.env.GRAPHQL_URL }); const getSubscribedUsers = (symbol, triggerValue) => { return fetch({ query: `query getSubscribedUsers($symbol: String, $triggerValue: numeric) { events(where: {symbol: {_eq: $symbol}, trigger_type: {_eq: "event"}, trigger_value: {_gte: $triggerValue}}) { user_id user_subscription { subscription } } }`, variables: { symbol, triggerValue } }).then(response => response.data.events) } const handleStockValueTrigger = async (eventData, res) => { const symbol = eventData.data.new.symbol; const triggerValue = eventData.data.new.close; const subscribedUsers = await getSubscribedUsers(symbol, triggerValue); const webpushPayload = { title: `${symbol} - Stock Update`, body: `The price of this stock is ${triggerValue}` } subscribedUsers.map((data) => { sendWebpush(data.user_subscription.subscription, JSON.stringify(webpushPayload)); }) res.json(eventData.toString()); }
في الدالة handleStockValueTrigger
أعلاه ، نقوم أولاً بإحضار المستخدمين المشتركين باستخدام وظيفة getSubscribedUsers
. نقوم بعد ذلك بإرسال إشعارات الدفع عبر الويب إلى كل من هؤلاء المستخدمين. تُستخدم الوظيفة sendWebpush
لإرسال الإشعارات. سننظر في تطبيق Web-push في غضون لحظة.
تستخدم الدالة getSubscribedUsers
الاستعلام:
query getSubscribedUsers($symbol: String, $triggerValue: numeric) { events(where: {symbol: {_eq: $symbol}, trigger_type: {_eq: "event"}, trigger_value: {_gte: $triggerValue}}) { user_id user_subscription { subscription } } }
يأخذ هذا الاستعلام رمز المخزون والقيمة ويجلب تفاصيل المستخدم بما user-id
user_subscription
الذي يطابق الشروط التالية:
- يساوي
symbol
الذي يتم تمريره في الحمولة. -
trigger_type
يساويevent
. -
trigger_value
أكبر من أو تساوي الوظيفة التي يتم تمريرها إلى هذه الوظيفة (close
في هذه الحالة).
بمجرد أن نحصل على قائمة المستخدمين ، فإن الشيء الوحيد المتبقي هو إرسال إشعارات عبر الويب إليهم! دعونا نفعل ذلك على الفور!
إرسال إخطارات Web-Push إلى المستخدمين المشتركين
يتعين علينا أولاً الحصول على مفاتيح VAPID العامة والخاصة لإرسال إشعارات الدفع عبر الويب. يرجى تخزين هذه المفاتيح في ملف .env
وتعيين هذه التفاصيل في index.js
على النحو التالي:
webPush.setVapidDetails( 'mailto:<YOUR_MAIL_ID>', process.env.PUBLIC_VAPID_KEY, process.env.PRIVATE_VAPID_KEY ); const sendWebpush = (subscription, webpushPayload) => { webPush.sendNotification(subscription, webpushPayload).catch(err => console.log('error while sending webpush', err)) }
يتم استخدام وظيفة sendNotification
لإرسال دفعة الويب على نقطة نهاية الاشتراك المقدمة كمعامل أول.
هذا كل ما هو مطلوب لإرسال إشعارات الدفع عبر الويب بنجاح إلى المستخدمين المشتركين. إليك الكود الكامل المحدد في 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 على النحو التالي:
يمكننا استخدام نفس عنوان URL الخاص بخطاف الويب والتعامل مع المستخدمين المشتركين بناءً على نوع حدث المشغل مثل stock_price_time_based_trigger
. التنفيذ مشابه للمشغل المستند إلى الحدث.
خاتمة
في هذه المقالة ، قمنا ببناء تطبيق إشعار أسعار الأسهم. تعلمنا كيفية جلب الأسعار باستخدام واجهات برمجة تطبيقات Alpha Vantage وتخزين نقاط البيانات في قاعدة بيانات Postgres المدعومة من Hasura. تعلمنا أيضًا كيفية إعداد محرك Hasura GraphQL وإنشاء مشغلات قائمة على الأحداث ومجدولة. قمنا ببناء مشروع خلل لإرسال إشعارات الويب إلى المستخدمين المشتركين.