إنشاء تطبيق ويب تقدّمي (PWA) في Google، الجزء 1

ما تعلّمه فريق Bulletin عن مشغّلات الخدمات أثناء تطوير تطبيق متوافق مع الويب

Douglas Parker
Douglas Parker
Joel Riley
Joel Riley
Dikla Cohen
Dikla Cohen

هذه هي المقالة الأولى من سلسلة من المقالات حول الدروس التي تعلمها فريق Google Bulletin أثناء إنشاء تطبيق متوافق مع الأجهزة الجوّالة (PWA) موجه للعملاء الخارجيين. سنشارك في هذه المشاركات بعض التحديات التي واجهناها، والطرق التي اتّبعناها للتغلب عليها، ونصائح عامة لتجنّب المشاكل. لا يُعدّ هذا القسم بأي شكل من الأشكال نظرة عامة كاملة على التطبيقات المتوافقة مع الأجهزة الجوّالة. والهدف من ذلك هو مشاركة ما تعلمناه من تجربة فريقنا.

في هذه المشاركة الأولى، سنتناول بعض المعلومات الأساسية أولاً ثم نتعمّق في كل المعلومات التي تعلمناها عن مهام الخدمة.

الخلفية

كانت خدمة "نشرة الأخبار" قيد التطوير النشط من منتصف عام 2017 إلى منتصف عام 2019.

سبب اختيارنا إنشاء تطبيق متوافق مع الأجهزة الجوّالة

قبل التعمّق في عملية التطوير، دعونا نفحص السبب الذي جعل إنشاء تطبيق ويب تقدّمي (PWA) خيارًا جذابًا لهذا المشروع:

  • القدرة على تكرار الإجراء بسرعة: هذه المعلومات مفيدة بشكل خاص لأنّه سيتم إطلاق Bulletin في أسواق متعدّدة.
  • قاعدة رمز برمجي واحدة: كان المستخدمون موزّعين بالتساوي تقريبًا بين Android وiOS. من خلال تطبيق الويب التقدّمي، أصبح بإمكاننا إنشاء تطبيق ويب واحد يعمل على كلا النظامَين الأساسيَين. وقد أدّى ذلك إلى زيادة سرعة الفريق وتأثيره.
  • يتم التعديل بسرعة وبشكل مستقل عن سلوك المستخدم. يمكن لتطبيقات الويب التقدّمية التحديث تلقائيًا، ما يؤدي إلى خفض عدد العملاء الذين يستخدمون إصدارات قديمة من التطبيق. لقد تمكّنا من فرض تغييرات جذرية في الخلفية مع فترة قصيرة جدًا من وقت الترحيل للعملاء.
  • دمج سهل مع التطبيقات التابعة للطرف الأول والتطبيقات التابعة لجهات خارجية وكانت عمليات الدمج هذه مطلوبة للتطبيق. وغالبًا ما كان استخدام تطبيق الويب التقدّمي (PWA) يعني فتح عنوان URL ببساطة.
  • التخلّص من المعاناة الناتجة عن تثبيت التطبيق

إطار العمل الذي نتّبعه

بالنسبة إلى تطبيق Bulletin، استخدمنا البوليمر، ولكن سيعمل أي إطارات عمل حديثة ومدعومة جيدًا.

ما تعلمناه عن مشغّلات الخدمات

لا يمكنك الحصول على تطبيق متوافق مع الأجهزة الجوّالة بدون خدمة عامل. تمنحك مشغّلات الخدمات العديد من المزايا، مثل استراتيجيات التخزين المؤقت المتقدّمة وإمكانيات العمل بلا إنترنت والمزامنة في الخلفية وغيرها. ومع أنّ مشغّلات الخدمات تضيف بعض التعقيد، تبيّن لنا أنّ مزاياها تفوق التعقيد المُضاف.

يتم إنشاؤه إذا استطعت

تجنَّب كتابة نص برمجي لمشغّل الخدمات يدويًا. تتطلّب كتابة مشغّلات الخدمات يدويًا إدارة الموارد المخزّنة مؤقتًا وإعادة كتابة المنطق الشائع لمعظم مكتبات مشغّلات الخدمات، مثل Workbox.

مع ذلك، بسبب حزمة التكنولوجيا الداخلية لدينا، لم نتمكّن من استخدام المكتبة لإنشاء مشغّل الخدمات وإدارته. تعكس المعلومات التي نقدّمها أدناه ذلك في بعض الأحيان. انتقِل إلى المشاكل المتعلّقة بمشغّلات الخدمات غير المُنشأة للاطّلاع على مزيد من المعلومات.

ليست كل المكتبات متوافقة مع مهام الخدمة.

تفترض بعض مكتبات JavaScript افتراضات لا تعمل على النحو المتوقّع عند تشغيلها بواسطة عامل خدمة. على سبيل المثال، بافتراض توفّر window أو document، أو استخدام واجهة برمجة تطبيقات غير متاحة لعمال الخدمة (XMLHttpRequest أو مساحة التخزين المحلية أو غير ذلك). تأكَّد من أنّ أي مكتبات مهمة تحتاجها لتطبيقك متوافقة مع مهام الخدمة. بالنسبة إلى تطبيق الويب التقدّمي هذا تحديدًا، أردنا استخدام gapi.js للمصادقة، ولكن تعذّر علينا ذلك لأنّه لم يكن يعمل مع مشغّلي الخدمات. على مؤلفي المكتبات أيضًا تقليل أو إزالة الافتراضات غير الضرورية حول سياق JavaScript كلما أمكن ذلك لدعم حالات استخدام عامل الخدمة، مثل تجنُّب واجهات برمجة التطبيقات غير المتوافقة مع عامل الخدمة وتجنُّب الحالة العميقة.

تجنَّب الوصول إلى IndexedDB أثناء الإعداد.

لا تقرأ IndexedDB عند بدء نص عامل الخدمة، وإلا قد تواجه هذا الموقف غير المرغوب فيه:

  1. لدى المستخدم تطبيق ويب يستخدم الإصدار N من IndexedDB (IDB).
  2. يتمّ نشر تطبيق ويب جديد باستخدام الإصدار N+1 من قاعدة بيانات IDB
  3. يزور المستخدم تطبيق الويب التقدّمي، ما يؤدي إلى تنزيل عامل الخدمة الجديد.
  4. يقرأ مشغّل الخدمة الجديد من قاعدة بيانات IDB قبل تسجيل معالِج حدث install، ما يؤدي إلى بدء دورة ترقية قاعدة بيانات IDB للانتقال من الإصدار N إلى الإصدار N+1.
  5. بما أنّ المستخدم لديه عميل قديم بالإصدار N، تتعطل عملية ترقية الخدمة العاملة لأنّ عمليات الربط النشطة لا تزال مفتوحة للإصدار القديم من قاعدة البيانات.
  6. يتعطل مشغّل الخدمة ولا يتم تثبيته مطلقًا

في حالتنا، تم إلغاء صلاحية ذاكرة التخزين المؤقت عند تثبيت الخدمة العاملة، لذا إذا لم يتم تثبيت الخدمة العاملة أبدًا، لن يتلقّى المستخدمون التطبيق المُحدَّث مطلقًا.

التحلّي بالمرونة

على الرغم من أنّ النصوص البرمجية لمشغّل الخدمات تعمل في الخلفية، يمكن أيضًا إنهاؤها في أي وقت، حتى في منتصف عمليات الإدخال والإخراج (الشبكة أو IDB أو غير ذلك). يجب أن يكون بالإمكان إعادة تنفيذ أي عملية طويلة في أي وقت.

في حال عملية مزامنة حمّلت ملفات كبيرة إلى الخادم وحفظتها في قاعدة بيانات IDB، كان الحلّ الذي وجدناه لعمليات التحميل الجزئية المتقطّعة هو الاستفادة من نظام التحميل المُجدوَل في مكتبة التحميل الداخلية، وحفظ عنوان URL للتحميل المُجدوَل في قاعدة بيانات IDB قبل التحميل، واستخدام عنوان URL هذا لاستئناف عمليةتحميل إذا لم تكتمل في المرة الأولى. قبل أي عملية إدخال/إخراج طويلة، يتم أيضًا حفظ الحالة في قاعدة بيانات IDB للإشارة إلى المرحلة التي وصلنا إليها في العملية لكل سجلّ.

