دليل شامل لتطبيقات الويب التقدمية
نشرت: 2022-03-10كان عيد ميلاد والدي ، وأردت أن أطلب كعكة الشوكولاتة وقميصًا له. توجهت إلى Google للبحث عن كعك الشوكولاتة والنقر على الرابط الأول في نتائج البحث. كانت هناك شاشة فارغة لبضع ثوان ؛ لم أفهم ما كان يحدث. بعد بضع ثوانٍ من التحديق بصبر ، امتلأت شاشة هاتفي المحمول بالكعك اللذيذ. بمجرد النقر فوق أحدهم للتحقق من تفاصيله ، تلقيت نافذة منبثقة قبيحة ، تطلب مني تثبيت تطبيق Android حتى أحصل على تجربة سلسة أثناء طلب كعكة.
كان ذلك مخيبا للآمال. لم يسمح لي ضميري بالنقر فوق الزر "تثبيت". كل ما أردت فعله هو طلب كعكة صغيرة وأن أكون في طريقي.
لقد قمت بالنقر فوق رمز التقاطع في أقصى يمين النافذة المنبثقة للخروج منه في أسرع وقت ممكن. ولكن بعد ذلك ، جلست نافذة التثبيت المنبثقة في الجزء السفلي من الشاشة ، واحتلت ربع المساحة. ومع واجهة المستخدم غير المستقرة ، كان التمرير لأسفل يمثل تحديًا. بطريقة ما تمكنت من طلب كعكة هولندية.
بعد هذه التجربة الرهيبة ، كان التحدي التالي لي هو طلب قميص لأبي. كما كان من قبل ، أبحث في Google عن قمصان. لقد قمت بالنقر فوق الرابط الأول ، وفي غمضة عين ، كان المحتوى بالكامل أمامي مباشرة. كان التمرير سلسًا. لا لافتة التثبيت. شعرت كما لو كنت أتصفح تطبيقًا محليًا. كانت هناك لحظة استسلم فيها الاتصال بالإنترنت الرهيب ، لكنني كنت لا أزال قادرًا على رؤية المحتوى بدلاً من لعبة الديناصورات. حتى مع خدمة الإنترنت المبتذلة لدي ، تمكنت من طلب قميص وجينز لأبي. الأمر الأكثر إثارة للدهشة هو أنني تلقيت إشعارات حول طلبي.
أود أن أسمي هذا تجربة سلسة حريرية. هؤلاء الناس كانوا يفعلون شيئًا صحيحًا. يجب على كل موقع أن يفعل ذلك لمستخدميه. يطلق عليه تطبيق الويب التدريجي.
كما ذكر أليكس راسل في إحدى مشاركاته على مدونته:
"يحدث على الويب من وقت لآخر ظهور تقنيات قوية دون الاستفادة من أقسام التسويق أو التعبئة والتغليف الأنيقة. إنهم يتباطأون وينمون في الأطراف ، ويصبحون قبعة قديمة لمجموعة صغيرة بينما يظلون غير مرئيين تقريبًا لأي شخص آخر. حتى يسميهم شخص ما ".
تجربة سلسة كالحرير على الويب ، تُعرف أحيانًا باسم تطبيق ويب تقدمي
تطبيقات الويب التقدمية (PWAs) هي أكثر منهجية تتضمن مجموعة من التقنيات لإنشاء تطبيقات ويب قوية. مع تحسين تجربة المستخدم ، سيقضي الأشخاص المزيد من الوقت على مواقع الويب ويرون المزيد من الإعلانات. إنهم يميلون إلى شراء المزيد ، ومع تحديثات الإشعارات ، من المرجح أن يزوروا كثيرًا. تخلت Financial Times عن تطبيقاتها الأصلية في عام 2011 وأنشأت تطبيقًا على الويب باستخدام أفضل التقنيات المتاحة في ذلك الوقت. الآن ، نما المنتج إلى PWA كامل.
ولكن لماذا ، بعد كل هذا الوقت ، ستنشئ تطبيق ويب عندما يقوم تطبيق محلي بالمهمة بشكل جيد بما فيه الكفاية؟
لنلقِ نظرة على بعض المقاييس المشتركة في Google IO 17.
تم توصيل خمسة مليارات جهاز بالويب ، مما يجعل الويب أكبر منصة في تاريخ الحوسبة. على الويب عبر الأجهزة المحمولة ، يذهب 11.4 مليون زائر شهريًا إلى أفضل 1000 موقع ويب ، ويذهب 4 ملايين إلى أفضل ألف تطبيق. يستقطب الويب المحمول حوالي أربعة أضعاف عدد المستخدمين للتطبيقات الأصلية. لكن هذا الرقم ينخفض بشكل حاد عندما يتعلق الأمر بالمشاركة.
يقضي المستخدم في المتوسط 188.6 دقيقة في التطبيقات المحلية و 9.3 دقيقة فقط على الويب عبر الهاتف المحمول. تستفيد التطبيقات الأصلية من قوة أنظمة التشغيل لإرسال إشعارات فورية لمنح المستخدمين تحديثات مهمة. إنها توفر تجربة مستخدم أفضل ويتم تشغيلها بسرعة أكبر من مواقع الويب في المتصفح. بدلاً من كتابة عنوان URL في متصفح الويب ، يتعين على المستخدمين فقط النقر فوق رمز التطبيق على الشاشة الرئيسية.
من غير المحتمل أن يعود معظم الزائرين على الويب ، لذلك توصل المطورون إلى حل بديل لإظهار لافتات لهم لتثبيت تطبيقات أصلية ، في محاولة لإبقائهم على اتصال بعمق. ولكن بعد ذلك ، سيتعين على المستخدمين اتباع الإجراء الممل المتمثل في تثبيت ثنائي التطبيق الأصلي. يعد إجبار المستخدمين على تثبيت أحد التطبيقات أمرًا مزعجًا ويقلل من فرصة قيامهم بتثبيته في المقام الأول. فرصة الويب واضحة.
يوصى بقراءة : Native و PWA: اختيارات وليست منافسين!
إذا كانت تطبيقات الويب تتمتع بتجربة مستخدم ثرية ، ودفع الإخطارات ، والدعم في وضع عدم الاتصال والتحميل الفوري ، فيمكنها التغلب على العالم. هذا ما يفعله تطبيق الويب التدريجي.
يوفر PWA تجربة مستخدم ثرية لأنه يحتوي على العديد من نقاط القوة:
بسرعة
واجهة المستخدم ليست غير مستقر. التمرير سلس. ويستجيب التطبيق بسرعة لتفاعل المستخدم.موثوق
يجبر موقع الويب العادي المستخدمين على الانتظار ، وعدم القيام بأي شيء ، بينما يكون مشغولاً في القيام بجولات إلى الخادم. وفي الوقت نفسه ، يقوم PWA بتحميل البيانات على الفور من ذاكرة التخزين المؤقت. يعمل PWA بسلاسة ، حتى على اتصال 2G. يمر كل طلب شبكة لجلب أحد الأصول أو جزء من البيانات عبر عامل خدمة (المزيد عن ذلك لاحقًا) ، والذي يتحقق أولاً مما إذا كانت الاستجابة لطلب معين موجودة بالفعل في ذاكرة التخزين المؤقت. عندما يحصل المستخدمون على محتوى حقيقي على الفور تقريبًا ، حتى في حالة الاتصال الضعيف ، فإنهم يثقون في التطبيق أكثر ويعتبرونه أكثر موثوقية.الانخراط
يمكن لـ PWA كسب مكان على الشاشة الرئيسية للمستخدم. يقدم تجربة شبيهة بالتطبيق الأصلي من خلال توفير منطقة عمل بملء الشاشة. إنه يستخدم دفع الإخطارات للحفاظ على تفاعل المستخدمين.
الآن بعد أن عرفنا ما تجلبه PWAs إلى الطاولة ، دعنا ندخل في تفاصيل ما يمنح PWAs ميزة على التطبيقات الأصلية. تم تصميم PWAs باستخدام تقنيات مثل العاملين في الخدمة ، وبيانات تطبيقات الويب ، وإشعارات الدفع ، وهيكل بيانات IndexedDB / بنية البيانات المحلية للتخزين المؤقت. دعونا ننظر في كل التفاصيل بالتفصيل.
عمال الخدمة
عامل الخدمة هو ملف JavaScript يعمل في الخلفية دون التدخل في تفاعلات المستخدم. تمر جميع طلبات GET إلى الخادم من خلال عامل خدمة. إنه يتصرف مثل وكيل من جانب العميل. من خلال اعتراض طلبات الشبكة ، فإنه يأخذ السيطرة الكاملة على الاستجابة التي يتم إرسالها مرة أخرى إلى العميل. يتم تحميل PWA على الفور لأن العاملين في الخدمة يتخلصون من الاعتماد على الشبكة من خلال الاستجابة ببيانات من ذاكرة التخزين المؤقت.
يمكن لعامل الخدمة فقط اعتراض طلب الشبكة الموجود في نطاقه. على سبيل المثال ، يمكن لعامل الخدمة المحدد الجذر اعتراض جميع طلبات الجلب الواردة من صفحة الويب. عامل الخدمة يعمل كنظام يحركه الحدث. يدخل في حالة سبات عندما لا تكون هناك حاجة إليه ، وبالتالي الحفاظ على الذاكرة. لاستخدام عامل خدمة في تطبيق ويب ، يتعين علينا أولاً تسجيله على الصفحة باستخدام JavaScript.
(function main () { /* navigator is a WEB API that allows scripts to register themselves and carry out their activities. */ if ('serviceWorker' in navigator) { console.log('Service Worker is supported in your browser') /* register method takes in the path of service worker file and returns a promises, which returns the registration object */ navigator.serviceWorker.register('./service-worker.js').then (registration => { console.log('Service Worker is registered!') }) } else { console.log('Service Worker is not supported in your browser') } })()
نتحقق أولاً مما إذا كان المتصفح يدعم العاملين في الخدمة. لتسجيل عامل خدمة في تطبيق ويب ، نقدم عنوان URL الخاص به كمعامل لوظيفة register
، والمتوفر في navigator.serviceWorker
( navigator
عبارة عن واجهة برمجة تطبيقات ويب تتيح للبرامج النصية تسجيل نفسها وتنفيذ أنشطتها). يتم تسجيل عامل الخدمة مرة واحدة فقط. التسجيل لا يحدث في كل تحميل صفحة. يقوم المتصفح بتنزيل ملف عامل الخدمة ( ./service-worker.js
) فقط إذا كان هناك فرق بايت بين عامل الخدمة المنشط الحالي والأحدث أو إذا تم تغيير عنوان URL الخاص به.
سيعترض عامل الخدمة أعلاه جميع الطلبات الواردة من الجذر ( /
). للحد من نطاق عامل الخدمة ، سنقوم بتمرير معلمة اختيارية مع أحد المفاتيح كنطاق.
if ('serviceWorker' in navigator) { /* register method takes in an optional second parameter as an object. To restrict the scope of a service worker, the scope should be provided. scope: '/books' will intercept requests with '/books' in the url. */ navigator.serviceWorker.register('./service-worker.js', { scope: '/books' }).then(registration => { console.log('Service Worker for scope /books is registered', registration) }) }
سيعترض عامل الخدمة أعلاه الطلبات التي تحتوي على /books
في عنوان URL. على سبيل المثال ، لن يعترض الطلب مع /products
، ولكن يمكنه بشكل جيد للغاية اعتراض الطلبات مع /books/products
.
كما ذكرنا ، يعمل عامل الخدمة كنظام يحركه الحدث. يستمع إلى الأحداث (التثبيت ، التنشيط ، الجلب ، الدفع) وبناءً عليه يستدعي معالج الحدث المعني. بعض هذه الأحداث هي جزء من دورة حياة عامل الخدمة ، والتي تمر بهذه الأحداث بالتسلسل لتنشيطها.
تثبيت
بمجرد تسجيل عامل الخدمة بنجاح ، يتم تشغيل حدث التثبيت. يعد هذا مكانًا جيدًا للقيام بأعمال التهيئة ، مثل إعداد ذاكرة التخزين المؤقت أو إنشاء مخازن الكائنات في IndexedDB. (ستكون قاعدة البيانات المفهرسة أكثر منطقية بالنسبة إليك بمجرد أن ندخل في تفاصيلها. في الوقت الحالي ، يمكننا فقط أن نقول إنها بنية زوجية ذات قيمة رئيسية.)
self.addEventListener('install', (event) => { let CACHE_NAME = 'xyz-cache' let urlsToCache = [ '/', '/styles/main.css', '/scripts/bundle.js' ] event.waitUntil( /* open method available on caches, takes in the name of cache as the first parameter. It returns a promise that resolves to the instance of cache All the URLS above can be added to cache using the addAll method. */ caches.open(CACHE_NAME) .then (cache => cache.addAll(urlsToCache)) ) })
هنا ، نقوم بتخزين بعض الملفات مؤقتًا بحيث يكون التحميل التالي فوريًا. يشير self
إلى مثيل عامل الخدمة. event.waitUntil
يجعل عامل الخدمة ينتظر حتى ينتهي تنفيذ جميع التعليمات البرمجية الموجودة بداخله.
التنشيط
بمجرد تثبيت عامل الخدمة ، لا يمكنه الاستماع لطلبات الجلب. بدلاً من ذلك ، يتم تشغيل حدث activate
. إذا لم يكن هناك عامل خدمة نشط يعمل على موقع الويب في نفس النطاق ، فسيتم تنشيط عامل الخدمة المثبت على الفور. ومع ذلك ، إذا كان أحد مواقع الويب يحتوي بالفعل على عامل خدمة نشط ، فسيتم تأخير تنشيط عامل الخدمة الجديد حتى يتم إغلاق جميع علامات التبويب التي تعمل على عامل الخدمة القديم. هذا أمر منطقي لأن عامل الخدمة القديم ربما يستخدم مثيل ذاكرة التخزين المؤقت التي تم تعديلها الآن في النسخة الأحدث. لذا ، فإن خطوة التنشيط هي مكان جيد للتخلص من ذاكرات التخزين المؤقت القديمة.
self.addEventListener('activate', (event) => { let cacheWhitelist = ['products-v2'] // products-v2 is the name of the new cache event.waitUntil( caches.keys().then (cacheNames => { return Promise.all( cacheNames.map( cacheName => { /* Deleting all the caches except the ones that are in cacheWhitelist array */ if (cacheWhitelist.indexOf(cacheName) === -1) { return caches.delete(cacheName) } }) ) }) ) })
في الكود أعلاه ، نقوم بحذف ذاكرة التخزين المؤقت القديمة. إذا كان اسم ذاكرة التخزين المؤقت لا يتطابق مع cacheWhitelist
، فسيتم حذفها. لتخطي مرحلة الانتظار وتفعيل عامل الخدمة فورًا ، نستخدم skip.waiting()
.
self.addEventListener('activate', (event) => { self.skipWaiting() // The usual stuff })
بمجرد تنشيط عامل الخدمة ، يمكنه الاستماع إلى طلبات الجلب ودفع الأحداث.
معالج إحضار الأحداث
عندما تطلق صفحة ويب طلب جلب لمورد عبر الشبكة ، يتم استدعاء حدث الجلب من عامل الخدمة. يبحث معالج حدث الجلب أولاً عن المورد المطلوب في ذاكرة التخزين المؤقت. إذا كان موجودًا في ذاكرة التخزين المؤقت ، فإنه يقوم بإرجاع الاستجابة مع المورد المخزن مؤقتًا. وإلا ، فإنه يبدأ طلب الجلب إلى الخادم ، وعندما يرسل الخادم الاستجابة مع المورد المطلوب ، فإنه يضعها في ذاكرة التخزين المؤقت للطلبات اللاحقة.
/* Fetch event handler for responding to GET requests with the cached assets */ self.addEventListener('fetch', (event) => { event.respondWith( caches.open('products-v2') .then (cache => { /* Checking if the request is already present in the cache. If it is present, sending it directly to the client */ return cache.match(event.request).then (response => { if (response) { console.log('Cache hit! Fetching response from cache', event.request.url) return response } /* If the request is not present in the cache, we fetch it from the server and then put it in cache for subsequent requests. */ fetch(event.request).then (response => { cache.put(event.request, response.clone()) return response }) }) }) ) })
يتيح event.respondWith
لعامل الخدمة إرسال استجابة مخصصة إلى العميل.
غير متصل أولاً أصبح شيئًا الآن. لأي طلب غير هام ، يجب أن نخدم الاستجابة من ذاكرة التخزين المؤقت ، بدلاً من الانتقال إلى الخادم. في حالة عدم وجود أي أصل في ذاكرة التخزين المؤقت ، نحصل عليه من الخادم ثم نخزنه مؤقتًا للطلبات اللاحقة.
يعمل عمال الخدمة فقط على مواقع HTTPS لأن لديهم القدرة على معالجة استجابة أي طلب جلب. قد يعبث شخص ما بقصد ضار في الاستجابة لطلب موجود على موقع ويب HTTP. لذا ، فإن استضافة PWA على HTTPS أمر إلزامي. لا يقوم عمال الخدمة بمقاطعة الأداء الطبيعي لـ DOM. لا يمكنهم التواصل مباشرة مع صفحة الويب. لإرسال أي رسالة إلى صفحة ويب ، فإنه يستخدم رسائل النشر.
إخطارات دفع الويب
لنفترض أنك مشغول بلعب لعبة على هاتفك المحمول ، وانبثاق إشعار يخبرك بخصم 30٪ على علامتك التجارية المفضلة. دون أي مزيد من اللغط ، يمكنك النقر فوق الإشعار وتسوق أنفاسك. الحصول على تحديثات مباشرة ، على سبيل المثال ، لعبة كريكيت أو مباراة كرة قدم أو تلقي رسائل بريد إلكتروني وتذكيرات مهمة كإخطارات يعد أمرًا مهمًا عندما يتعلق الأمر بإشراك المستخدمين في أحد المنتجات. كانت هذه الميزة متاحة فقط في التطبيقات الأصلية حتى ظهور PWA. يستخدم PWA إشعارات الويب للتنافس مع هذه الميزة القوية التي توفرها التطبيقات الأصلية خارج الصندوق. سيظل المستخدم يتلقى إشعارًا بالدفع عبر الويب حتى إذا لم يكن PWA مفتوحًا في أي من علامات تبويب المتصفح وحتى إذا لم يكن المتصفح مفتوحًا.
يجب أن يطلب تطبيق الويب إذنًا من المستخدم لإرسال إشعارات فورية إليه.

بمجرد تأكيد المستخدم بالنقر فوق الزر "السماح" ، يتم إنشاء رمز اشتراك فريد بواسطة المتصفح. هذا الرمز مميز لهذا الجهاز. تنسيق رمز الاشتراك الذي تم إنشاؤه بواسطة Chrome هو كما يلي:
{ "endpoint": "https://fcm.googleapis.com/fcm/send/c7Veb8VpyM0:APA91bGnMFx8GIxf__UVy6vJ-n9i728CUJSR1UHBPAKOCE_SrwgyP2N8jL4MBXf8NxIqW6NCCBg01u8c5fcY0kIZvxpDjSBA75sVz64OocQ-DisAWoW7PpTge3SwvQAx5zl_45aAXuvS", "expirationTime": null, "keys": { "p256dh": "BJsj63kz8RPZe8Lv1uu-6VSzT12RjxtWyWCzfa18RZ0-8sc5j80pmSF1YXAj0HnnrkyIimRgLo8ohhkzNA7lX4w", "auth": "TJXqKozSJxcWvtQasEUZpQ" } }
ستكون endpoint
الواردة في الرمز المميز أعلاه فريدة لكل اشتراك. على موقع ويب متوسط ، يوافق آلاف المستخدمين على تلقي إشعارات الدفع ، وستكون endpoint
هذه فريدة لكل منهم. لذلك ، بمساعدة endpoint
هذه ، يمكن للتطبيق استهداف هؤلاء المستخدمين في المستقبل عن طريق إرسال إشعارات فورية إليهم. expirationTime
هو مقدار الوقت الذي يكون فيه الاشتراك صالحًا لجهاز معين. إذا كانت expirationTime
20 يومًا ، فهذا يعني أن اشتراك الدفع للمستخدم سينتهي بعد 20 يومًا ولن يتمكن المستخدم من تلقي إشعارات الدفع على الاشتراك الأقدم. في هذه الحالة ، سيقوم المتصفح بإنشاء رمز اشتراك جديد لهذا الجهاز. يتم استخدام مفاتيح auth
و p256dh
للتشفير.
الآن ، لإرسال إشعارات فورية إلى هؤلاء الآلاف من المستخدمين في المستقبل ، يتعين علينا أولاً حفظ رموز الاشتراك الخاصة بهم. إنها مهمة خادم التطبيق (الخادم الخلفي ، ربما برنامج نصي Node.js) لإرسال إشعارات إلى هؤلاء المستخدمين. قد يبدو هذا بسيطًا مثل تقديم طلب POST
إلى عنوان URL لنقطة النهاية مع بيانات الإشعار في حمولة الطلب. ومع ذلك ، تجدر الإشارة إلى أنه إذا لم يكن المستخدم متصلاً بالإنترنت عندما يتم تشغيل إشعار دفع مخصص له بواسطة الخادم ، فلا يزال يتعين عليه تلقي هذا الإشعار بمجرد عودته عبر الإنترنت. سيتعين على الخادم الاهتمام بمثل هذه السيناريوهات ، إلى جانب إرسال آلاف الطلبات إلى المستخدمين. يبدو خادمًا يتعقب اتصال المستخدم أمرًا معقدًا. لذلك ، هناك شيء ما في المنتصف سيكون مسؤولاً عن توجيه إشعارات الويب من الخادم إلى العميل. وهذا ما يسمى خدمة الدفع ، ولكل متصفح تنفيذه الخاص لخدمة الدفع. يجب أن يقوم المتصفح بإخبار خدمة الدفع بالمعلومات التالية لإرسال أي إشعار:
- وقت العيش
هذه هي المدة التي يجب أن تكون فيها الرسالة في قائمة الانتظار ، في حالة عدم تسليمها إلى المستخدم. بمجرد انقضاء هذا الوقت ، ستتم إزالة الرسالة من قائمة الانتظار. - استعجال الرسالة
هذا حتى تحافظ خدمة الدفع على بطارية المستخدم عن طريق إرسال الرسائل ذات الأولوية العالية فقط.
تقوم خدمة الدفع بتوجيه الرسائل إلى العميل. نظرًا لأنه يتعين على العميل تلقي الدفع حتى إذا لم يكن تطبيق الويب الخاص به مفتوحًا في المتصفح ، فيجب الاستماع إلى أحداث الدفع بواسطة شيء يراقب باستمرار في الخلفية. كنت تفكر في ذلك: هذا هو عمل عامل الخدمة. يستمع عامل الخدمة إلى أحداث الدفع ويقوم بمهمة إظهار الإشعارات للمستخدم.
لذلك ، نحن نعلم الآن أن المتصفح وخدمة الدفع وعامل الخدمة وخادم التطبيق يعملان في تناغم لإرسال إشعارات الدفع إلى المستخدم. دعونا ننظر في تفاصيل التنفيذ.
عميل دفع الويب
طلب الإذن من المستخدم هو أمر يحدث لمرة واحدة. إذا منح المستخدم إذنًا بالفعل لتلقي الإشعارات الفورية ، فلا يجب أن نطلب ذلك مرة أخرى. يتم حفظ قيمة الإذن في Notification.permission
.
/* Notification.permission can have one of these three values: default, granted or denied. */ if (Notification.permission === 'default') { /* The Notification.requestPermission() method shows a notification permission prompt to the user. It returns a promise that resolves to the value of permission*/ Notification.requestPermission().then (result => { if (result === 'denied') { console.log('Permission denied') return } if (result === 'granted') { console.log('Permission granted') /* This means the user has clicked the Allow button. We're to get the subscription token generated by the browser and store it in our database. The subscription token can be fetched using the getSubscription method available on pushManager of the serviceWorkerRegistration object. If subscription is not available, we subscribe using the subscribe method available on pushManager. The subscribe method takes in an object. */ serviceWorkerRegistration.pushManager.getSubscription() .then (subscription => { if (!subscription) { const applicationServerKey = ' ' serviceWorkerRegistration.pushManager.subscribe({ userVisibleOnly: true, // All push notifications from server should be displayed to the user applicationServerKey // VAPID Public key }) } else { saveSubscriptionInDB(subscription, userId) // A method to save subscription token in the database } }) } }) }
/* Notification.permission can have one of these three values: default, granted or denied. */ if (Notification.permission === 'default') { /* The Notification.requestPermission() method shows a notification permission prompt to the user. It returns a promise that resolves to the value of permission*/ Notification.requestPermission().then (result => { if (result === 'denied') { console.log('Permission denied') return } if (result === 'granted') { console.log('Permission granted') /* This means the user has clicked the Allow button. We're to get the subscription token generated by the browser and store it in our database. The subscription token can be fetched using the getSubscription method available on pushManager of the serviceWorkerRegistration object. If subscription is not available, we subscribe using the subscribe method available on pushManager. The subscribe method takes in an object. */ serviceWorkerRegistration.pushManager.getSubscription() .then (subscription => { if (!subscription) { const applicationServerKey = ' ' serviceWorkerRegistration.pushManager.subscribe({ userVisibleOnly: true, // All push notifications from server should be displayed to the user applicationServerKey // VAPID Public key }) } else { saveSubscriptionInDB(subscription, userId) // A method to save subscription token in the database } }) } }) }
في طريقة subscribe
أعلاه ، نقوم بتمرير userVisibleOnly
و applicationServerKey
لإنشاء رمز اشتراك. يجب أن تكون الخاصية userVisibleOnly
دائمًا صحيحة لأنها تخبر المتصفح أن أي إشعار دفع يرسله الخادم سيظهر للعميل. لفهم الغرض من applicationServerKey
، دعنا نفكر في السيناريو.

إذا حصل شخص ما على الآلاف من رموز الاشتراك الخاصة بك ، فيمكنه إرسال إشعارات إلى نقاط النهاية الموجودة في هذه الاشتراكات. لا توجد طريقة لربط نقطة النهاية بهويتك الفريدة. لتوفير هوية فريدة لرموز الاشتراك التي تم إنشاؤها في تطبيق الويب الخاص بك ، فإننا نستخدم بروتوكول VAPID. باستخدام VAPID ، يقوم خادم التطبيق بتعريف نفسه طواعية لخدمة الدفع أثناء إرسال إشعارات الدفع. نقوم بإنشاء مفتاحين مثل:
const webpush = require('web-push') const vapidKeys = webpush.generateVAPIDKeys()
دفع الويب هو وحدة npm. سيكون لدى vapidKeys
مفتاح عام واحد ومفتاح خاص واحد. مفتاح خادم التطبيق المستخدم أعلاه هو المفتاح العام.
خادم دفع الويب
وظيفة خادم دفع الويب (خادم التطبيق) واضحة ومباشرة. يرسل حمولة إعلام إلى رموز الاشتراك.
const options = { TTL: 24*60*60, //TTL is the time to live, the time that the notification will be queued in the push service vapidDetails: { subject: '[email protected]', publicKey: ' ', privateKey: ' ' } } const data = { title: 'Update', body: 'Notification sent by the server' } webpush.sendNotification(subscription, data, options)
const options = { TTL: 24*60*60, //TTL is the time to live, the time that the notification will be queued in the push service vapidDetails: { subject: '[email protected]', publicKey: ' ', privateKey: ' ' } } const data = { title: 'Update', body: 'Notification sent by the server' } webpush.sendNotification(subscription, data, options)
const options = { TTL: 24*60*60, //TTL is the time to live, the time that the notification will be queued in the push service vapidDetails: { subject: '[email protected]', publicKey: ' ', privateKey: ' ' } } const data = { title: 'Update', body: 'Notification sent by the server' } webpush.sendNotification(subscription, data, options)
يستخدم طريقة sendNotification
من مكتبة دفع الويب.
عمال الخدمة
يُظهر عامل الخدمة الإشعار للمستخدم على النحو التالي:
self.addEventListener('push', (event) => { let options = { body: event.data.body, icon: 'images/example.png', } event.waitUntil( /* The showNotification method is available on the registration object of the service worker. The first parameter to showNotification method is the title of notification, and the second parameter is an object */ self.registration.showNotification(event.data.title, options) ) })
حتى الآن ، رأينا كيف يستخدم عامل الخدمة ذاكرة التخزين المؤقت لتخزين الطلبات ويجعل PWA سريعًا وموثوقًا ، وقد رأينا كيف تحافظ إشعارات الويب على تفاعل المستخدمين.
لتخزين مجموعة من البيانات على جانب العميل للدعم دون اتصال بالإنترنت ، نحتاج إلى بنية بيانات عملاقة. لنلق نظرة على Financial Times PWA. عليك أن تشهد قوة هيكل البيانات هذا بنفسك. قم بتحميل عنوان URL في المستعرض الخاص بك ، ثم قم بإيقاف تشغيل اتصال الإنترنت الخاص بك. خذ المخاطر. لا شيء يمكن أن يحل محل التجربة. جاه! هل ما زالت تعمل؟ أنه. (كما قلت ، وضع عدم الاتصال هو اللون الأسود الجديد). البيانات لا تأتي من الأسلاك. يتم تقديمه من المنزل. توجه إلى علامة التبويب "التطبيقات" في Chrome Developer Tools. ضمن "التخزين" ، ستجد "قاعدة بيانات مفهرسة".

تحقق من متجر الكائنات "Articles" ، وقم بتوسيع أي من العناصر لترى السحر بنفسك. قامت Financial Times بتخزين هذه البيانات للدعم دون اتصال بالإنترنت. تسمى بنية البيانات هذه التي تتيح لنا تخزين كمية هائلة من البيانات باسم قاعدة البيانات المفهرسة. IndexedDB هي قاعدة بيانات كائنية تعتمد على JavaScript لتخزين البيانات المهيكلة. يمكننا إنشاء مخازن كائنات مختلفة في قاعدة البيانات هذه لأغراض مختلفة. على سبيل المثال ، كما نرى في الصورة أعلاه ، يُطلق على "الموارد" و "ArticleImages" و "Articles" اسم مخازن الكائنات. يتم تعريف كل سجل في ملف تخزين العناصر بشكل فريد بمفتاح. يمكن استخدام IndexedDB لتخزين الملفات والنقاط الكبيرة.
دعنا نحاول فهم قاعدة بيانات مفهرسة عن طريق إنشاء قاعدة بيانات لتخزين الكتب.
let openIdbRequest = window.indexedDB.open('booksdb', 1)
إذا لم تكن قاعدة البيانات booksdb
موجودة بالفعل ، فسيقوم الكود أعلاه بإنشاء قاعدة بيانات booksdb
. المعلمة الثانية للطريقة المفتوحة هي إصدار قاعدة البيانات. تحديد إصدار يعتني بالتغييرات المتعلقة بالمخطط التي قد تحدث في المستقبل. على سبيل المثال ، booksdb
الآن على جدول واحد فقط ، ولكن عندما ينمو التطبيق ، نعتزم إضافة جدولين آخرين إليه. للتأكد من أن قاعدة البيانات الخاصة بنا متزامنة مع المخطط المحدث ، سنحدد إصدارًا أعلى من الإصدار السابق.
لا يؤدي استدعاء الطريقة open
إلى فتح قاعدة البيانات على الفور. إنه طلب غير متزامن يقوم بإرجاع كائن IDBOpenDBRequest
. هذا الكائن له خصائص النجاح والخطأ ؛ سيتعين علينا كتابة معالجات مناسبة لهذه الخصائص لإدارة حالة اتصالنا.
let dbInstance openIdbRequest.onsuccess = (event) => { dbInstance = event.target.result console.log('booksdb is opened successfully') } openIdbRequest.onerror = (event) => { console.log('There was an error in opening booksdb database') } openIdbRequest.onupgradeneeded = (event) => { let db = event.target.result let objectstore = db.createObjectStore('books', { keyPath: 'id' }) }
لإدارة إنشاء أو تعديل مخازن الكائنات (مخازن الكائنات مماثلة للجداول المستندة إلى SQL - لها بنية ذات قيمة مفتاح) ، يتم استدعاء طريقة onupgradeneeded
في كائن openIdbRequest
. سيتم استدعاء طريقة onupgradeneeded
كلما تغير الإصدار. في مقتطف الشفرة أعلاه ، نقوم بإنشاء مخزن كائنات للكتب بمفتاح فريد مثل المعرف.
لنفترض أنه بعد نشر هذا الجزء من الكود ، يتعين علينا إنشاء مخزن كائنات آخر ، يُطلق عليه اسم users
. إذن ، سيكون إصدار قاعدة البيانات الخاصة بنا الآن هو 2
.
let openIdbRequest = window.indexedDB.open('booksdb', 2) // New Version - 2 /* Success and error event handlers remain the same. The onupgradeneeded method gets called when the version of the database changes. */ openIdbRequest.onupgradeneeded = (event) => { let db = event.target.result if (!db.objectStoreNames.contains('books')) { let objectstore = db.createObjectStore('books', { keyPath: 'id' }) } let oldVersion = event.oldVersion let newVersion = event.newVersion /* The users tables should be added for version 2. If the existing version is 1, it will be upgraded to 2, and the users object store will be created. */ if (oldVersion === 1) { db.createObjectStore('users', { keyPath: 'id' }) } }
لقد قمنا dbInstance
مؤقتًا في معالج حدث النجاح للطلب المفتوح. لاسترداد أو إضافة البيانات في IndexedDB ، سنستخدم dbInstance
. دعنا نضيف بعض سجلات الكتب في متجر كائنات الكتب لدينا.
let transaction = dbInstance.transaction('books') let objectstore = transaction.objectstore('books') let bookRecord = { id: '1', name: 'The Alchemist', author: 'Paulo Coelho' } let addBookRequest = objectstore.add(bookRecord) addBookRequest.onsuccess = (event) => { console.log('Book record added successfully') } addBookRequest.onerror = (event) => { console.log('There was an error in adding book record') }
نحن نستفيد من transactions
، خاصة أثناء كتابة السجلات في مخازن الكائنات. المعاملة هي مجرد غلاف حول عملية لضمان تكامل البيانات. في حالة فشل أي من الإجراءات في إحدى المعاملات ، فلن يتم تنفيذ أي إجراء على قاعدة البيانات.
دعنا نعدل سجل كتاب باستخدام طريقة put
:
let modifyBookRequest = objectstore.put(bookRecord) // put method takes in an object as the parameter modifyBookRequest.onsuccess = (event) => { console.log('Book record updated successfully') }
دعنا نسترجع سجل كتاب باستخدام طريقة get
:
let transaction = dbInstance.transaction('books') let objectstore = transaction.objectstore('books') /* get method takes in the id of the record */ let getBookRequest = objectstore.get(1) getBookRequest.onsuccess = (event) => { /* event.target.result contains the matched record */ console.log('Book record', event.target.result) } getBookRequest.onerror = (event) => { console.log('Error while retrieving the book record.') }
إضافة أيقونة على الشاشة الرئيسية
الآن بما أنه لا يكاد يوجد أي تمييز بين PWA والتطبيق الأصلي ، فمن المنطقي تقديم موقع رئيسي لـ PWA. إذا كان موقع الويب الخاص بك يفي بالمعايير الأساسية لـ PWA (مستضاف على HTTPS ، يتكامل مع عمال الخدمة ولديه ملف manifest.json
) وبعد أن يقضي المستخدم بعض الوقت على صفحة الويب ، سيقوم المتصفح باستدعاء مطالبة في الجزء السفلي يسأل يقوم المستخدم بإضافة التطبيق إلى شاشتهم الرئيسية ، كما هو موضح أدناه:

عندما ينقر المستخدم على "إضافة FT إلى الشاشة الرئيسية" ، فإن PWA يضع قدمه على الشاشة الرئيسية ، وكذلك في درج التطبيق. عندما يبحث المستخدم عن أي تطبيق على هاتفه ، سيتم سرد أي PWAs تطابق استعلام البحث. ستظهر أيضًا في إعدادات النظام ، مما يسهل على المستخدمين إدارتها. بهذا المعنى ، يتصرف PWA مثل تطبيق محلي.
تستخدم PWAs ملف manifest.json
لتوفير هذه الميزة. لنلق نظرة على ملف manifest.json
البسيط.
{ "name": "Demo PWA", "short_name": "Demo", "start_url": "/?standalone", "background_color": "#9F0C3F", "theme_color": "#fff1e0", "display": "standalone", "icons": [{ "src": "/lib/img/icons/xxhdpi.png?v2", "sizes": "192x192" }] }
يظهر short_name
على الشاشة الرئيسية للمستخدم وفي إعدادات النظام. يظهر name
في موجه الكروم وعلى شاشة البداية. شاشة البداية هي ما يراه المستخدم عندما يكون التطبيق جاهزًا للتشغيل. start_url
هي الشاشة الرئيسية لتطبيقك. إنه ما يحصل عليه المستخدمون عندما ينقرون على أيقونة على الشاشة الرئيسية. يتم استخدام background_color
على شاشة البداية. يحدد theme_color
لون شريط الأدوات. تشير القيمة standalone
لوضع display
إلى أنه سيتم تشغيل التطبيق في وضع ملء الشاشة (إخفاء شريط أدوات المتصفح). عندما يقوم المستخدم بتثبيت PWA ، يكون حجمه بالكيلو بايت فقط ، وليس بالميجابايت للتطبيقات الأصلية.
يعوض العاملون في الخدمة وإشعارات الدفع عبر الويب و IndexedDB وموضع الشاشة الرئيسية عن الدعم والموثوقية والمشاركة في وضع عدم الاتصال. وتجدر الإشارة إلى أن عامل الخدمة لا يأتي إلى الحياة ويبدأ في أداء عمله على أول حمولة. سيظل الحمل الأول بطيئًا حتى يتم تخزين جميع الأصول الثابتة والموارد الأخرى مؤقتًا. يمكننا تنفيذ بعض الاستراتيجيات لتحسين الحمل الأول.
تجميع الأصول
سيتم جلب جميع الموارد ، بما في ذلك HTML وأوراق الأنماط والصور وجافا سكريبت ، من الخادم. كلما زاد عدد الملفات ، زادت طلبات HTTPS المطلوبة لجلبها. يمكننا استخدام حزم مثل WebPack لتجميع أصولنا الثابتة ، وبالتالي تقليل عدد طلبات HTTP إلى الخادم. يقوم WebPack بعمل رائع في زيادة تحسين الحزمة باستخدام تقنيات مثل تقسيم الكود (أي تجميع الملفات المطلوبة فقط لتحميل الصفحة الحالية ، بدلاً من تجميعها جميعًا معًا) وهز الشجرة (أي إزالة التبعيات المكررة أو التبعيات التي تم استيرادها ولكن لم يتم استخدامها في الكود).
تقليل الرحلات ذهابًا وإيابًا
يعد وقت استجابة الشبكة أحد الأسباب الرئيسية للبطء على الويب. يختلف الوقت الذي يستغرقه البايت في الانتقال من أ إلى ب باختلاف اتصال الشبكة. على سبيل المثال ، تستغرق رحلة الذهاب والإياب عبر شبكة Wi-Fi 50 مللي ثانية و 500 مللي ثانية على اتصال 3G ، ولكن 2500 مللي ثانية على اتصال 2G. يتم إرسال هذه الطلبات باستخدام بروتوكول HTTP ، مما يعني أنه أثناء استخدام اتصال معين لطلب ، لا يمكن استخدامه لأي طلبات أخرى حتى يتم تقديم استجابة الطلب السابق. يمكن لموقع ويب إنشاء ستة طلبات HTTP غير متزامنة في وقت واحد لأن ستة اتصالات متاحة لموقع ويب لعمل طلبات HTTP. يقدم موقع الويب المتوسط ما يقرب من 100 طلب ؛ لذلك ، مع توفر ستة اتصالات كحد أقصى ، قد ينتهي الأمر بالمستخدم إلى إنفاق حوالي 833 مللي ثانية في رحلة واحدة ذهابًا وإيابًا. (الحساب هو 833 مللي ثانية - 100 6 = 1666. علينا قسمة 1666 على 2 لأننا نحسب الوقت المنقضي في رحلة الذهاب والإياب.) مع تطبيق HTTP2 ، يتم تقليل وقت الاستجابة بشكل كبير. لا يحظر HTTP2 رأس الاتصال ، لذلك يمكن إرسال طلبات متعددة في وقت واحد.
تحتوي معظم استجابات HTTP على الرؤوس last-modified
مؤخرًا و etag
. last-modified
هو تاريخ آخر تعديل للملف ، و etag
هو قيمة فريدة تستند إلى محتويات الملف. سيتم تغييره فقط عندما يتم تغيير محتويات الملف. يمكن استخدام هذين العنوانين لتجنب تنزيل الملف مرة أخرى إذا كانت النسخة المخزنة مؤقتًا متوفرة محليًا بالفعل. إذا كان المتصفح يحتوي على إصدار من هذا الملف متاحًا محليًا ، فيمكنه إضافة أي من هذين العنوانين في الطلب على النحو التالي:

يمكن للخادم التحقق مما إذا كانت محتويات الملف قد تغيرت. If the contents of the file have not changed, then it responds with a status code of 304 ( not modified ).

This indicates to the browser to use the locally available cached version of the file. By doing all of this, we've prevented the file from being downloaded.
Faster responses are in now place, but our job is not done yet. We still have to parse the HTML, load the style sheets and make the web page interactive. It makes sense to show some empty boxes with a loader to the user, instead of a blank screen. While the HTML document is getting parsed, when it comes across <script src='asset.js'></script>
, it will make a synchronous HTTP request to the server to fetch asset.js
, and the whole parsing process will be paused until the response comes back. Imagine having a dozen of synchronous static asset references. These could very well be managed just by making use of the async
keyword in script references, like <script src='asset.js' async></script>
. With the introduction of the async
keyword here, the browser will make an asynchronous request to fetch asset.js
without hindering the parsing of the HTML. If a script file is required at a later stage, we can defer the downloading of that file until the entire HTML has been parsed. A script file can be deferred by using the defer
keyword, like <script src='asset.js' defer></script>
.
خاتمة
We've learned a lot of many new things that make for a cool web application. Here's a summary of all of the things we've explored in this article:
- Service workers make good use of the cache to speed up the loading of assets.
- Web push notifications work under the hood.
- We use IndexedDB to store a massive amount of data.
- Some of the optimizations for instant first load, like using HTTP2 and adding headers like
Etag
,last-modified
andIf-None-Match
, prevent the downloading of valid cached assets.
That's all, folks!