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

المعلومات التي تعلّمها فريق تطبيق Bulletin عن موظفي الخدمات أثناء تطوير تطبيق ويب تقدّمي (PWA)

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

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

في هذا المنشور الأول، سنتناول القليل من المعلومات الأساسية أولاً ثم نتعمق في جميع المعلومات التي تعلمناها عن العاملين في الخدمة.

الخلفية

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

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

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

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

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

بالنسبة إلى تطبيق "قصص"، استخدمنا Polymer، ولكن يمكن استخدام أي إطار عمل حديث ومتوافق بشكل جيد.

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

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

أنشئ رقم تعريف إذا أمكن

تجنَّب كتابة نص برمجي لمشغّل الخدمة يدويًا. تتطلّب كتابة مشغّلي الخدمات يدويًا إدارة الموارد المخزّنة مؤقتًا وإعادة كتابة المنطق الشائع في معظم مكتبات عاملي الخدمة، مثل 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. يتعطل مشغّل الخدمة ولا يتم تثبيته مطلقًا

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

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

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

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

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

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

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

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

منارة

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

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

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

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

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

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

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

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

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

من الأنماط الشائعة لتطبيقات الويب التقدّمية أن يُثبِّت عامل الخدمة جميع ملفات التطبيق الثابتة أثناء مرحلته 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)));

بسبب الصعوبات التي تواجه اختبار الوحدة لنص عامل الخدمة، أبقينا على النص الأساسي لعامل الخدمة بسيطًا قدر الإمكان، وقسمنا معظم عملية التنفيذ إلى وحدات أخرى. وبما أنّ هذه الملفات كانت مجرد وحدات JS عادية، يمكن اختبارها بسهولة أكبر باستخدام مكتبات اختبار عادية.

يُرجى متابعتنا لمعرفة المزيد من التفاصيل في الجزءَين 2 و3.

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