حياة عامل الخدمات

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

قبل التعمق في Workbox، من المهم فهم دورة حياة عامل الخدمة بحيث يكون ما يفعله Workbox منطقيًا.

تعريف المصطلحات

قبل الدخول في دورة حياة عامل الخدمة، يستحق الأمر تحديد بعض المصطلحات حول كيفية عمل دورة الحياة هذه.

التحكّم والنطاق

تُعد فكرة التحكم بالغة الأهمية لفهم كيفية عمل عاملي الخدمة. الصفحة التي يتم وصفها بأنّها خاضعة لتحكّم أحد مشغّلي الخدمات هي صفحة تتيح لعامل الخدمة اعتراض طلبات الشبكة نيابةً عنه. توفُّر مشغّل الخدمات وقادر على تنفيذ العمل للصفحة ضمن نطاق معيَّن.

النطاق

يُحدَّد نطاق مشغّل الخدمات من خلال موقعه على خادم الويب. إذا كان مشغّل الخدمات يعمل على صفحة تقع في /subdir/index.html ويقع في /subdir/sw.js، يكون نطاق مشغّل الخدمات هو /subdir/. للاطلاع على مفهوم النطاق عمليًا، راجع هذا المثال:

  1. انتقِل إلى https://service-worker-scope-viewer.glitch.me/subdir/index.html. ستظهر رسالة تفيد بأنه لا يوجد مشغّل خدمات يتحكم في الصفحة. ومع ذلك، تسجِّل هذه الصفحة مشغّل خدمات من https://service-worker-scope-viewer.glitch.me/subdir/sw.js.
  2. إعادة تحميل الصفحة بما أنّ عامل الخدمة قد تم تسجيله وأصبح نشطًا الآن، فهو يتحكّم في الصفحة. سيظهر نموذج يحتوي على نطاق مشغّل الخدمات وحالته الحالية وعنوان URL الخاص به. ملاحظة: إنّ الاضطرار إلى إعادة تحميل الصفحة ليس له علاقة بالنطاق، وإنما بدورة حياة عامل الخدمة التي سيتم شرحها لاحقًا.
  3. انتقِل الآن إلى https://service-worker-scope-viewer.glitch.me/index.html. وعلى الرغم من تسجيل عامل خدمة في هذا المصدر، لا تزال هناك رسالة تفيد بأنّه ليس هناك عامل خدمات حالي. ويرجع ذلك إلى أنّ هذه الصفحة لا تندرج ضمن نطاق مشغّل الخدمات المسجَّل.

يحدّ النطاق من الصفحات التي يتحكّم فيها مشغّل الخدمات. في هذا المثال، يعني ذلك أنّ مشغّل الخدمات الذي تم تحميله من /subdir/sw.js يمكنه فقط التحكّم في الصفحات المتوفّرة في /subdir/ أو الشجرة الفرعية الخاصة به.

يوضح ما ورد أعلاه آلية عمل تحديد النطاق تلقائيًا، ولكن يمكن إلغاء الحد الأقصى المسموح به من خلال ضبط عنوان الاستجابة Service-Worker-Allowed، بالإضافة إلى ضبط خيار scope على الطريقة register.

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

العميل

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

دورة حياة عامل الخدمات الجديد

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

تسجيل

التسجيل هو الخطوة الأولية في دورة حياة عامل الخدمة:

<!-- In index.html, for example: -->
<script>
  // Don't register the service worker
  // until the page has fully loaded
  window.addEventListener('load', () => {
    // Is service worker available?
    if ('serviceWorker' in navigator) {
      navigator.serviceWorker.register('/sw.js').then(() => {
        console.log('Service worker registered!');
      }).catch((error) => {
        console.warn('Error registering service worker:');
        console.warn(error);
      });
    }
  });
</script>

يعمل هذا الرمز على سلسلة التعليمات الرئيسية ويقوم بما يلي:

  1. يُرجى الانتظار حتى يتم تحميل الصفحة بالكامل قبل تسجيل المستخدم لأنّ أول زيارة له إلى أحد المواقع الإلكترونية تتم بدون تسجيل مشغّل خدمات. يتجنب ذلك تزايد الطلب على معدل نقل البيانات في حال كان عامل الخدمة يخزن أي شيء بشكل مسبق.
  2. مع أنّ مشغّل الخدمات متوافق بشكل جيد، يساعد الفحص السريع في تجنُّب الأخطاء في المتصفِّحات التي لا يكون متوافقًا معها.
  3. عند تحميل الصفحة بالكامل، وإذا كان مشغّل الخدمات متوافقًا، يجب تسجيل /sw.js.

إليك بعض الأمور الأساسية التي يجب فهمها:

  • لا يتوفّر مشغّلو الخدمة إلا عبر HTTPS أو مضيف محلي.
  • إذا كان محتوى مشغّل الخدمات يحتوي على أخطاء في البنية، يتعذّر التسجيل ويتم تجاهل عامل الخدمة.
  • تذكير: عاملو الخدمة يعملون ضمن نطاق. هنا، النطاق هو الأصل بالكامل، كما تم تحميله من الدليل الجذري.
  • عند بدء التسجيل، يتم ضبط حالة مشغّل الخدمات على 'installing'.

بعد انتهاء التسجيل، يبدأ التثبيت.

تثبيت

يعمل عامل خدمات على تنشيط حدث install الخاص به بعد التسجيل. يتم استدعاء "install" مرة واحدة فقط لكل مشغّل خدمات، ولن يتم تنشيطه مرة أخرى حتى يتم تحديثه. يمكن تسجيل معاودة الاتصال للحدث "install" على مستوى العامل من خلال addEventListener:

// /sw.js
self.addEventListener('install', (event) => {
  const cacheKey = 'MyFancyCacheName_v1';

  event.waitUntil(caches.open(cacheKey).then((cache) => {
    // Add all the assets in the array to the 'MyFancyCacheName_v1'
    // `Cache` instance for later use.
    return cache.addAll([
      '/css/global.bc7b80b7.css',
      '/css/home.fe5d0b23.css',
      '/js/home.d3cc4ba4.js',
      '/js/jquery.43ca4933.js'
    ]);
  }));
});

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

  1. تنشئ هذه الدالة مثيل Cache جديدًا باسم 'MyFancyCache_v1'.
  2. بعد إنشاء ذاكرة التخزين المؤقت، يتم تخزين مصفوفة من عناوين URL لمواد العرض بشكل مسبق باستخدام طريقة addAll غير المتزامنة.

يتعذّر إجراء عملية التثبيت في حال تم رفض الوعود التي تم إرسالها إلى event.waitUntil. وفي حال حدوث ذلك، يتم تجاهل مشغّل الخدمات.

في حال resolve الوعد، تمت عملية التثبيت بنجاح وستتغير حالة عامل الخدمة إلى 'installed' وسيتم تفعيلها بعد ذلك.

التفعيل

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

بالنسبة إلى عاملي الخدمة الجدد، يتم تنشيط activate مباشرةً بعد نجاح install. بعد انتهاء التفعيل، تصبح حالة عامل الخدمة 'activated'. تجدر الإشارة إلى أنّ مشغّل الخدمات الجديد لن يبدأ تلقائيًا في التحكّم في الصفحة إلا بعد الانتقال أو إعادة تحميل الصفحة التالية.

معالجة تحديثات مشغّلي الخدمات

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

عند إجراء التحديثات

ستتحقّق المتصفِّحات من توفُّر تحديثات لعامل الخدمات في الحالات التالية:

  • انتقال المستخدم إلى صفحة ضمن نطاق مشغّل الخدمات.
  • يتم استدعاء navigator.serviceWorker.register() باستخدام عنوان URL مختلف عن مشغّل الخدمات المثبَّت حاليًا، ولكن لا تغيّر عنوان URL لعامل الخدمة.
  • يتم استدعاء navigator.serviceWorker.register() بعنوان URL نفسه الذي يستخدمه مشغّل الخدمات المثبّت، ولكن بنطاق مختلف. مرة أخرى، تجنب ذلك من خلال إبقاء النطاق في جذر الأصل إن أمكن.
  • عند بدء أحداث مثل 'push' أو 'sync' خلال آخر 24 ساعة، ولكن لا داعي للقلق بشأن هذه الأحداث حتى الآن.

كيفية إجراء التعديلات

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

تكتشف المتصفحات التغييرات بطريقتين:

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

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

تشغيل عمليات البحث عن التحديثات يدويًا

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

navigator.serviceWorker.ready.then((registration) => {
  registration.update();
});

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

تثبيت

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

self.addEventListener('install', (event) => {
  const cacheKey = 'MyFancyCacheName_v2';

  event.waitUntil(caches.open(cacheKey).then((cache) => {
    // Add all the assets in the array to the 'MyFancyCacheName_v2'
    // `Cache` instance for later use.
    return cache.addAll([
      '/css/global.ced4aef2.css',
      '/css/home.cbe409ad.css',
      '/js/home.109defa4.js',
      '/js/jquery.38caf32d.js'
    ]);
  }));
});

يختلف شيئان عن أول مثال على حدث install عما سبق:

  1. يتم إنشاء مثيل Cache جديد بمفتاح 'MyFancyCacheName_v2'.
  2. تم تغيير أسماء مواد العرض المخزّنة مؤقتًا.

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

بشكل تلقائي، سيتم تفعيل مشغّل خدمات جديد عندما لا يتحكم البرنامج القديم في أي عملاء. ويحدث ذلك عند إغلاق جميع علامات التبويب المفتوحة للموقع الإلكتروني المعنيّ.

التفعيل

وعندما يتم تثبيت أحد مشغّلي الخدمات المحدَّث وتنتهي مرحلة الانتظار، يتم تفعيله، ويتم تجاهل عامل الخدمات القديم. من المهام الشائعة التي يمكن تنفيذها في حدث activate لدى مشغّل الخدمات المعدّل إزالة ذاكرات التخزين المؤقت القديمة. يمكنك إزالة ذاكرات التخزين المؤقت القديمة من خلال الحصول على مفاتيح جميع مثيلات Cache المفتوحة باستخدام caches.keys وحذف ذاكرات التخزين المؤقت غير المدرَجة في قائمة مسموح بها محدّدة من خلال caches.delete:

self.addEventListener('activate', (event) => {
  // Specify allowed cache keys
  const cacheAllowList = ['MyFancyCacheName_v2'];

  // Get all the currently active `Cache` instances.
  event.waitUntil(caches.keys().then((keys) => {
    // Delete all caches that aren't in the allow list:
    return Promise.all(keys.map((key) => {
      if (!cacheAllowList.includes(key)) {
        return caches.delete(key);
      }
    }));
  }));
});

ولا تُرتب ذاكرات التخزين المؤقت القديمة نفسها. علينا إجراء ذلك بأنفسنا وإلا لن نخاطر بتجاوز حصص التخزين. بما أنّ إصدار 'MyFancyCacheName_v1' من مشغّل الخدمات الأول قديم، يتم تعديل قائمة "السماح بذاكرة التخزين المؤقت" لتحديد 'MyFancyCacheName_v2'، ما يؤدي إلى حذف ذاكرات التخزين المؤقت التي تحمل اسمًا مختلفًا.

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

تستمر دورة الحياة

سواء تم استخدام Workbox لمعالجة نشر مشغّلي الخدمات وتحديثاته أو استخدام Service Worker API بشكل مباشر، من المفيد فهم دورة حياة عامل الخدمة. بهذا الفهم، يجب أن تبدو سلوكيات عاملي الخدمات أكثر منطقية من الغموض.

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