عدم الاعتماد على الحالة الشاملة

ونظرًا لوجود عمال الخدمة في سياق مختلف، فإن العديد من الرموز التي قد تتوقع وجودها غير موجودة. تم تشغيل الكثير من الرموز البرمجية في سياق window، بالإضافة إلى سياق الخدمة العاملة (مثل تسجيل السجلّات ووضع الإشارات والمزامنة وما إلى ذلك). يجب أن يكون الرمز البرمجي دفاعيًا بشأن الخدمات التي يستخدمها، مثل التخزين على الجهاز أو ملفات تعريف الارتباط. يمكنك استخدام globalThis للإشارة إلى الكائن العام بطريقة تعمل في جميع السياقات. استخدِم أيضًا البيانات المخزّنة في المتغيّرات الشاملة بشكل مقتصد، إذ لا يمكن ضمان وقت إنهاء النص البرمجي وإزالة الحالة.

التطوير المحلي

إنّ أحد العناصر الرئيسية في مهام الخدمة هو تخزين الموارد مؤقتًا على الجهاز. ومع ذلك، فإنّ هذا الإجراء يؤدي أثناء التطوير إلى عكس ما تريده تمامًا، خاصةً عند إجراء التعديلات بشكل بطيء. لا يزال عليك تثبيت عامل الخادم حتى تتمكّن من تصحيح أخطاء المشاكل المتعلّقة به أو العمل مع واجهات برمجة تطبيقات أخرى، مثل المزامنة في الخلفية أو الإشعارات. في Chrome، يمكنك إجراء ذلك من خلال "أدوات مطوّري البرامج في Chrome" من خلال تفعيل مربّع الاختيار الاستبعاد للشبكة (لوحة التطبيق > لوحة عمال الخدمة) بالإضافة إلى تفعيل مربّع الاختيار إيقاف ذاكرة التخزين المؤقت في لوحة الشبكة لإيقاف ذاكرة التخزين المؤقت أيضًا. لتغطية المزيد من المتصفّحات، اخترنا حلًا مختلفًا من خلال تضمين علامة لإيقاف ميزة التخزين المؤقت في الخدمة العاملة التي تكون مفعّلة تلقائيًا في ملف compiling المطوّر. يضمن ذلك حصول المطوّرين دائمًا على أحدث التغييرات بدون أي مشاكل في التخزين المؤقت. من المهم تضمين العنوان Cache-Control: no-cache أيضًا لمنع المتصفّح من تخزين أي مواد عرض.

منارة

يوفّر Lighthouse عددًا من أدوات تصحيح الأخطاء المفيدة لتطبيقات الويب التقدّمية. تفحص هذه الأداة الموقع الإلكتروني وتنشئ تقارير تتناول تطبيقات الويب المتقدّمة والأداء و إمكانية الوصول وتحسين محركات البحث وأفضل الممارسات الأخرى. ننصحك بتشغيل Lighthouse في وضع دمج متواصل لتنبيهك في حال مخالفة أحد المعايير اللازمة لتطبيق الويب التقدّمي. حدثت لنا هذه المشكلة مرة واحدة، حيث لم يتم تثبيت الخدمة ولم نلاحظ ذلك قبل طرح الإصدار في قناة الإصدار العلني. كان بإمكاننا منع حدوث ذلك باستخدام Lighthouse كجزء من عملية التطوير المتكامل.

اتّباع أسلوب التسليم المستمر

ولأنّ مهام الخدمة يمكن أن يتم تحديثها تلقائيًا، لا يمكن للمستخدمين الحد من الترقيات. وهذا يقلل بشكل كبير من عدد العملاء غير الجدد بشكل عام. عندما يفتح المستخدم تطبيقنا، يعرض عامل الخدمة العميل القديم بينما ينزِّل العميل الجديد بشكلٍ كسول. بعد تنزيل العميل الجديد، سيُطلب من المستخدم إعادة تحميل الصفحة للوصول إلى الميزات الجديدة. حتى إذا تجاهل العميل هذا الطلب، سيتلقّى الإصدار الجديد من العميل في المرة التالية التي يُعيد فيها تحميل الصفحة. ونتيجةً لذلك، من الصعب جدًا على المستخدم رفض التحديثات بالطريقة نفسها التي يمكنه بها رفض تحديثات تطبيقات iOS/Android.

وقد تمكّنا من فرض تغييرات كبيرة في الخلفية خلال فترة زمنية قصيرة جدًا لنقل البيانات للعملاء. عادةً ما نمنح المستخدمين شهرًا لتحديث برامج العملاء الأحدث قبل إجراء تغييرات جذرية. وبما أنّ التطبيق كان يعرض البيانات القديمة، كان من الممكن أن تظهر للعملاء الأقدم بيانات قديمة إذا لم يفتح المستخدم التطبيق لفترة طويلة. على نظام التشغيل iOS، تتم إزالتها بعد بضعة أسابيع لذا لا يحدث هذا الموقف. بالنسبة إلى Android، يمكن تخفيف هذه المشكلة من خلال عدم عرض الإعلانات عندما تكون قديمة، أو من خلال إبطال صلاحية المحتوى يدويًا بعد بضعة أسابيع. من الناحية العملية، لم نواجه أبدًا مشاكل من العملاء الذين لم يُجروا أيّ عمليات مؤخرًا. يعتمد مدى صرامة فريق معيّن على حالة الاستخدام المحدّدة له، إلا أنّ تطبيقات الويب التقدّمية (PWA) توفّر مرونة أكبر بكثير من تطبيقات iOS/Android.

الحصول على قيم ملفات تعريف الارتباط في عامل خدمة

يكون من الضروري أحيانًا الوصول إلى قيم ملفات تعريف الارتباط في سياق مشغّل الخدمات. في حالتنا، كان علينا الوصول إلى قيم ملفات تعريف الارتباط لإنشاء رمز مميّز لمصادقة طلبات واجهة برمجة التطبيقات التابعة للطرف الأول. في مشغّل الخدمات، لا تتوفّر واجهات برمجة التطبيقات المتزامنة، مثل document.cookies. يمكنك في أي وقت إرسال رسالة إلى العملاء النشطين (المشغّلين في نافذة) من الخدمة العاملة لطلب قيم ملفات تعريف الارتباط، مع أنّه من الممكن أن يتم تشغيل الخدمة العاملة في الخلفية بدون توفّر أي عملاء مشغّلين في نافذة، مثلاً أثناء المزامنة في الخلفية. لحلّ هذه المشكلة، أنشأنا نقطة نهاية على خادم الواجهة الأمامية التي تعيد ببساطة قيمة ملف تعريف الارتباط إلى العميل. أرسل العامل في الخدمة طلبًا على الشبكة إلى نقطة النهاية هذه وقرأ الردّ للحصول على قيم ملفات تعريف الارتباط.

مع إصدار واجهة برمجة التطبيقات Cookie Store API، لن يكون هذا الحلّ البديل ضروريًا للمتصفحات التي تتيح استخدامها، لأنّها توفّر إمكانية الوصول غير المتزامن إلى ملفات تعريف الارتباط للمتصفّح ويمكن أن يستخدمها عامل الخدمة مباشرةً.

المشاكل المتعلقة بملفات تشغيل الخدمات غير المُنشأة

التأكّد من تغيير نص برمجي عامل الخدمة في حال تغيير أي ملف ثابت مخزّن مؤقتًا

أحد الأنماط الشائعة لتطبيق الويب التقدّمي (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.

في الجزءَين 2 و3 من هذه السلسلة، سنتحدث عن إدارة الوسائط والمشاكل المتعلّقة بنظام التشغيل iOS. إذا أردت معرفة المزيد من المعلومات عن إنشاء تطبيق متوافق مع الأجهزة الجوّالة (PWA) في Google، يُرجى الانتقال إلى الملفات الشخصية للمؤلفين لمعرفة كيفية التواصل معنا: