المعلومات التي تعلّمها فريق تطبيق Bulletin عن موظفي الخدمة أثناء تطوير تطبيق الويب التقدّمي (PWA)
وهذه هي أول سلسلة من مشاركات المدونات حول الدروس التي تعلمها فريق Google Bulletin أثناء إنشاء تطبيق ويب تقدّمي (PWA) خارجي. سنشارك في هذه المشاركات بعض التحديات التي واجهناها، والأساليب التي اتخذناها للتغلب عليها، ونصائح عامة لتجنب الصعوبات. هذا لا يعني بأي شكل من الأشكال نظرة عامة كاملة عن تطبيقات الويب التقدّمية. الهدف هو مشاركة الدروس المستفادة من تجربة فريقنا.
في هذا المنشور الأول، سنتناول القليل من المعلومات الأساسية أولاً ثم نتعمق في جميع المعلومات التي تعلمناها عن العاملين في الخدمة.
الخلفية
كان تطبيق "قصص" قيد التطوير المستمر من منتصف عام 2017 إلى منتصف عام 2019.
سبب اختيارنا لإنشاء تطبيق ويب تقدّمي (PWA)
قبل التعمّق في عملية التطوير، دعونا نفحص السبب الذي جعل إنشاء تطبيق ويب تقدّمي (PWA) خيارًا جذابًا لهذا المشروع:
- القدرة على التكرار بسرعة: ويحظى هذا التطبيق بأهمية خاصة لأنّه ستتم تجربة تطبيق "قصص" في أسواق متعددة.
- قاعدة رموز واحدة. تم تقسيم مستخدمينا بالتساوي تقريبًا بين Android وiOS. يعني تطبيق الويب التقدّمي أنه يمكننا إنشاء تطبيق ويب واحد يعمل على كلا النظامين الأساسيين. أدى ذلك إلى زيادة سرعة الفريق وتأثيره.
- يتم التعديل بسرعة وبشكل مستقل عن سلوك المستخدم. يمكن لتطبيقات الويب التقدّمية إجراء تحديث تلقائي، ما يقلّل من عدد العملاء غير النشطين في العالم. لقد تمكنا من طرح تغييرات جذرية في الخلفية مع فترة قصيرة جدًا من وقت النقل للعملاء.
- يمكن دمجه بسهولة مع تطبيقات الطرف الأول والطرف الثالث. وكانت عمليات الدمج هذه مطلوبة للتطبيق. وغالبًا ما كان استخدام تطبيق الويب التقدّمي (PWA) يعني فتح عنوان URL ببساطة.
- التخلّص من المعاناة الناتجة عن تثبيت التطبيق
إطار العمل
بالنسبة إلى تطبيق Bulletin، استخدمنا البوليمر، ولكن سيعمل أي إطارات عمل حديثة ومدعومة جيدًا.
المعلومات التي تعلّمناها عن مشغِّلي الخدمات
لا يمكنك استخدام تطبيق ويب تقدّمي (PWA) بدون مشغّل خدمات. ويمنحك مشغّلو الخدمات قدرًا كبيرًا من الإمكانيات، مثل استراتيجيات التخزين المؤقت المتقدّمة وإمكانيات الاتصال بالإنترنت ومزامنة الخلفية وغير ذلك. ومع أنّ العاملين في الخدمات يضيفون بعض التعقيدات، وجدنا أنّ مزاياهم تفوقت على مستوى التعقيدات الإضافية.
يتم إنشاؤه إذا استطعت
تجنَّب كتابة نص برمجي لمشغّل الخدمات يدويًا. تتطلّب كتابة مشغّلي الخدمات يدويًا إدارة الموارد المخزّنة مؤقتًا وإعادة كتابة المنطق الشائع في معظم مكتبات عاملي الخدمة، مثل Workbox.
مع ذلك، بسبب حزمة التكنولوجيا الداخلية لدينا، لم نتمكّن من استخدام المكتبة لإنشاء مشغّل الخدمات وإدارته. تعكس المعلومات التي نقدّمها أدناه ذلك في بعض الأحيان. يمكنك الانتقال إلى المخاطر التي يواجهها عاملو الخدمات الذين لم يتم إنشاء بياناتهم للحصول على مزيد من المعلومات.
ليست كل المكتبات متوافقة مع مشغّل الخدمات
تضع بعض مكتبات JavaScript افتراضات لا تعمل كما هو متوقع عند تشغيلها من قِبل عامل خدمات. على سبيل
المثال، بافتراض توفّر window
أو document
، أو استخدام واجهة برمجة تطبيقات غير متاحة لمشغّلي الخدمات (XMLHttpRequest
أو مساحة تخزين على الجهاز أو غير ذلك). تأكد من أن أي مكتبات مهمة تحتاجها
لتطبيقك متوافقة مع مشغّلي الخدمات. بالنسبة إلى تطبيق الويب التقدّمي هذا بالتحديد، أردنا استخدام
gapi.js للمصادقة، ولكن لم نتمكّن من ذلك لأنّه لم يكن متوافقًا مع مشغّلي الخدمات. على مؤلفي المكتبات أيضًا تقليل أو إزالة
الافتراضات غير الضرورية حول سياق JavaScript كلما أمكن ذلك لدعم حالات استخدام عاملي الخدمات،
مثل تجنّب واجهات برمجة التطبيقات غير المتوافقة مع مشغّل الخدمة وتجنُّب الحالة
العالمية.
تجنُّب الوصول إلى IndexedDB أثناء الإعداد
لا تقرأ IndexedDB عند تهيئة النص البرمجي لمشغِّل الخدمات، وإلّا يمكن أن تتعرض لهذا الموقف غير المرغوب فيه:
- لدى المستخدم تطبيق ويب يستخدِم الإصدار IndexedDB (IDB) N.
- طرح تطبيق الويب الجديد باستخدام الإصدار N+1 من IDB
- يزور المستخدم تطبيق الويب التقدّمي (PWA)، الذي يؤدي إلى تنزيل مشغّل الخدمات الجديد.
- يقرأ مشغّل الخدمات الجديد من IDB قبل تسجيل معالج أحداث
install
، ما يؤدي إلى بدء دورة ترقية IDB للانتقال من N إلى N+1. - نظرًا لأن المستخدم لديه برنامج قديم بالإصدار N، فإن عملية ترقية عامل الخدمة معلّقة لأن الاتصالات النشطة لا تزال مفتوحة للإصدار القديم من قاعدة البيانات
- عامل الخدمة يتوقّف عن العمل ولا يثبّت أبدًا
وفي هذه الحالة، تم إيقاف ذاكرة التخزين المؤقت عند تثبيت عامل الخدمة، وبالتالي إذا لم يثبِّت عامل الخدمة مطلقًا، لن يتلقّى المستخدمون التطبيق المحدَّث.
التحلّي بالمرونة
على الرغم من أنّ النصوص البرمجية لمشغّل الخدمات تعمل في الخلفية، يمكن أيضًا إنهاؤها في أي وقت، حتى إذا كانت في منتصف عمليات الإدخال والإخراج (الشبكة أو IDB أو غير ذلك). يجب أن تكون أي عملية طويلة الأمد قابلة للاستئناف في أي وقت.
في حالة عملية المزامنة التي تم من خلالها تحميل ملفات كبيرة الحجم إلى الخادم وحفظها في IDB، كان حل عمليات التحميل الجزئية التي تمت مقاطعتها هو الاستفادة من نظام مكتبة التحميل الداخلية القابل للاستئناف، وحفظ عنوان URL للتحميل القابل للاستئناف في IDB قبل التحميل، واستخدام عنوان URL لاستئناف عملية التحميل إذا لم يتم إكمال عملية التحميل الأولى. وأيضًا قبل أي عملية إدخال/إخراج طويلة، تم حفظ الحالة في IDB للإشارة إلى مكان وجودنا في العملية لكل سجل.
لا تعتمد على الحالة العامة
ونظرًا لوجود عمال الخدمة في سياق مختلف، فإن العديد من الرموز التي قد تتوقع وجودها غير موجودة. وتم تشغيل الكثير من الرموز البرمجية في سياق window
بالإضافة إلى سياق مشغّل الخدمات (مثل التسجيل والعلامات والمزامنة وما إلى ذلك). يجب أن يكون الرمز دفاعيًا بشأن الخدمات التي يستخدمها، مثل التخزين المحلي أو ملفات تعريف الارتباط. يمكنك استخدام
globalThis
للإشارة إلى الكائن العام بطريقة تعمل في جميع السياقات. استخدم أيضًا البيانات المخزنة
في المتغيرات العمومية باعتدال، لأنه لا يوجد ضمان لموعد إنهاء النص البرمجي
وإخراج الحالة.
تنمية محلية
يُعد التخزين المؤقت للموارد محليًا أحد المكونات الرئيسية للعاملين في الخدمة. مع ذلك، أثناء مرحلة التطوير، هذا هو عكس ما تريد تحديدًا، وخاصةً عندما يتم إجراء التحديثات بشكل كسول. لا تزال ترغب في تثبيت عامل الخادم حتى تتمكن من تصحيح الأخطاء المرتبطة به أو العمل مع واجهات برمجة تطبيقات أخرى مثل المزامنة في الخلفية أو الإشعارات. على Chrome، يمكنك تحقيق ذلك من خلال أدوات مطوري البرامج في Chrome من خلال تفعيل مربّع الاختيار تجاوز الشبكة (لوحة التطبيق > جزء عاملو الخدمات) بالإضافة إلى تفعيل مربّع الاختيار إيقاف ذاكرة التخزين المؤقت في لوحة الشبكة لإيقاف ذاكرة التخزين المؤقت للذاكرة أيضًا. لتغطية المزيد من المتصفّحات، اخترنا حلاً مختلفًا من خلال تضمين علامة لإيقاف التخزين المؤقت في مشغّل الخدمات الذي يتم تفعيله بشكلٍ تلقائي في إصدارات المطوّرين. يضمن ذلك حصول المطوّرين دائمًا على أحدث التغييرات بدون أي مشاكل في التخزين المؤقت. من
المهم تضمين العنوان Cache-Control: no-cache
بالإضافة إلى منع المتصفّح من
التخزين المؤقت لأي مواد عرض.
منارة
يوفر Lighthouse عددًا من أدوات تصحيح الأخطاء المفيدة لتطبيقات الويب التقدّمية (PWA). ويفحص الموقع الإلكتروني وينشئ تقارير تتناول تطبيقات الويب التقدّمية (PWA) والأداء وتسهيل الاستخدام وتحسين محركات البحث وغيرها من أفضل الممارسات. ننصحك بتشغيل Lighthouse على عمليات الدمج المستمر لتنبيهك في حال تخطي أحد معايير استخدام تطبيق الويب التقدّمي (PWA). لقد حدث هذا بالفعل لنا في إحدى المرات، حيث لم يكن عامل الخدمة يقوم بتثبيته ولم نكن ندرك ذلك قبل البدء بإنتاج أحد التطبيقات. فاستخدام أداة Lighthouse كجزء من CI يمكننا منع ذلك.
تقديم المحتوى بشكل متواصل
نظرًا لأن عاملي الخدمة يمكنهم إجراء التحديث تلقائيًا، يفتقر المستخدمون إلى القدرة على الحد من الترقيات. وهذا يقلل بشكل كبير من عدد العملاء غير الجدد بشكل عام. عندما يفتح المستخدم تطبيقنا، كان عامل الخدمة يخدم العميل القديم أثناء تنزيل البرنامج الجديد بكسول. بعد تنزيل البرنامج الجديد، سيُطلب من المستخدم إعادة تحميل الصفحة للوصول إلى الميزات الجديدة. وحتى إذا تجاهل المستخدم هذا الطلب، سيستلم الإصدار الجديد من البرنامج في المرة التالية التي أعاد فيها تحميل الصفحة. ونتيجةً لذلك، من الصعب جدًا على المستخدم رفض تحديثات تطبيقات iOS/Android بالطريقة نفسها.
وقد تمكّنا من فرض تغييرات كبيرة في الخلفية خلال فترة زمنية قصيرة جدًا لنقل البيانات للعملاء. عادةً، نمنح المستخدمين شهرًا للتحديث إلى عملاء جدد قبل إجراء تغييرات كبيرة. نظرًا لأن التطبيق كان يعمل على الرغم من أنه قديم، كان من الممكن في الواقع للعملاء الأكبر سنًا التواجد في البرية إذا لم يفتح المستخدم التطبيق لفترة طويلة. على نظام التشغيل iOS، يتم طرد عاملي الخدمة بعد أسبوعين، لذلك لا تحدث هذه الحالة. بالنسبة إلى Android، يمكن الحدّ من هذه المشكلة من خلال عدم عرض المحتوى عندما يكون قديمًا أو انتهاء صلاحية المحتوى يدويًا بعد بضعة أسابيع. من الناحية العملية، لم نواجه أبدًا مشكلات من العملاء القدامى. يعتمد مدى صرامة فريق معيّن في حالة الاستخدام المحددة له، إلا أنّ تطبيقات الويب التقدّمية (PWA) توفّر مرونة أكبر بكثير من تطبيقات iOS/Android.
الحصول على قيم ملفات تعريف الارتباط في مشغّل خدمات
يكون من الضروري أحيانًا الوصول إلى قيم ملفات تعريف الارتباط في سياق مشغّل الخدمات. في حالتنا هذه، علينا الوصول إلى قيم ملفات تعريف الارتباط لإنشاء رمز مميّز لمصادقة طلبات بيانات الطرف الأول من واجهة برمجة التطبيقات. في مشغّل الخدمات، لا تتوفّر واجهات برمجة التطبيقات المتزامنة، مثل document.cookies
. يمكنك دائمًا إرسال رسالة إلى عملاء نشطين (في إطار) من مشغّل الخدمات لطلب قيم ملفات تعريف الارتباط، على الرغم من أنّه من الممكن أن يعمل عامل الخدمة في الخلفية بدون توفّر أي برامج ضمن النافذة،
مثل أثناء المزامنة في الخلفية. للتغلب على هذا، أنشأنا نقطة نهاية على خادم
الواجهة الأمامية التي ترددت ببساطة قيمة ملف تعريف الارتباط إلى العميل. قدَّم عامل الخدمة طلب شبكة إلى نقطة النهاية هذه وقرأ الاستجابة للحصول على قيم ملفات تعريف الارتباط.
ومع إطلاق واجهة برمجة تطبيقات Cookie Store، من المفترض ألا يكون هذا الحل البديل ضروريًا للمتصفّحات التي تتوافق معه، لأنّه يوفّر إمكانية الوصول غير المتزامن إلى ملفات تعريف الارتباط في المتصفّح ويمكن استخدامه مباشرةً بواسطة مشغّل الخدمات.
المشاكل التي يواجهها مشغّلو الخدمات الذين لم يتم إنشاؤهم
التأكّد من تغيير النص البرمجي لمشغِّل الخدمة في حال تغيير أي ملف ثابت مخزَّن مؤقتًا
أحد الأنماط الشائعة لتطبيق الويب التقدّمي (PWA) هو أن يثبِّت مشغّل الخدمات جميع ملفات التطبيقات الثابتة أثناء مرحلة
install
، ما يتيح للعملاء النقر على ذاكرة التخزين المؤقت في Cache Storage API مباشرةً لجميع
الزيارات اللاحقة . لا يتمّ تثبيت مشغّلي الخدمات إلا عندما يكتشف المتصفّح أنّ النصّ البرمجي
لعامل الخدمة قد تغيّر بطريقة ما، لذا كان علينا التأكّد من تغيير ملف النصّ البرمجي
لعامل الخدمة نفسه بطريقة ما عند تغيير ملف مخزَّن مؤقتًا. وقد تم إجراء ذلك يدويًا من خلال تضمين تجزئة من مجموعة ملفات الموارد الثابتة في النص البرمجي لمشغِّل الخدمات، لذا أنتج كل إصدار ملف JavaScript خاص بمشغِّل الخدمات. يمكن لمكتبات مشغّلي الخدمات مثل Workbox تنفيذ هذه العملية تلقائيًا نيابةً عنك.
اختبار الوحدة
تعمل واجهات برمجة التطبيقات لمشغِّل الخدمات عن طريق إضافة أدوات معالجة الأحداث إلى الكائن العمومي. مثلاً:
self.addEventListener('fetch', (evt) => evt.respondWith(fetch('/foo')));
قد يصعب عليك هذا الاختبار لأنّك تحتاج إلى محاكاة عامل تشغيل الحدث، كائن الحدث، وانتظار معاودة الاتصال باستخدام respondWith()
، ثم انتظار الوعد، قبل التأكيد على النتيجة. ومن أسهل طريقة لتنظيم هذه العملية تفويض جميع عمليات التنفيذ إلى ملف آخر يمكن اختباره بسهولة أكبر.
import fetchHandler from './fetch_handler.js';
self.addEventListener('fetch', (evt) => evt.respondWith(fetchHandler(evt)));
بسبب صعوبات اختبار الوحدة للنص البرمجي الخاص بمشغِّل الخدمات، احتفظنا بالنص البرمجي الخاص بعامل الخدمة الأساسي قدر الإمكان، وقسّمنا معظم مراحل التنفيذ إلى وحدات أخرى. ونظرًا لأن هذه الملفات كانت مجرد وحدات JavaScript قياسية، فيمكن اختبارها بسهولة أكبر باستخدام مكتبات الاختبار القياسية.
ترقَّب الجزء الثاني والثالث.
سنتحدث في الجزءَين 2 و3 من هذه السلسلة عن إدارة الوسائط والمشاكل المتعلّقة بنظام التشغيل iOS. إذا كنت تريد أن تسألنا المزيد حول إنشاء تطبيق ويب تقدّمي (PWA) على Google، يمكنك الانتقال إلى الملفات الشخصية للمؤلفين لمعرفة كيفية التواصل معنا: