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

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

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

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

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

الخلفية

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

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

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

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

منارة

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

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

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

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

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

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

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

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

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

من الأنماط الشائعة لتطبيقات الويب التقدّمية أن يُثبِّت عامل الخدمة جميع ملفات التطبيق الثابتة أثناء مرحلته 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، يُرجى الانتقال إلى الملفات الشخصية للمؤلفين لمعرفة كيفية التواصل معنا: